@wrongstack/tools 0.141.0 → 0.155.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/builtin.js CHANGED
@@ -1,18 +1,40 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
3
  import { buildChildEnv, expectDefined, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan, loadTasks, emptyTaskFile, saveTasks, computeTaskItemProgress, formatTaskList, resolveWstackPaths } from '@wrongstack/core';
4
- import * as fs12 from 'node:fs/promises';
5
- import * as path from 'node:path';
4
+ import * as fs from 'node:fs';
5
+ import { statSync, writeFileSync, mkdirSync } from 'node:fs';
6
+ import * as path2 from 'node:path';
6
7
  import { resolve, sep, dirname } from 'node:path';
8
+ import * as fs13 from 'node:fs/promises';
7
9
  import * as os from 'node:os';
8
10
  import { createRequire } from 'node:module';
9
- import * as fs from 'node:fs';
10
- import { statSync, writeFileSync, mkdirSync } from 'node:fs';
11
11
  import * as ts from 'typescript';
12
12
  import * as dns from 'node:dns/promises';
13
13
  import * as net from 'node:net';
14
14
  import { Agent } from 'undici';
15
15
 
16
+ // src/_spawn-stream.ts
17
+ function resolveWin32Command(cmd) {
18
+ if (process.platform !== "win32") return cmd;
19
+ if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd)) {
20
+ return cmd;
21
+ }
22
+ const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
23
+ const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
24
+ for (const dir of pathDirs) {
25
+ const base = path2.join(dir, cmd);
26
+ for (const ext of pathext) {
27
+ const full = `${base}${ext}`;
28
+ try {
29
+ fs.accessSync(full, fs.constants.X_OK);
30
+ return full;
31
+ } catch {
32
+ }
33
+ }
34
+ }
35
+ return cmd;
36
+ }
37
+
16
38
  // src/_spawn-stream.ts
17
39
  async function* spawnStream(opts) {
18
40
  const max = opts.maxBytes ?? 2e5;
@@ -21,11 +43,14 @@ async function* spawnStream(opts) {
21
43
  let stderr = "";
22
44
  let pending = "";
23
45
  let error;
24
- const child = spawn(opts.cmd, opts.args, {
46
+ const cmd = resolveWin32Command(opts.cmd);
47
+ const needsShell = process.platform === "win32" && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
48
+ const child = spawn(cmd, opts.args, {
25
49
  cwd: opts.cwd,
26
50
  signal: opts.signal,
27
51
  env: buildChildEnv(),
28
- stdio: ["ignore", "pipe", "pipe"]
52
+ stdio: ["ignore", "pipe", "pipe"],
53
+ ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
29
54
  });
30
55
  const queue = [];
31
56
  let waiter;
@@ -107,13 +132,13 @@ async function detectPackageManager(cwd) {
107
132
  return "npm";
108
133
  }
109
134
  function resolvePath(input, ctx) {
110
- return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
135
+ return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.cwd, input);
111
136
  }
112
137
  function ensureInsideRoot(absPath, ctx) {
113
- const root = path.resolve(ctx.projectRoot);
114
- const target = path.resolve(absPath);
115
- const rel = path.relative(root, target);
116
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
138
+ const root = path2.resolve(ctx.projectRoot);
139
+ const target = path2.resolve(absPath);
140
+ const rel = path2.relative(root, target);
141
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
117
142
  throw new Error(`Path "${absPath}" is outside project root "${root}"`);
118
143
  }
119
144
  return target;
@@ -122,23 +147,23 @@ function safeResolve(input, ctx) {
122
147
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
123
148
  }
124
149
  async function assertRealInsideRoot(absPath, ctx) {
125
- const realRoot = await fs12.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
150
+ const realRoot = await fs13.realpath(ctx.projectRoot).catch(() => path2.resolve(ctx.projectRoot));
126
151
  let probe = absPath;
127
152
  for (; ; ) {
128
153
  let real;
129
154
  try {
130
- real = await fs12.realpath(probe);
155
+ real = await fs13.realpath(probe);
131
156
  } catch (err) {
132
157
  if (err.code === "ENOENT") {
133
- const parent = path.dirname(probe);
158
+ const parent = path2.dirname(probe);
134
159
  if (parent === probe) return;
135
160
  probe = parent;
136
161
  continue;
137
162
  }
138
163
  throw err;
139
164
  }
140
- const rel = path.relative(realRoot, real);
141
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
165
+ const rel = path2.relative(realRoot, real);
166
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
142
167
  throw new Error(
143
168
  `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
144
169
  );
@@ -1142,7 +1167,7 @@ var IndexStore = class {
1142
1167
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
1143
1168
  fs.mkdirSync(this.indexDir, { recursive: true });
1144
1169
  const Database = loadDatabaseSync();
1145
- this.db = new Database(path.join(this.indexDir, DB_FILE));
1170
+ this.db = new Database(path2.join(this.indexDir, DB_FILE));
1146
1171
  this.initSchema();
1147
1172
  }
1148
1173
  initSchema() {
@@ -1440,7 +1465,7 @@ var IndexStore = class {
1440
1465
  }));
1441
1466
  }
1442
1467
  sizeBytes() {
1443
- const dbPath = path.join(this.indexDir, DB_FILE);
1468
+ const dbPath = path2.join(this.indexDir, DB_FILE);
1444
1469
  try {
1445
1470
  return fs.statSync(dbPath).size;
1446
1471
  } catch {
@@ -1879,10 +1904,10 @@ func formatType(t ast.Expr) string {
1879
1904
  }
1880
1905
  `;
1881
1906
  function syncGoParse(filePath, content, lang) {
1882
- const tmpDir = path.join(os.tmpdir(), "ws-go-parse");
1907
+ const tmpDir = path2.join(os.tmpdir(), "ws-go-parse");
1883
1908
  try {
1884
1909
  mkdirSync(tmpDir, { recursive: true });
1885
- const scriptPath = path.join(tmpDir, "parse.go");
1910
+ const scriptPath = path2.join(tmpDir, "parse.go");
1886
1911
  writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
1887
1912
  const stdout = execFileSync("go", ["run", scriptPath], {
1888
1913
  input: content,
@@ -2126,9 +2151,9 @@ print(json.dumps([s.to_dict() for s in syms]))
2126
2151
  `;
2127
2152
  function syncPyParse(filePath, lang) {
2128
2153
  try {
2129
- const tmpDir = path.join(os.tmpdir(), "ws-py-parse");
2154
+ const tmpDir = path2.join(os.tmpdir(), "ws-py-parse");
2130
2155
  mkdirSync(tmpDir, { recursive: true });
2131
- const scriptPath = path.join(tmpDir, "parse.py");
2156
+ const scriptPath = path2.join(tmpDir, "parse.py");
2132
2157
  writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
2133
2158
  const stdout = execFileSync("python", [scriptPath, filePath], {
2134
2159
  timeout: 15e3,
@@ -2169,7 +2194,7 @@ function parseSymbols4(opts) {
2169
2194
  function checkNativeParser() {
2170
2195
  try {
2171
2196
  execFileSync("rustc", ["--version"], { stdio: "pipe" });
2172
- const toolsDir = path.join(process.cwd(), "tools");
2197
+ const toolsDir = path2.join(process.cwd(), "tools");
2173
2198
  try {
2174
2199
  execFileSync(
2175
2200
  "cargo",
@@ -2179,7 +2204,7 @@ function checkNativeParser() {
2179
2204
  "--format-version",
2180
2205
  "1",
2181
2206
  "--manifest-path",
2182
- path.join(toolsDir, "Cargo.toml")
2207
+ path2.join(toolsDir, "Cargo.toml")
2183
2208
  ],
2184
2209
  { stdio: "pipe" }
2185
2210
  );
@@ -2193,13 +2218,13 @@ function checkNativeParser() {
2193
2218
  }
2194
2219
  function tryNativeParse(file, content) {
2195
2220
  try {
2196
- const toolsDir = path.join(process.cwd(), "tools");
2197
- const crateDir = path.join(toolsDir, "syn-parser");
2198
- const tmpFile = path.join(crateDir, "src", "input.rs");
2221
+ const toolsDir = path2.join(process.cwd(), "tools");
2222
+ const crateDir = path2.join(toolsDir, "syn-parser");
2223
+ const tmpFile = path2.join(crateDir, "src", "input.rs");
2199
2224
  writeFileSync(tmpFile, content, "utf8");
2200
2225
  const result = spawnSync(
2201
2226
  "cargo",
2202
- ["run", "--manifest-path", path.join(toolsDir, "Cargo.toml")],
2227
+ ["run", "--manifest-path", path2.join(toolsDir, "Cargo.toml")],
2203
2228
  {
2204
2229
  cwd: process.cwd(),
2205
2230
  encoding: "utf8",
@@ -2297,7 +2322,7 @@ function parseSymbols5(opts) {
2297
2322
  function regexParse2(opts) {
2298
2323
  const { file, content, lang } = opts;
2299
2324
  const symbols = [];
2300
- const basename2 = path.basename(file).toLowerCase();
2325
+ const basename2 = path2.basename(file).toLowerCase();
2301
2326
  const isPackageJson = basename2 === "package.json";
2302
2327
  const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
2303
2328
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -2323,11 +2348,11 @@ function regexParse2(opts) {
2323
2348
  const line = lineFromOffset(offset);
2324
2349
  symbols.push(
2325
2350
  makeSymbol({
2326
- name: path.basename(file),
2351
+ name: path2.basename(file),
2327
2352
  kind: "object",
2328
2353
  line,
2329
2354
  col: 0,
2330
- signature: `"${path.basename(file)}" = { ... }`,
2355
+ signature: `"${path2.basename(file)}" = { ... }`,
2331
2356
  file,
2332
2357
  lang
2333
2358
  })
@@ -2681,7 +2706,7 @@ function compileGitignore(lines) {
2681
2706
  async function loadGitignoreMatcher(projectRoot) {
2682
2707
  let lines = [];
2683
2708
  try {
2684
- const raw = await fs12.readFile(path.join(projectRoot, ".gitignore"), "utf8");
2709
+ const raw = await fs13.readFile(path2.join(projectRoot, ".gitignore"), "utf8");
2685
2710
  lines = raw.split("\n");
2686
2711
  } catch {
2687
2712
  }
@@ -2723,6 +2748,16 @@ var YIELD_EVERY_N = 50;
2723
2748
  function yieldEventLoop() {
2724
2749
  return new Promise((resolve7) => setImmediate(resolve7));
2725
2750
  }
2751
+ function throwIfAborted(signal) {
2752
+ if (!signal?.aborted) return;
2753
+ if (signal.reason instanceof Error) throw signal.reason;
2754
+ throw new Error(
2755
+ typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
2756
+ );
2757
+ }
2758
+ function isAbortError(err) {
2759
+ return err instanceof DOMException && err.name === "AbortError";
2760
+ }
2726
2761
  var DEFAULT_IGNORE = [
2727
2762
  "node_modules",
2728
2763
  ".git",
@@ -2734,7 +2769,7 @@ var DEFAULT_IGNORE = [
2734
2769
  "__snapshots__",
2735
2770
  ".nyc_output"
2736
2771
  ];
2737
- async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
2772
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2738
2773
  const results = [];
2739
2774
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2740
2775
  const globs = [
@@ -2749,23 +2784,30 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
2749
2784
  { ext: ".yaml", pat: compileGlob("**/*.yaml") },
2750
2785
  { ext: ".yml", pat: compileGlob("**/*.yml") }
2751
2786
  ];
2787
+ let dirCount = 0;
2752
2788
  const walk = async (dir) => {
2789
+ throwIfAborted(signal);
2790
+ if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
2791
+ await yieldEventLoop();
2792
+ throwIfAborted(signal);
2793
+ }
2753
2794
  let entries;
2754
2795
  try {
2755
- entries = await fs12.readdir(dir, { withFileTypes: true });
2796
+ entries = await fs13.readdir(dir, { withFileTypes: true });
2756
2797
  } catch {
2757
2798
  return;
2758
2799
  }
2800
+ dirCount++;
2759
2801
  for (const e of entries) {
2760
2802
  if (ignoreSet.has(e.name)) continue;
2761
- const full = path.join(dir, e.name);
2762
- const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
2803
+ const full = path2.join(dir, e.name);
2804
+ const rel = path2.relative(projectRoot, full).replace(/\\/g, "/");
2763
2805
  if (e.isDirectory()) {
2764
2806
  if (isGitIgnored(rel, true)) continue;
2765
2807
  await walk(full);
2766
2808
  } else if (e.isFile()) {
2767
2809
  if (isGitIgnored(rel, false)) continue;
2768
- const ext = path.extname(e.name);
2810
+ const ext = path2.extname(e.name);
2769
2811
  for (const { ext: extName, pat } of globs) {
2770
2812
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
2771
2813
  results.push(full);
@@ -2800,7 +2842,7 @@ async function parseFile(file, content, lang) {
2800
2842
  }
2801
2843
  }
2802
2844
  async function runIndexer(_ctx, opts) {
2803
- const { projectRoot, force = false, langs, ignore = [], indexDir } = opts;
2845
+ const { projectRoot, force = false, langs, ignore = [], indexDir, signal } = opts;
2804
2846
  const store = new IndexStore(projectRoot, { indexDir });
2805
2847
  const startMs = Date.now();
2806
2848
  const errors = [];
@@ -2810,9 +2852,9 @@ async function runIndexer(_ctx, opts) {
2810
2852
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2811
2853
  let files;
2812
2854
  if (opts.files && opts.files.length > 0) {
2813
- files = opts.files.map((f) => path.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path.relative(projectRoot, f).replace(/\\/g, "/"), false));
2855
+ files = opts.files.map((f) => path2.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path2.relative(projectRoot, f).replace(/\\/g, "/"), false));
2814
2856
  } else {
2815
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored);
2857
+ files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
2816
2858
  }
2817
2859
  if (langs && langs.length > 0) {
2818
2860
  const langSet = new Set(langs);
@@ -2831,11 +2873,14 @@ async function runIndexer(_ctx, opts) {
2831
2873
  _setIndexProgress(fi + 1, files.length);
2832
2874
  if (fi > 0 && fi % YIELD_EVERY_N === 0) {
2833
2875
  await yieldEventLoop();
2876
+ throwIfAborted(signal);
2834
2877
  }
2835
2878
  let stat10;
2836
2879
  try {
2837
- stat10 = await fs12.stat(file);
2838
- } catch {
2880
+ const statOpts = signal ? { signal } : {};
2881
+ stat10 = await fs13.stat(file, statOpts);
2882
+ } catch (e) {
2883
+ if (isAbortError(e)) throw e;
2839
2884
  store.deleteFile(file);
2840
2885
  continue;
2841
2886
  }
@@ -2853,8 +2898,9 @@ async function runIndexer(_ctx, opts) {
2853
2898
  store.deleteSymbolsForFile(file);
2854
2899
  let content;
2855
2900
  try {
2856
- content = await fs12.readFile(file, "utf8");
2901
+ content = await fs13.readFile(file, { encoding: "utf8", signal });
2857
2902
  } catch (e) {
2903
+ if (isAbortError(e)) throw e;
2858
2904
  errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
2859
2905
  continue;
2860
2906
  }
@@ -2903,7 +2949,7 @@ async function runIndexer(_ctx, opts) {
2903
2949
  }
2904
2950
  for (const [file_] of existingMeta) {
2905
2951
  try {
2906
- await fs12.stat(file_);
2952
+ await fs13.stat(file_);
2907
2953
  } catch {
2908
2954
  store.deleteFile(file_);
2909
2955
  }
@@ -2944,12 +2990,13 @@ var codebaseIndexTool = {
2944
2990
  }
2945
2991
  }
2946
2992
  },
2947
- async execute(input, ctx) {
2993
+ async execute(input, ctx, execOpts) {
2948
2994
  const result = await runIndexer(ctx, {
2949
2995
  projectRoot: ctx.projectRoot,
2950
2996
  force: input.force ?? false,
2951
2997
  langs: input.langs,
2952
- indexDir: codebaseIndexDirOverride(ctx)
2998
+ indexDir: codebaseIndexDirOverride(ctx),
2999
+ signal: execOpts?.signal
2953
3000
  });
2954
3001
  setIndexReady();
2955
3002
  return result;
@@ -2964,7 +3011,7 @@ function tokenise(text) {
2964
3011
  return sanitised.toLowerCase().split(" ").filter(Boolean);
2965
3012
  }
2966
3013
  function splitName(name) {
2967
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_\-]+/g, " ").trim();
3014
+ return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
2968
3015
  }
2969
3016
  function buildIndexableText(name, signature, docComment) {
2970
3017
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -3294,11 +3341,11 @@ function findGitDir(cwd) {
3294
3341
  let dir = cwd;
3295
3342
  for (let i = 0; i < 20; i++) {
3296
3343
  try {
3297
- const stat10 = statSync(path.join(dir, ".git"));
3344
+ const stat10 = statSync(path2.join(dir, ".git"));
3298
3345
  if (stat10.isDirectory()) return dir;
3299
3346
  } catch {
3300
3347
  }
3301
- const parent = path.dirname(dir);
3348
+ const parent = path2.dirname(dir);
3302
3349
  if (parent === dir) break;
3303
3350
  dir = parent;
3304
3351
  }
@@ -3338,9 +3385,9 @@ async function fileDiff(input, ctx, _signal) {
3338
3385
  const results = [];
3339
3386
  for (const file of files) {
3340
3387
  const absPath = safeResolve(file, ctx);
3341
- const stat10 = await fs12.stat(absPath).catch(() => null);
3388
+ const stat10 = await fs13.stat(absPath).catch(() => null);
3342
3389
  if (!stat10?.isFile()) continue;
3343
- const content = await fs12.readFile(absPath, "utf8");
3390
+ const content = await fs13.readFile(absPath, "utf8");
3344
3391
  const lines = content.split(/\r?\n/);
3345
3392
  results.push(formatWithLineNumbers(file, lines));
3346
3393
  }
@@ -3402,7 +3449,7 @@ var documentTool = {
3402
3449
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
3403
3450
  for (const absPath of fileList) {
3404
3451
  try {
3405
- const content = await fs12.readFile(absPath, "utf8");
3452
+ const content = await fs13.readFile(absPath, "utf8");
3406
3453
  filesProcessed++;
3407
3454
  const processed = processFile(
3408
3455
  content,
@@ -3438,7 +3485,7 @@ async function resolveFiles(filesInput, cwd) {
3438
3485
  for (const f of files) {
3439
3486
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
3440
3487
  try {
3441
- const stat10 = await fs12.stat(absPath);
3488
+ const stat10 = await fs13.stat(absPath);
3442
3489
  if (stat10.isFile()) resolved.push(absPath);
3443
3490
  } catch {
3444
3491
  }
@@ -3530,7 +3577,7 @@ var editTool = {
3530
3577
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
3531
3578
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
3532
3579
  const absPath = await safeResolveReal(input.path, ctx);
3533
- const stat10 = await fs12.stat(absPath).catch((err) => {
3580
+ const stat10 = await fs13.stat(absPath).catch((err) => {
3534
3581
  if (err.code === "ENOENT") {
3535
3582
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
3536
3583
  }
@@ -3540,8 +3587,8 @@ var editTool = {
3540
3587
  if (!ctx.hasRead(absPath)) {
3541
3588
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
3542
3589
  }
3543
- const original = await fs12.readFile(absPath, "utf8");
3544
- const updated = await fs12.stat(absPath);
3590
+ const original = await fs13.readFile(absPath, "utf8");
3591
+ const updated = await fs13.stat(absPath);
3545
3592
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
3546
3593
  const lastReadMtime = ctx.lastReadMtime(absPath);
3547
3594
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -3581,7 +3628,7 @@ var editTool = {
3581
3628
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
3582
3629
  const newFile = toStyle(newFileLf, style);
3583
3630
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
3584
- const written = await fs12.stat(absPath);
3631
+ const written = await fs13.stat(absPath);
3585
3632
  ctx.recordRead(absPath, written.mtimeMs);
3586
3633
  ctx.session.recordFileChange({
3587
3634
  path: absPath,
@@ -3806,9 +3853,9 @@ var execTool = {
3806
3853
  allowed: false
3807
3854
  };
3808
3855
  }
3809
- const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
3810
- const rel = path.relative(ctx.projectRoot, requestedCwd);
3811
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
3856
+ const requestedCwd = input.cwd ? path2.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
3857
+ const rel = path2.relative(ctx.projectRoot, requestedCwd);
3858
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
3812
3859
  return {
3813
3860
  command: cmd,
3814
3861
  args,
@@ -3830,11 +3877,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3830
3877
  let stderr = "";
3831
3878
  let killed = false;
3832
3879
  const startedAt = Date.now();
3833
- const child = spawn(cmd, args, {
3880
+ const resolved = resolveWin32Command(cmd);
3881
+ const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3882
+ const child = spawn(resolved, args, {
3834
3883
  cwd,
3835
3884
  signal,
3836
3885
  env: buildChildEnv(sessionId),
3837
- stdio: ["ignore", "pipe", "pipe"]
3886
+ stdio: ["ignore", "pipe", "pipe"],
3887
+ ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3838
3888
  });
3839
3889
  const registry = getProcessRegistry();
3840
3890
  const pid = child.pid;
@@ -4617,7 +4667,7 @@ var globTool = {
4617
4667
  }
4618
4668
  let entries;
4619
4669
  try {
4620
- entries = await fs12.readdir(dir, { withFileTypes: true });
4670
+ entries = await fs13.readdir(dir, { withFileTypes: true });
4621
4671
  } catch {
4622
4672
  return;
4623
4673
  }
@@ -4626,14 +4676,14 @@ var globTool = {
4626
4676
  if (DEFAULT_IGNORE2.includes(name)) continue;
4627
4677
  if (ignored.includes(name)) continue;
4628
4678
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
4629
- const full = path.join(dir, name);
4679
+ const full = path2.join(dir, name);
4630
4680
  if (e.isDirectory()) {
4631
4681
  await walk(full, rel);
4632
4682
  if (truncated) return;
4633
4683
  } else if (e.isFile()) {
4634
4684
  if (re.test(rel) || re.test(name)) {
4635
4685
  try {
4636
- const st = await fs12.stat(full);
4686
+ const st = await fs13.stat(full);
4637
4687
  results.push({ rel: full, mtime: st.mtimeMs });
4638
4688
  if (results.length >= limit) {
4639
4689
  truncated = true;
@@ -4652,7 +4702,7 @@ var globTool = {
4652
4702
  };
4653
4703
  async function readGitignore(dir) {
4654
4704
  try {
4655
- const raw = await fs12.readFile(path.join(dir, ".gitignore"), "utf8");
4705
+ const raw = await fs13.readFile(path2.join(dir, ".gitignore"), "utf8");
4656
4706
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
4657
4707
  } catch {
4658
4708
  return [];
@@ -4670,7 +4720,7 @@ var DANGEROUS_PATTERNS = [
4670
4720
  // Quantifier on alternation with length 2+
4671
4721
  /\([^|)]+\|[^)]+\)[+*][+*]/,
4672
4722
  // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
4673
- /[\(\[][^)\]]*[+*][^)\]]*[\)\]][^)]*\?\??/
4723
+ /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
4674
4724
  ];
4675
4725
  function compileUserRegex(pattern, flags) {
4676
4726
  if (typeof pattern !== "string") {
@@ -4936,7 +4986,7 @@ async function runNative(input, base, mode, limit, signal) {
4936
4986
  if (stopped || signal.aborted) return;
4937
4987
  let entries;
4938
4988
  try {
4939
- entries = await fs12.readdir(dir, { withFileTypes: true });
4989
+ entries = await fs13.readdir(dir, { withFileTypes: true });
4940
4990
  } catch {
4941
4991
  return;
4942
4992
  }
@@ -4944,16 +4994,16 @@ async function runNative(input, base, mode, limit, signal) {
4944
4994
  if (stopped) return;
4945
4995
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
4946
4996
  if (e.isSymbolicLink()) continue;
4947
- const full = path.join(dir, e.name);
4997
+ const full = path2.join(dir, e.name);
4948
4998
  if (e.isDirectory()) {
4949
4999
  await walk(full);
4950
5000
  } else if (e.isFile()) {
4951
5001
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
4952
5002
  if (globRe) globRe.lastIndex = 0;
4953
5003
  try {
4954
- const stat10 = await fs12.stat(full);
5004
+ const stat10 = await fs13.stat(full);
4955
5005
  if (stat10.size > 1e6) continue;
4956
- const head = await fs12.readFile(full);
5006
+ const head = await fs13.readFile(full);
4957
5007
  if (isBinaryBuffer(head)) continue;
4958
5008
  const text = head.toString("utf8");
4959
5009
  const lines = text.split(/\r?\n/);
@@ -5134,7 +5184,7 @@ var jsonTool = {
5134
5184
  let raw;
5135
5185
  if (input.file) {
5136
5186
  try {
5137
- raw = await fs12.readFile(input.file, "utf8");
5187
+ raw = await fs13.readFile(input.file, "utf8");
5138
5188
  } catch {
5139
5189
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
5140
5190
  }
@@ -5172,8 +5222,8 @@ var jsonTool = {
5172
5222
  };
5173
5223
  }
5174
5224
  };
5175
- function query(data, path19) {
5176
- const parts = path19.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5225
+ function query(data, path20) {
5226
+ const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5177
5227
  let current = data;
5178
5228
  for (const part of parts) {
5179
5229
  if (current === null || current === void 0) return void 0;
@@ -5444,7 +5494,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5444
5494
  }
5445
5495
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
5446
5496
  var MAX_TAIL_LINES = 1e5;
5447
- async function fileLogs(path19, lines, filterRe, stream) {
5497
+ async function fileLogs(path20, lines, filterRe, stream) {
5448
5498
  const { createInterface } = await import('node:readline');
5449
5499
  const { createReadStream } = await import('node:fs');
5450
5500
  const entries = [];
@@ -5453,7 +5503,7 @@ async function fileLogs(path19, lines, filterRe, stream) {
5453
5503
  let writeIdx = 0;
5454
5504
  let totalLines = 0;
5455
5505
  const rl = createInterface({
5456
- input: createReadStream(path19),
5506
+ input: createReadStream(path20),
5457
5507
  crlfDelay: Number.POSITIVE_INFINITY
5458
5508
  });
5459
5509
  for await (const line of rl) {
@@ -5474,7 +5524,7 @@ async function fileLogs(path19, lines, filterRe, stream) {
5474
5524
  if (parsed) entries.push(parsed);
5475
5525
  }
5476
5526
  return {
5477
- source: path19,
5527
+ source: path20,
5478
5528
  entries,
5479
5529
  total: entries.length,
5480
5530
  truncated: totalLines > effLines,
@@ -5557,7 +5607,9 @@ function runOutdated(manager, args, cwd, signal) {
5557
5607
  let stdout = "";
5558
5608
  let stderr = "";
5559
5609
  const MAX = 1e5;
5560
- const child = spawn(manager, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
5610
+ const resolved = resolveWin32Command(manager);
5611
+ const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5612
+ const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
5561
5613
  child.stdout?.on("data", (c) => {
5562
5614
  if (stdout.length < MAX) stdout += c.toString();
5563
5615
  });
@@ -5641,9 +5693,9 @@ var patchTool = {
5641
5693
  for (const t of targets) {
5642
5694
  const stripped = stripPathComponents(t, strip);
5643
5695
  if (!stripped) continue;
5644
- const candidate = path.resolve(dir, stripped);
5645
- const rel = path.relative(ctx.projectRoot, candidate);
5646
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
5696
+ const candidate = path2.resolve(dir, stripped);
5697
+ const rel = path2.relative(ctx.projectRoot, candidate);
5698
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
5647
5699
  return {
5648
5700
  applied: 0,
5649
5701
  rejected: 1,
@@ -5653,12 +5705,12 @@ var patchTool = {
5653
5705
  };
5654
5706
  }
5655
5707
  }
5656
- const tmpDir = await fs12.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
5708
+ const tmpDir = await fs13.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
5657
5709
  try {
5658
- await fs12.chmod(tmpDir, 448).catch(() => {
5710
+ await fs13.chmod(tmpDir, 448).catch(() => {
5659
5711
  });
5660
- const patchFile = path.join(tmpDir, "in.diff");
5661
- await fs12.writeFile(patchFile, input.patch, { mode: 384 });
5712
+ const patchFile = path2.join(tmpDir, "in.diff");
5713
+ await fs13.writeFile(patchFile, input.patch, { mode: 384 });
5662
5714
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
5663
5715
  const result = await runPatch(args, dir, opts.signal);
5664
5716
  if (result.exitCode !== 0 && !dryRun) {
@@ -5679,7 +5731,7 @@ var patchTool = {
5679
5731
  message: result.stdout || "patch applied"
5680
5732
  };
5681
5733
  } finally {
5682
- await fs12.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5734
+ await fs13.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5683
5735
  });
5684
5736
  }
5685
5737
  }
@@ -5922,7 +5974,7 @@ var readTool = {
5922
5974
  const absPath = await safeResolveReal(input.path, ctx);
5923
5975
  let stat10;
5924
5976
  try {
5925
- stat10 = await fs12.stat(absPath);
5977
+ stat10 = await fs13.stat(absPath);
5926
5978
  } catch (err) {
5927
5979
  const code = err.code;
5928
5980
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -5934,7 +5986,7 @@ var readTool = {
5934
5986
  if (stat10.size > MAX_BYTES2) {
5935
5987
  throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
5936
5988
  }
5937
- const buf = await fs12.readFile(absPath);
5989
+ const buf = await fs13.readFile(absPath);
5938
5990
  if (isBinaryBuffer(buf)) {
5939
5991
  throw new Error(`read: "${input.path}" appears to be binary`);
5940
5992
  }
@@ -6002,11 +6054,11 @@ var replaceTool = {
6002
6054
  const dryRun = input.dry_run ?? false;
6003
6055
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
6004
6056
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
6005
- const realRoot = await fs12.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
6057
+ const realRoot = await fs13.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
6006
6058
  const results = [];
6007
6059
  let totalReplacements = 0;
6008
6060
  for (const absPath of fileList) {
6009
- const lstat2 = await fs12.lstat(absPath).catch((err) => {
6061
+ const lstat2 = await fs13.lstat(absPath).catch((err) => {
6010
6062
  if (err.code === "ENOENT") return null;
6011
6063
  throw err;
6012
6064
  });
@@ -6014,17 +6066,17 @@ var replaceTool = {
6014
6066
  if (lstat2.isSymbolicLink()) continue;
6015
6067
  let realPath;
6016
6068
  try {
6017
- realPath = await fs12.realpath(absPath);
6069
+ realPath = await fs13.realpath(absPath);
6018
6070
  } catch {
6019
6071
  continue;
6020
6072
  }
6021
- const rel = path.relative(realRoot, realPath);
6022
- if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
6023
- const stat10 = await fs12.stat(realPath).catch(() => null);
6073
+ const rel = path2.relative(realRoot, realPath);
6074
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) continue;
6075
+ const stat10 = await fs13.stat(realPath).catch(() => null);
6024
6076
  if (!stat10 || !stat10.isFile()) continue;
6025
6077
  let content;
6026
6078
  try {
6027
- const buf = await fs12.readFile(realPath);
6079
+ const buf = await fs13.readFile(realPath);
6028
6080
  if (isBinaryBuffer(buf)) continue;
6029
6081
  content = buf.toString("utf8");
6030
6082
  } catch {
@@ -6076,7 +6128,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
6076
6128
  const resolved = [];
6077
6129
  for (const p of parts) {
6078
6130
  const absPath = safeResolve(p, ctx);
6079
- const stat10 = await fs12.stat(absPath).catch(() => null);
6131
+ const stat10 = await fs13.stat(absPath).catch(() => null);
6080
6132
  if (stat10?.isFile()) {
6081
6133
  resolved.push(absPath);
6082
6134
  }
@@ -6131,15 +6183,15 @@ async function globNative(pattern, base, extraGlob) {
6131
6183
  const walk = async (dir) => {
6132
6184
  let entries;
6133
6185
  try {
6134
- entries = await fs12.readdir(dir, { withFileTypes: true });
6186
+ entries = await fs13.readdir(dir, { withFileTypes: true });
6135
6187
  } catch {
6136
6188
  return;
6137
6189
  }
6138
6190
  for (const e of entries) {
6139
6191
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
6140
- const full = path.join(dir, e.name);
6192
+ const full = path2.join(dir, e.name);
6141
6193
  try {
6142
- const stat10 = await fs12.lstat(full);
6194
+ const stat10 = await fs13.lstat(full);
6143
6195
  if (stat10.isSymbolicLink()) continue;
6144
6196
  } catch {
6145
6197
  continue;
@@ -6307,16 +6359,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
6307
6359
  let filesCreated = 0;
6308
6360
  for (const [filePath, content] of Object.entries(templateFiles)) {
6309
6361
  const resolvedPath = substituteVars(filePath, name, vars);
6310
- const joinedPath = path.join(cwd, resolvedPath);
6311
- const root = path.resolve(ctx.projectRoot);
6312
- const target = path.resolve(joinedPath);
6313
- const rel = path.relative(root, target);
6314
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
6362
+ const joinedPath = path2.join(cwd, resolvedPath);
6363
+ const root = path2.resolve(ctx.projectRoot);
6364
+ const target = path2.resolve(joinedPath);
6365
+ const rel = path2.relative(root, target);
6366
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
6315
6367
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
6316
6368
  }
6317
6369
  const fullPath = target;
6318
6370
  if (!dryRun) {
6319
- await fs12.mkdir(path.dirname(fullPath), { recursive: true });
6371
+ await fs13.mkdir(path2.dirname(fullPath), { recursive: true });
6320
6372
  await atomicWrite(fullPath, substituteVars(content, name, vars));
6321
6373
  }
6322
6374
  files.push(resolvedPath);
@@ -6742,7 +6794,7 @@ var testTool = {
6742
6794
  type: "final",
6743
6795
  output: {
6744
6796
  runner: "none",
6745
- exit_code: 1,
6797
+ exit_code: 0,
6746
6798
  tests_run: 0,
6747
6799
  passed: 0,
6748
6800
  failed: 0,
@@ -6772,14 +6824,14 @@ async function detectRunner(cwd) {
6772
6824
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
6773
6825
  for (const f of candidates) {
6774
6826
  try {
6775
- await stat10(path.join(cwd, f));
6827
+ await stat10(path2.join(cwd, f));
6776
6828
  if (f.includes("vitest")) return "vitest";
6777
6829
  if (f.includes("jest")) return "jest";
6778
6830
  if (f.includes("mocha")) return "mocha";
6779
6831
  } catch {
6780
6832
  }
6781
6833
  }
6782
- return "vitest";
6834
+ return null;
6783
6835
  }
6784
6836
  function buildArgs2(runner, input) {
6785
6837
  const args = [];
@@ -7318,7 +7370,7 @@ var treeTool = {
7318
7370
  }
7319
7371
  };
7320
7372
  async function walkDir(dir, depth, opts) {
7321
- const entries = await fs12.readdir(dir, { withFileTypes: true }).catch(() => []);
7373
+ const entries = await fs13.readdir(dir, { withFileTypes: true }).catch(() => []);
7322
7374
  const filtered = entries.filter((e) => {
7323
7375
  if (!opts.showHidden && e.name.startsWith(".")) return false;
7324
7376
  if (opts.exclude.has(e.name)) return false;
@@ -7348,7 +7400,7 @@ async function walkDir(dir, depth, opts) {
7348
7400
  opts.lines.push(opts.prefix + branch + displayName);
7349
7401
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
7350
7402
  const childPrefix = opts.prefix + connector;
7351
- await walkDir(path.join(dir, entry.name), depth + 1, {
7403
+ await walkDir(path2.join(dir, entry.name), depth + 1, {
7352
7404
  ...opts,
7353
7405
  prefix: childPrefix,
7354
7406
  isLast
@@ -7431,8 +7483,8 @@ async function findTsConfig(cwd) {
7431
7483
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
7432
7484
  for (const f of candidates) {
7433
7485
  try {
7434
- const s = await stat10(path.join(cwd, f));
7435
- if (s.isFile()) return path.join(cwd, f);
7486
+ const s = await stat10(path2.join(cwd, f));
7487
+ if (s.isFile()) return path2.join(cwd, f);
7436
7488
  } catch {
7437
7489
  }
7438
7490
  }
@@ -7468,14 +7520,14 @@ var writeTool = {
7468
7520
  let existed = false;
7469
7521
  let prev = "";
7470
7522
  try {
7471
- const stat11 = await fs12.stat(absPath);
7523
+ const stat11 = await fs13.stat(absPath);
7472
7524
  existed = stat11.isFile();
7473
7525
  if (existed) {
7474
7526
  if (!ctx.hasRead(absPath)) {
7475
- prev = await fs12.readFile(absPath, "utf8");
7527
+ prev = await fs13.readFile(absPath, "utf8");
7476
7528
  ctx.recordRead(absPath, stat11.mtimeMs);
7477
7529
  } else {
7478
- prev = await fs12.readFile(absPath, "utf8");
7530
+ prev = await fs13.readFile(absPath, "utf8");
7479
7531
  }
7480
7532
  }
7481
7533
  } catch (err) {
@@ -7486,7 +7538,7 @@ var writeTool = {
7486
7538
  await atomicWrite(absPath, input.content);
7487
7539
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
7488
7540
  + (new file, ${input.content.split("\n").length} lines)`;
7489
- const stat10 = await fs12.stat(absPath);
7541
+ const stat10 = await fs13.stat(absPath);
7490
7542
  ctx.recordRead(absPath, stat10.mtimeMs);
7491
7543
  ctx.session.recordFileChange({
7492
7544
  path: absPath,