Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0
Build
* cross build switches to pkgsCross.ucrt64 (shell-win.nix); curl (Schannel),
winpthreads and ncurses 6.6 are linked statically, so the release is
cst-agent.exe + busybox.exe (busybox-w32 Unicode) with no extra DLLs
* windres embeds a manifest with activeCodePage=UTF-8 and a version resource
* ncurses 6.6 Windows console driver patches (mouse button state, A_DIM,
wide-char declarations): scripts/patches/
* scripts/bootstrap_msys2.sh builds the same release natively in MSYS2
UCRT64; PR #1's MINGW64 notes now point there
* vendor/tre-regex (musl's TRE) provides <regex.h> on MinGW
UTF-8
* paths cross the system call boundary as UTF-16 (*W APIs); console code
pages set to 65001 and restored on exit; files are read and written in
binary mode
* child output that is not valid UTF-8 is decoded from the console, OEM or
ANSI system code page
* read_file hides CR; edit_file, apply_patch and write_file keep CRLF line
endings and a UTF-8 BOM
OS layer and processes
* os_dir_* and cst_fnmatch replace the POSIX-only directory walks in
subagent, plugins, skills, undo and fs_tools; realpath / path_under and
symlink rejection work on Windows
* every child goes through CreateProcessW with its own environment block
inside a Job Object, so timeouts and Esc end the whole process tree
Sandbox (auto mode, "auto · lowil")
* run_shell runs with a restricted Low integrity token; the workspace gets
an inheritable Low label once, the sandbox TEMP is labeled Low, and the
credentials file gets a no-read-up label
* drive roots, the profile and system folders are never labeled
* network commands still ask, since Low IL does not block the network;
Access denied and MSYS2's 0xc0000142 offer a rerun outside the sandbox
* /sandbox lists labeled folders, /sandbox reset-labels restores them
TUI
* raster console fonts are switched to Consolas / NSimSun while running
* the home logo falls back to text when the console font lacks U+2580
* mouse drag copy goes to the Windows clipboard
Distribution
* scripts/install_windows.ps1 installs a Gitea release into
%LOCALAPPDATA%\Programs\cst-agent and adds it to the user PATH;
cst-agent-update.cmd reruns it; scripts/release_windows.sh uploads the
zip with the token taken from the environment
* scripts/run_windows.bat is gone; docs/README-windows.md is rewritten
Tests
* new test_lowil, test_win_utf8 and test_os_dir; tests made portable
(cst_test_util.h)
* scripts/win_test.sh and scripts/winvm/ run the suite on English and
Chinese Windows Server VMs as Administrator, a standard user and a
Chinese user name: 29/29 on all six
This commit is contained in:
+48
-77
@@ -12,12 +12,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <fnmatch.h>
|
||||
#include <regex.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
@@ -154,6 +149,9 @@ int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
FILE *fp = fopen(full, "rb");
|
||||
if (!fp)
|
||||
return tool_set_resultf(r, 1, "error: cannot open file: %.200s", path);
|
||||
char bom[3];
|
||||
if (fread(bom, 1, 3, fp) != 3 || !cst_bom_len(bom, 3))
|
||||
rewind(fp);
|
||||
|
||||
long offset = tool_arg_long(c, "offset", 0);
|
||||
long limit = tool_arg_long(c, "limit", 0);
|
||||
@@ -178,6 +176,8 @@ int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
return tool_set_resultf(r, 1, "error: %.200s is a binary file (%zu bytes)",
|
||||
path, n);
|
||||
}
|
||||
cst_strip_bom(buf, &n);
|
||||
n = cst_crlf_to_lf(buf, n); /* edits match LF text and keep CRLF */
|
||||
cst_tool_result_free(r);
|
||||
r->out = buf;
|
||||
r->out_len = n;
|
||||
@@ -217,6 +217,9 @@ int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
}
|
||||
if (numbers)
|
||||
cst_buf_printf(&b, "%6ld|", lineno);
|
||||
if (line.len >= 2 && line.buf[line.len - 2] == '\r' &&
|
||||
line.buf[line.len - 1] == '\n')
|
||||
line.buf[--line.len - 1] = '\n';
|
||||
cst_buf_append(&b, line.buf, line.len);
|
||||
shown++;
|
||||
}
|
||||
@@ -442,6 +445,14 @@ static int plan_edit(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
nwn = n2n;
|
||||
}
|
||||
}
|
||||
} else if (cnt && memchr(nw, '\n', nwn) &&
|
||||
cst_text_is_crlf(pl->oldc, pl->olen)) {
|
||||
size_t n2n;
|
||||
n2 = to_crlf(nw, nwn, &n2n);
|
||||
if (n2) {
|
||||
nw = n2;
|
||||
nwn = n2n;
|
||||
}
|
||||
}
|
||||
int rc = 0;
|
||||
if (cnt == 0) {
|
||||
@@ -539,7 +550,6 @@ int cst_preview_edit_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
/* --- delete_file / move_file --------------------------------------------- */
|
||||
|
||||
static int reject_symlink(const cst_tool_ctx_t *ctx, const char *path) {
|
||||
#ifndef _WIN32
|
||||
char root[OS_PATH_MAX], joined[OS_PATH_MAX];
|
||||
if (os_realpath(ctx->workspace_root, root, sizeof(root)) != 0)
|
||||
return 1;
|
||||
@@ -547,13 +557,9 @@ static int reject_symlink(const cst_tool_ctx_t *ctx, const char *path) {
|
||||
snprintf(joined, sizeof(joined), "%s", path);
|
||||
else if (os_path_join(joined, sizeof(joined), root, path) != 0)
|
||||
return 1;
|
||||
struct stat st;
|
||||
if (lstat(joined, &st) == 0 && S_ISLNK(st.st_mode))
|
||||
os_stat_t st;
|
||||
if (os_lstat(joined, &st) == 0 && st.is_link)
|
||||
return 1;
|
||||
#else
|
||||
(void)ctx;
|
||||
(void)path;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -672,8 +678,6 @@ int cst_tool_move_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
|
||||
/* --- directory walking (list_dir / glob / grep) -------------------------- */
|
||||
|
||||
#ifndef _WIN32
|
||||
|
||||
static const char *const SKIP_DIRS[] = {
|
||||
".git", ".hg", ".svn", "node_modules", ".agent", "__pycache__",
|
||||
".venv", ".cache", ".idea", ".vscode", ".mypy_cache", ".pytest_cache"};
|
||||
@@ -703,7 +707,7 @@ static void ignore_load(const char *root, ignore_t *ig) {
|
||||
char p[OS_PATH_MAX];
|
||||
if (os_path_join(p, sizeof(p), root, ".gitignore") != 0)
|
||||
return;
|
||||
FILE *fp = fopen(p, "r");
|
||||
FILE *fp = fopen(p, "rb");
|
||||
if (!fp)
|
||||
return;
|
||||
char line[512];
|
||||
@@ -749,7 +753,7 @@ static int ignored(const ignore_t *ig, const char *rel, const char *name,
|
||||
const ign_rule_t *r = &ig->v[i];
|
||||
if (r->dir_only && !isdir)
|
||||
continue;
|
||||
if (fnmatch(r->pat, r->anchored ? rel : name, 0) == 0)
|
||||
if (cst_fnmatch(r->pat, r->anchored ? rel : name, 0) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
@@ -758,7 +762,7 @@ static int ignored(const ignore_t *ig, const char *rel, const char *name,
|
||||
typedef struct walker walker_t;
|
||||
/* Return non-zero to stop the walk. */
|
||||
typedef int (*walk_fn)(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth);
|
||||
const os_stat_t *st, int depth);
|
||||
|
||||
struct walker {
|
||||
const char *root; /* workspace realpath */
|
||||
@@ -781,15 +785,13 @@ static void walk_dir(walker_t *w, const char *rel, int depth) {
|
||||
snprintf(dirfull, sizeof(dirfull), "%s/%s", w->root, rel);
|
||||
else
|
||||
snprintf(dirfull, sizeof(dirfull), "%s", w->root);
|
||||
DIR *d = opendir(dirfull);
|
||||
os_dir_t *d = os_dir_open(dirfull);
|
||||
if (!d)
|
||||
return;
|
||||
char **names = NULL;
|
||||
int n = 0, cap = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
|
||||
continue;
|
||||
const os_dirent_t *de;
|
||||
while ((de = os_dir_next(d)) != NULL) {
|
||||
if (n == cap) {
|
||||
cap = cap ? cap * 2 : 32;
|
||||
char **nv = realloc(names, (size_t)cap * sizeof(char *));
|
||||
@@ -797,11 +799,11 @@ static void walk_dir(walker_t *w, const char *rel, int depth) {
|
||||
break;
|
||||
names = nv;
|
||||
}
|
||||
names[n] = strdup(de->d_name);
|
||||
names[n] = strdup(de->name);
|
||||
if (names[n])
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
os_dir_close(d);
|
||||
qsort(names, (size_t)n, sizeof(char *), name_cmp);
|
||||
for (int i = 0; i < n && !w->stop; i++) {
|
||||
char crel[OS_PATH_MAX], cfull[OS_PATH_MAX];
|
||||
@@ -810,10 +812,10 @@ static void walk_dir(walker_t *w, const char *rel, int depth) {
|
||||
int k2 = snprintf(cfull, sizeof(cfull), "%s/%s", w->root, crel);
|
||||
if (k1 >= (int)sizeof(crel) || k2 >= (int)sizeof(cfull))
|
||||
continue; /* path too long */
|
||||
struct stat st;
|
||||
if (lstat(cfull, &st) != 0)
|
||||
os_stat_t st;
|
||||
if (os_lstat(cfull, &st) != 0)
|
||||
continue;
|
||||
int isdir = S_ISDIR(st.st_mode);
|
||||
int isdir = st.is_dir;
|
||||
if (ignored(&w->ig, crel, names[i], isdir))
|
||||
continue;
|
||||
if (w->fn(w, crel, cfull, &st, depth))
|
||||
@@ -861,21 +863,18 @@ typedef struct {
|
||||
|
||||
/* Entries directly inside a directory (for the depth boundary). */
|
||||
static int dir_entries(const char *full) {
|
||||
DIR *d = opendir(full);
|
||||
os_dir_t *d = os_dir_open(full);
|
||||
if (!d)
|
||||
return -1;
|
||||
int n = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0)
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
while (os_dir_next(d))
|
||||
n++;
|
||||
os_dir_close(d);
|
||||
return n;
|
||||
}
|
||||
|
||||
static int list_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
const os_stat_t *st, int depth) {
|
||||
list_ud_t *u = w->ud;
|
||||
if (u->count >= LIST_MAX) {
|
||||
if (++u->more >= LIST_COUNT_MAX) {
|
||||
@@ -888,7 +887,7 @@ static int list_cb(walker_t *w, const char *rel, const char *full,
|
||||
name = name ? name + 1 : rel;
|
||||
for (int i = 0; i < depth; i++)
|
||||
cst_buf_puts(&u->b, " ");
|
||||
if (S_ISDIR(st->st_mode)) {
|
||||
if (st->is_dir) {
|
||||
int k = -1;
|
||||
if (skip_dir_name(name))
|
||||
cst_buf_printf(&u->b, "%s/ (skipped)\n", name);
|
||||
@@ -897,10 +896,10 @@ static int list_cb(walker_t *w, const char *rel, const char *full,
|
||||
k == 1 ? "y" : "ies");
|
||||
else
|
||||
cst_buf_printf(&u->b, "%s/%s\n", name, k == 0 ? " (empty)" : "");
|
||||
} else if (S_ISLNK(st->st_mode))
|
||||
} else if (st->is_link)
|
||||
cst_buf_printf(&u->b, "%s@\n", name);
|
||||
else
|
||||
cst_buf_printf(&u->b, "%s (%lld B)\n", name, (long long)st->st_size);
|
||||
cst_buf_printf(&u->b, "%s (%lld B)\n", name, st->size);
|
||||
u->count++;
|
||||
return 0;
|
||||
}
|
||||
@@ -974,7 +973,7 @@ static int seg_match(const char *pat, const char *path) {
|
||||
pseg[pl] = 0;
|
||||
memcpy(sseg, path, sl);
|
||||
sseg[sl] = 0;
|
||||
if (fnmatch(pseg, sseg, FNM_PERIOD) != 0)
|
||||
if (cst_fnmatch(pseg, sseg, CST_FNM_PERIOD) != 0)
|
||||
return 0;
|
||||
if (!ps)
|
||||
return !ss;
|
||||
@@ -994,11 +993,11 @@ typedef struct {
|
||||
} glob_ud_t;
|
||||
|
||||
static int glob_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
const os_stat_t *st, int depth) {
|
||||
(void)full;
|
||||
(void)depth;
|
||||
glob_ud_t *u = w->ud;
|
||||
if (!S_ISREG(st->st_mode))
|
||||
if (!st->is_reg)
|
||||
return 0;
|
||||
const char *sub = rel;
|
||||
size_t bl = strlen(u->base);
|
||||
@@ -1009,7 +1008,7 @@ static int glob_cb(walker_t *w, const char *rel, const char *full,
|
||||
}
|
||||
const char *name = strrchr(sub, '/');
|
||||
name = name ? name + 1 : sub;
|
||||
int ok = u->basename_only ? fnmatch(u->pattern, name, FNM_PERIOD) == 0
|
||||
int ok = u->basename_only ? cst_fnmatch(u->pattern, name, CST_FNM_PERIOD) == 0
|
||||
: seg_match(u->pattern, sub);
|
||||
if (!ok)
|
||||
return 0;
|
||||
@@ -1025,7 +1024,7 @@ static int glob_cb(walker_t *w, const char *rel, const char *full,
|
||||
return 1;
|
||||
}
|
||||
u->hits[u->n] = strdup(rel);
|
||||
u->mtimes[u->n] = (double)st->st_mtime;
|
||||
u->mtimes[u->n] = st->mtime;
|
||||
if (u->hits[u->n])
|
||||
u->n++;
|
||||
if (u->n >= GLOB_MAX * 20) {
|
||||
@@ -1109,11 +1108,11 @@ typedef struct {
|
||||
} files_ud_t;
|
||||
|
||||
static int files_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
const os_stat_t *st, int depth) {
|
||||
(void)full;
|
||||
(void)depth;
|
||||
files_ud_t *u = w->ud;
|
||||
if (!S_ISREG(st->st_mode))
|
||||
if (!st->is_reg)
|
||||
return 0;
|
||||
if (u->n >= u->max)
|
||||
return 1;
|
||||
@@ -1275,16 +1274,16 @@ static void grep_file(grep_ud_t *u, const char *rel, const char *full) {
|
||||
}
|
||||
|
||||
static int grep_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
const os_stat_t *st, int depth) {
|
||||
(void)depth;
|
||||
grep_ud_t *u = w->ud;
|
||||
if (!S_ISREG(st->st_mode) || st->st_size > READ_FILE_MAX)
|
||||
if (!st->is_reg || st->size > READ_FILE_MAX)
|
||||
return 0;
|
||||
if (u->glob) {
|
||||
const char *name = strrchr(rel, '/');
|
||||
name = name ? name + 1 : rel;
|
||||
int ok = strchr(u->glob, '/') ? fnmatch(u->glob, rel, 0) == 0
|
||||
: fnmatch(u->glob, name, 0) == 0;
|
||||
int ok = strchr(u->glob, '/') ? cst_fnmatch(u->glob, rel, 0) == 0
|
||||
: cst_fnmatch(u->glob, name, 0) == 0;
|
||||
if (!ok)
|
||||
return 0;
|
||||
}
|
||||
@@ -1391,32 +1390,4 @@ int cst_tool_grep(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
return tool_take_buf(r, &out, 0);
|
||||
}
|
||||
|
||||
#else /* _WIN32: directory tools are not implemented natively */
|
||||
|
||||
int cst_tool_list_dir(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
(void)ctx;
|
||||
(void)c;
|
||||
return tool_set_result(r, "error: list_dir is not available on Windows; "
|
||||
"use run_shell", 1);
|
||||
}
|
||||
int cst_workspace_files(const char *ws, char ***out, int max) {
|
||||
(void)ws;
|
||||
(void)max;
|
||||
*out = NULL;
|
||||
return 0;
|
||||
}
|
||||
int cst_tool_glob(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
(void)ctx;
|
||||
(void)c;
|
||||
return tool_set_result(r, "error: glob is not available on Windows", 1);
|
||||
}
|
||||
int cst_tool_grep(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
(void)ctx;
|
||||
(void)c;
|
||||
return tool_set_result(r, "error: grep is not available on Windows", 1);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+48
-3
@@ -1,4 +1,5 @@
|
||||
#include "cst_agent.h"
|
||||
#include "cst_util.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -129,9 +130,9 @@ static int parse_hunk_header(const char *p, size_t n, long *a, long *b,
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cst_patch_apply_buffer(const char *orig, size_t olen, const char *hunks,
|
||||
size_t hlen, char **out, size_t *out_len,
|
||||
int *nhunks, char *err, size_t err_sz) {
|
||||
static int patch_core(const char *orig, size_t olen, const char *hunks,
|
||||
size_t hlen, char **out, size_t *out_len, int *nhunks,
|
||||
char *err, size_t err_sz) {
|
||||
lines_t file = {0}, hl = {0};
|
||||
lines_t old_seq = {0}, new_seq = {0};
|
||||
int rc = -1;
|
||||
@@ -315,3 +316,47 @@ done:
|
||||
free(new_seq.v);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* A CRLF file is patched as LF (the model's hunks usually are) and written
|
||||
* back as CRLF; a BOM is kept out of the first line's context. */
|
||||
int cst_patch_apply_buffer(const char *orig, size_t olen, const char *hunks,
|
||||
size_t hlen, char **out, size_t *out_len,
|
||||
int *nhunks, char *err, size_t err_sz) {
|
||||
size_t bom = orig ? cst_bom_len(orig, olen) : 0;
|
||||
int crlf = orig && cst_text_is_crlf(orig, olen);
|
||||
if (!bom && !crlf)
|
||||
return patch_core(orig, olen, hunks, hlen, out, out_len, nhunks, err,
|
||||
err_sz);
|
||||
if (out)
|
||||
*out = NULL;
|
||||
char *o = malloc(olen - bom + 1), *h = malloc(hlen + 1), *res = NULL;
|
||||
size_t rlen = 0;
|
||||
int rc = -1;
|
||||
if (!o || !h) {
|
||||
snprintf(err, err_sz, "oom");
|
||||
goto done;
|
||||
}
|
||||
memcpy(o, orig + bom, olen - bom);
|
||||
o[olen - bom] = 0;
|
||||
size_t on = olen - bom, hn = hlen;
|
||||
if (hlen)
|
||||
memcpy(h, hunks, hlen);
|
||||
h[hlen] = 0;
|
||||
if (crlf) {
|
||||
on = cst_crlf_to_lf(o, on);
|
||||
hn = cst_crlf_to_lf(h, hn);
|
||||
}
|
||||
rc = patch_core(o, on, h, hn, &res, &rlen, nhunks, err, err_sz);
|
||||
if (rc == 0) {
|
||||
*out = cst_text_restyle(res, rlen, crlf, bom != 0, out_len);
|
||||
if (!*out) {
|
||||
snprintf(err, err_sz, "oom");
|
||||
rc = -1;
|
||||
}
|
||||
}
|
||||
done:
|
||||
free(res);
|
||||
free(o);
|
||||
free(h);
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_tools.h"
|
||||
@@ -182,14 +180,9 @@ const char *cst_tool_profile_name(int p) {
|
||||
#define DYN_MAX 256
|
||||
static cst_tool_def_t *g_dyn[DYN_MAX];
|
||||
static atomic_int g_ndyn;
|
||||
#ifndef _WIN32
|
||||
static pthread_mutex_t g_dyn_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define DYN_LOCK() pthread_mutex_lock(&g_dyn_mu)
|
||||
#define DYN_UNLOCK() pthread_mutex_unlock(&g_dyn_mu)
|
||||
#else
|
||||
#define DYN_LOCK() ((void)0)
|
||||
#define DYN_UNLOCK() ((void)0)
|
||||
#endif
|
||||
|
||||
int cst_tool_count(void) { return NTOOLS + atomic_load(&g_ndyn); }
|
||||
|
||||
|
||||
@@ -238,6 +238,22 @@ static int write_content(const ctl_tool_call_t *call, const char **content,
|
||||
return *content ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Overwriting a CRLF and/or BOM file with plain LF text keeps the file's
|
||||
* style: malloc'd restyled copy, or NULL to write content as given. */
|
||||
static char *keep_text_style(const char *oldc, size_t olen, const char *s,
|
||||
size_t n, size_t *out_len) {
|
||||
if (!oldc)
|
||||
return NULL;
|
||||
int has_crlf = 0;
|
||||
for (size_t i = 1; i < n && !has_crlf; i++)
|
||||
has_crlf = s[i] == '\n' && s[i - 1] == '\r';
|
||||
int crlf = !has_crlf && cst_text_is_crlf(oldc, olen);
|
||||
int bom = cst_bom_len(oldc, olen) && !cst_bom_len(s, n);
|
||||
if (!crlf && !bom)
|
||||
return NULL;
|
||||
return cst_text_restyle(s, n, crlf, 1, out_len);
|
||||
}
|
||||
|
||||
int cst_preview_write_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
char **out) {
|
||||
const char *path = tool_arg(c, "path");
|
||||
@@ -252,11 +268,18 @@ int cst_preview_write_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
char *oldc = NULL;
|
||||
if (cst_sandbox_resolve(ctx->workspace_root, path, full, sizeof(full)) == 0)
|
||||
oldc = tool_slurp(full, &olen, 0);
|
||||
size_t sn = 0;
|
||||
char *styled = keep_text_style(oldc, olen, content, n, &sn);
|
||||
if (styled) {
|
||||
content = styled;
|
||||
n = sn;
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "%s %s (%zu bytes)\n", oldc ? "overwrite" : "create",
|
||||
path, n);
|
||||
tool_append_diff(&b, path, oldc, olen, content, n);
|
||||
free(oldc);
|
||||
free(styled);
|
||||
*out = cst_buf_steal(&b);
|
||||
return 0;
|
||||
}
|
||||
@@ -276,14 +299,22 @@ int cst_tool_write_file(const char *workspace_root, const ctl_tool_call_t *call,
|
||||
|
||||
size_t olen = 0;
|
||||
char *oldc = tool_slurp(full, &olen, 0);
|
||||
size_t sn = 0;
|
||||
char *styled = keep_text_style(oldc, olen, content, n, &sn);
|
||||
if (styled) {
|
||||
content = styled;
|
||||
n = sn;
|
||||
}
|
||||
if (tool_write_whole(full, content, n) != 0) {
|
||||
free(oldc);
|
||||
free(styled);
|
||||
return tool_set_resultf(r, 1, "error: cannot write file: %.200s", path);
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "wrote %s (%zu bytes)\n", path, n);
|
||||
tool_append_diff(&b, path, oldc, olen, content, n);
|
||||
free(oldc);
|
||||
free(styled);
|
||||
return tool_take_buf(r, &b, 0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user