@wrongstack/tools 0.141.0 → 0.148.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
  }
@@ -2752,20 +2777,20 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
2752
2777
  const walk = async (dir) => {
2753
2778
  let entries;
2754
2779
  try {
2755
- entries = await fs12.readdir(dir, { withFileTypes: true });
2780
+ entries = await fs13.readdir(dir, { withFileTypes: true });
2756
2781
  } catch {
2757
2782
  return;
2758
2783
  }
2759
2784
  for (const e of entries) {
2760
2785
  if (ignoreSet.has(e.name)) continue;
2761
- const full = path.join(dir, e.name);
2762
- const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
2786
+ const full = path2.join(dir, e.name);
2787
+ const rel = path2.relative(projectRoot, full).replace(/\\/g, "/");
2763
2788
  if (e.isDirectory()) {
2764
2789
  if (isGitIgnored(rel, true)) continue;
2765
2790
  await walk(full);
2766
2791
  } else if (e.isFile()) {
2767
2792
  if (isGitIgnored(rel, false)) continue;
2768
- const ext = path.extname(e.name);
2793
+ const ext = path2.extname(e.name);
2769
2794
  for (const { ext: extName, pat } of globs) {
2770
2795
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
2771
2796
  results.push(full);
@@ -2810,7 +2835,7 @@ async function runIndexer(_ctx, opts) {
2810
2835
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2811
2836
  let files;
2812
2837
  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));
2838
+ files = opts.files.map((f) => path2.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path2.relative(projectRoot, f).replace(/\\/g, "/"), false));
2814
2839
  } else {
2815
2840
  files = await findSourceFiles(projectRoot, ignore, isGitIgnored);
2816
2841
  }
@@ -2834,7 +2859,7 @@ async function runIndexer(_ctx, opts) {
2834
2859
  }
2835
2860
  let stat10;
2836
2861
  try {
2837
- stat10 = await fs12.stat(file);
2862
+ stat10 = await fs13.stat(file);
2838
2863
  } catch {
2839
2864
  store.deleteFile(file);
2840
2865
  continue;
@@ -2853,7 +2878,7 @@ async function runIndexer(_ctx, opts) {
2853
2878
  store.deleteSymbolsForFile(file);
2854
2879
  let content;
2855
2880
  try {
2856
- content = await fs12.readFile(file, "utf8");
2881
+ content = await fs13.readFile(file, "utf8");
2857
2882
  } catch (e) {
2858
2883
  errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
2859
2884
  continue;
@@ -2903,7 +2928,7 @@ async function runIndexer(_ctx, opts) {
2903
2928
  }
2904
2929
  for (const [file_] of existingMeta) {
2905
2930
  try {
2906
- await fs12.stat(file_);
2931
+ await fs13.stat(file_);
2907
2932
  } catch {
2908
2933
  store.deleteFile(file_);
2909
2934
  }
@@ -2964,7 +2989,7 @@ function tokenise(text) {
2964
2989
  return sanitised.toLowerCase().split(" ").filter(Boolean);
2965
2990
  }
2966
2991
  function splitName(name) {
2967
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_\-]+/g, " ").trim();
2992
+ return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
2968
2993
  }
2969
2994
  function buildIndexableText(name, signature, docComment) {
2970
2995
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -3294,11 +3319,11 @@ function findGitDir(cwd) {
3294
3319
  let dir = cwd;
3295
3320
  for (let i = 0; i < 20; i++) {
3296
3321
  try {
3297
- const stat10 = statSync(path.join(dir, ".git"));
3322
+ const stat10 = statSync(path2.join(dir, ".git"));
3298
3323
  if (stat10.isDirectory()) return dir;
3299
3324
  } catch {
3300
3325
  }
3301
- const parent = path.dirname(dir);
3326
+ const parent = path2.dirname(dir);
3302
3327
  if (parent === dir) break;
3303
3328
  dir = parent;
3304
3329
  }
@@ -3338,9 +3363,9 @@ async function fileDiff(input, ctx, _signal) {
3338
3363
  const results = [];
3339
3364
  for (const file of files) {
3340
3365
  const absPath = safeResolve(file, ctx);
3341
- const stat10 = await fs12.stat(absPath).catch(() => null);
3366
+ const stat10 = await fs13.stat(absPath).catch(() => null);
3342
3367
  if (!stat10?.isFile()) continue;
3343
- const content = await fs12.readFile(absPath, "utf8");
3368
+ const content = await fs13.readFile(absPath, "utf8");
3344
3369
  const lines = content.split(/\r?\n/);
3345
3370
  results.push(formatWithLineNumbers(file, lines));
3346
3371
  }
@@ -3402,7 +3427,7 @@ var documentTool = {
3402
3427
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
3403
3428
  for (const absPath of fileList) {
3404
3429
  try {
3405
- const content = await fs12.readFile(absPath, "utf8");
3430
+ const content = await fs13.readFile(absPath, "utf8");
3406
3431
  filesProcessed++;
3407
3432
  const processed = processFile(
3408
3433
  content,
@@ -3438,7 +3463,7 @@ async function resolveFiles(filesInput, cwd) {
3438
3463
  for (const f of files) {
3439
3464
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
3440
3465
  try {
3441
- const stat10 = await fs12.stat(absPath);
3466
+ const stat10 = await fs13.stat(absPath);
3442
3467
  if (stat10.isFile()) resolved.push(absPath);
3443
3468
  } catch {
3444
3469
  }
@@ -3530,7 +3555,7 @@ var editTool = {
3530
3555
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
3531
3556
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
3532
3557
  const absPath = await safeResolveReal(input.path, ctx);
3533
- const stat10 = await fs12.stat(absPath).catch((err) => {
3558
+ const stat10 = await fs13.stat(absPath).catch((err) => {
3534
3559
  if (err.code === "ENOENT") {
3535
3560
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
3536
3561
  }
@@ -3540,8 +3565,8 @@ var editTool = {
3540
3565
  if (!ctx.hasRead(absPath)) {
3541
3566
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
3542
3567
  }
3543
- const original = await fs12.readFile(absPath, "utf8");
3544
- const updated = await fs12.stat(absPath);
3568
+ const original = await fs13.readFile(absPath, "utf8");
3569
+ const updated = await fs13.stat(absPath);
3545
3570
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
3546
3571
  const lastReadMtime = ctx.lastReadMtime(absPath);
3547
3572
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -3581,7 +3606,7 @@ var editTool = {
3581
3606
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
3582
3607
  const newFile = toStyle(newFileLf, style);
3583
3608
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
3584
- const written = await fs12.stat(absPath);
3609
+ const written = await fs13.stat(absPath);
3585
3610
  ctx.recordRead(absPath, written.mtimeMs);
3586
3611
  ctx.session.recordFileChange({
3587
3612
  path: absPath,
@@ -3806,9 +3831,9 @@ var execTool = {
3806
3831
  allowed: false
3807
3832
  };
3808
3833
  }
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)) {
3834
+ const requestedCwd = input.cwd ? path2.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
3835
+ const rel = path2.relative(ctx.projectRoot, requestedCwd);
3836
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
3812
3837
  return {
3813
3838
  command: cmd,
3814
3839
  args,
@@ -3830,11 +3855,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3830
3855
  let stderr = "";
3831
3856
  let killed = false;
3832
3857
  const startedAt = Date.now();
3833
- const child = spawn(cmd, args, {
3858
+ const resolved = resolveWin32Command(cmd);
3859
+ const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3860
+ const child = spawn(resolved, args, {
3834
3861
  cwd,
3835
3862
  signal,
3836
3863
  env: buildChildEnv(sessionId),
3837
- stdio: ["ignore", "pipe", "pipe"]
3864
+ stdio: ["ignore", "pipe", "pipe"],
3865
+ ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3838
3866
  });
3839
3867
  const registry = getProcessRegistry();
3840
3868
  const pid = child.pid;
@@ -4617,7 +4645,7 @@ var globTool = {
4617
4645
  }
4618
4646
  let entries;
4619
4647
  try {
4620
- entries = await fs12.readdir(dir, { withFileTypes: true });
4648
+ entries = await fs13.readdir(dir, { withFileTypes: true });
4621
4649
  } catch {
4622
4650
  return;
4623
4651
  }
@@ -4626,14 +4654,14 @@ var globTool = {
4626
4654
  if (DEFAULT_IGNORE2.includes(name)) continue;
4627
4655
  if (ignored.includes(name)) continue;
4628
4656
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
4629
- const full = path.join(dir, name);
4657
+ const full = path2.join(dir, name);
4630
4658
  if (e.isDirectory()) {
4631
4659
  await walk(full, rel);
4632
4660
  if (truncated) return;
4633
4661
  } else if (e.isFile()) {
4634
4662
  if (re.test(rel) || re.test(name)) {
4635
4663
  try {
4636
- const st = await fs12.stat(full);
4664
+ const st = await fs13.stat(full);
4637
4665
  results.push({ rel: full, mtime: st.mtimeMs });
4638
4666
  if (results.length >= limit) {
4639
4667
  truncated = true;
@@ -4652,7 +4680,7 @@ var globTool = {
4652
4680
  };
4653
4681
  async function readGitignore(dir) {
4654
4682
  try {
4655
- const raw = await fs12.readFile(path.join(dir, ".gitignore"), "utf8");
4683
+ const raw = await fs13.readFile(path2.join(dir, ".gitignore"), "utf8");
4656
4684
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
4657
4685
  } catch {
4658
4686
  return [];
@@ -4670,7 +4698,7 @@ var DANGEROUS_PATTERNS = [
4670
4698
  // Quantifier on alternation with length 2+
4671
4699
  /\([^|)]+\|[^)]+\)[+*][+*]/,
4672
4700
  // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
4673
- /[\(\[][^)\]]*[+*][^)\]]*[\)\]][^)]*\?\??/
4701
+ /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
4674
4702
  ];
4675
4703
  function compileUserRegex(pattern, flags) {
4676
4704
  if (typeof pattern !== "string") {
@@ -4936,7 +4964,7 @@ async function runNative(input, base, mode, limit, signal) {
4936
4964
  if (stopped || signal.aborted) return;
4937
4965
  let entries;
4938
4966
  try {
4939
- entries = await fs12.readdir(dir, { withFileTypes: true });
4967
+ entries = await fs13.readdir(dir, { withFileTypes: true });
4940
4968
  } catch {
4941
4969
  return;
4942
4970
  }
@@ -4944,16 +4972,16 @@ async function runNative(input, base, mode, limit, signal) {
4944
4972
  if (stopped) return;
4945
4973
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
4946
4974
  if (e.isSymbolicLink()) continue;
4947
- const full = path.join(dir, e.name);
4975
+ const full = path2.join(dir, e.name);
4948
4976
  if (e.isDirectory()) {
4949
4977
  await walk(full);
4950
4978
  } else if (e.isFile()) {
4951
4979
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
4952
4980
  if (globRe) globRe.lastIndex = 0;
4953
4981
  try {
4954
- const stat10 = await fs12.stat(full);
4982
+ const stat10 = await fs13.stat(full);
4955
4983
  if (stat10.size > 1e6) continue;
4956
- const head = await fs12.readFile(full);
4984
+ const head = await fs13.readFile(full);
4957
4985
  if (isBinaryBuffer(head)) continue;
4958
4986
  const text = head.toString("utf8");
4959
4987
  const lines = text.split(/\r?\n/);
@@ -5134,7 +5162,7 @@ var jsonTool = {
5134
5162
  let raw;
5135
5163
  if (input.file) {
5136
5164
  try {
5137
- raw = await fs12.readFile(input.file, "utf8");
5165
+ raw = await fs13.readFile(input.file, "utf8");
5138
5166
  } catch {
5139
5167
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
5140
5168
  }
@@ -5172,8 +5200,8 @@ var jsonTool = {
5172
5200
  };
5173
5201
  }
5174
5202
  };
5175
- function query(data, path19) {
5176
- const parts = path19.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5203
+ function query(data, path20) {
5204
+ const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5177
5205
  let current = data;
5178
5206
  for (const part of parts) {
5179
5207
  if (current === null || current === void 0) return void 0;
@@ -5444,7 +5472,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5444
5472
  }
5445
5473
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
5446
5474
  var MAX_TAIL_LINES = 1e5;
5447
- async function fileLogs(path19, lines, filterRe, stream) {
5475
+ async function fileLogs(path20, lines, filterRe, stream) {
5448
5476
  const { createInterface } = await import('node:readline');
5449
5477
  const { createReadStream } = await import('node:fs');
5450
5478
  const entries = [];
@@ -5453,7 +5481,7 @@ async function fileLogs(path19, lines, filterRe, stream) {
5453
5481
  let writeIdx = 0;
5454
5482
  let totalLines = 0;
5455
5483
  const rl = createInterface({
5456
- input: createReadStream(path19),
5484
+ input: createReadStream(path20),
5457
5485
  crlfDelay: Number.POSITIVE_INFINITY
5458
5486
  });
5459
5487
  for await (const line of rl) {
@@ -5474,7 +5502,7 @@ async function fileLogs(path19, lines, filterRe, stream) {
5474
5502
  if (parsed) entries.push(parsed);
5475
5503
  }
5476
5504
  return {
5477
- source: path19,
5505
+ source: path20,
5478
5506
  entries,
5479
5507
  total: entries.length,
5480
5508
  truncated: totalLines > effLines,
@@ -5557,7 +5585,9 @@ function runOutdated(manager, args, cwd, signal) {
5557
5585
  let stdout = "";
5558
5586
  let stderr = "";
5559
5587
  const MAX = 1e5;
5560
- const child = spawn(manager, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
5588
+ const resolved = resolveWin32Command(manager);
5589
+ const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5590
+ const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
5561
5591
  child.stdout?.on("data", (c) => {
5562
5592
  if (stdout.length < MAX) stdout += c.toString();
5563
5593
  });
@@ -5641,9 +5671,9 @@ var patchTool = {
5641
5671
  for (const t of targets) {
5642
5672
  const stripped = stripPathComponents(t, strip);
5643
5673
  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)) {
5674
+ const candidate = path2.resolve(dir, stripped);
5675
+ const rel = path2.relative(ctx.projectRoot, candidate);
5676
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
5647
5677
  return {
5648
5678
  applied: 0,
5649
5679
  rejected: 1,
@@ -5653,12 +5683,12 @@ var patchTool = {
5653
5683
  };
5654
5684
  }
5655
5685
  }
5656
- const tmpDir = await fs12.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
5686
+ const tmpDir = await fs13.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
5657
5687
  try {
5658
- await fs12.chmod(tmpDir, 448).catch(() => {
5688
+ await fs13.chmod(tmpDir, 448).catch(() => {
5659
5689
  });
5660
- const patchFile = path.join(tmpDir, "in.diff");
5661
- await fs12.writeFile(patchFile, input.patch, { mode: 384 });
5690
+ const patchFile = path2.join(tmpDir, "in.diff");
5691
+ await fs13.writeFile(patchFile, input.patch, { mode: 384 });
5662
5692
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
5663
5693
  const result = await runPatch(args, dir, opts.signal);
5664
5694
  if (result.exitCode !== 0 && !dryRun) {
@@ -5679,7 +5709,7 @@ var patchTool = {
5679
5709
  message: result.stdout || "patch applied"
5680
5710
  };
5681
5711
  } finally {
5682
- await fs12.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5712
+ await fs13.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5683
5713
  });
5684
5714
  }
5685
5715
  }
@@ -5922,7 +5952,7 @@ var readTool = {
5922
5952
  const absPath = await safeResolveReal(input.path, ctx);
5923
5953
  let stat10;
5924
5954
  try {
5925
- stat10 = await fs12.stat(absPath);
5955
+ stat10 = await fs13.stat(absPath);
5926
5956
  } catch (err) {
5927
5957
  const code = err.code;
5928
5958
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -5934,7 +5964,7 @@ var readTool = {
5934
5964
  if (stat10.size > MAX_BYTES2) {
5935
5965
  throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
5936
5966
  }
5937
- const buf = await fs12.readFile(absPath);
5967
+ const buf = await fs13.readFile(absPath);
5938
5968
  if (isBinaryBuffer(buf)) {
5939
5969
  throw new Error(`read: "${input.path}" appears to be binary`);
5940
5970
  }
@@ -6002,11 +6032,11 @@ var replaceTool = {
6002
6032
  const dryRun = input.dry_run ?? false;
6003
6033
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
6004
6034
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
6005
- const realRoot = await fs12.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
6035
+ const realRoot = await fs13.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
6006
6036
  const results = [];
6007
6037
  let totalReplacements = 0;
6008
6038
  for (const absPath of fileList) {
6009
- const lstat2 = await fs12.lstat(absPath).catch((err) => {
6039
+ const lstat2 = await fs13.lstat(absPath).catch((err) => {
6010
6040
  if (err.code === "ENOENT") return null;
6011
6041
  throw err;
6012
6042
  });
@@ -6014,17 +6044,17 @@ var replaceTool = {
6014
6044
  if (lstat2.isSymbolicLink()) continue;
6015
6045
  let realPath;
6016
6046
  try {
6017
- realPath = await fs12.realpath(absPath);
6047
+ realPath = await fs13.realpath(absPath);
6018
6048
  } catch {
6019
6049
  continue;
6020
6050
  }
6021
- const rel = path.relative(realRoot, realPath);
6022
- if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
6023
- const stat10 = await fs12.stat(realPath).catch(() => null);
6051
+ const rel = path2.relative(realRoot, realPath);
6052
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) continue;
6053
+ const stat10 = await fs13.stat(realPath).catch(() => null);
6024
6054
  if (!stat10 || !stat10.isFile()) continue;
6025
6055
  let content;
6026
6056
  try {
6027
- const buf = await fs12.readFile(realPath);
6057
+ const buf = await fs13.readFile(realPath);
6028
6058
  if (isBinaryBuffer(buf)) continue;
6029
6059
  content = buf.toString("utf8");
6030
6060
  } catch {
@@ -6076,7 +6106,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
6076
6106
  const resolved = [];
6077
6107
  for (const p of parts) {
6078
6108
  const absPath = safeResolve(p, ctx);
6079
- const stat10 = await fs12.stat(absPath).catch(() => null);
6109
+ const stat10 = await fs13.stat(absPath).catch(() => null);
6080
6110
  if (stat10?.isFile()) {
6081
6111
  resolved.push(absPath);
6082
6112
  }
@@ -6131,15 +6161,15 @@ async function globNative(pattern, base, extraGlob) {
6131
6161
  const walk = async (dir) => {
6132
6162
  let entries;
6133
6163
  try {
6134
- entries = await fs12.readdir(dir, { withFileTypes: true });
6164
+ entries = await fs13.readdir(dir, { withFileTypes: true });
6135
6165
  } catch {
6136
6166
  return;
6137
6167
  }
6138
6168
  for (const e of entries) {
6139
6169
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
6140
- const full = path.join(dir, e.name);
6170
+ const full = path2.join(dir, e.name);
6141
6171
  try {
6142
- const stat10 = await fs12.lstat(full);
6172
+ const stat10 = await fs13.lstat(full);
6143
6173
  if (stat10.isSymbolicLink()) continue;
6144
6174
  } catch {
6145
6175
  continue;
@@ -6307,16 +6337,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
6307
6337
  let filesCreated = 0;
6308
6338
  for (const [filePath, content] of Object.entries(templateFiles)) {
6309
6339
  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)) {
6340
+ const joinedPath = path2.join(cwd, resolvedPath);
6341
+ const root = path2.resolve(ctx.projectRoot);
6342
+ const target = path2.resolve(joinedPath);
6343
+ const rel = path2.relative(root, target);
6344
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
6315
6345
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
6316
6346
  }
6317
6347
  const fullPath = target;
6318
6348
  if (!dryRun) {
6319
- await fs12.mkdir(path.dirname(fullPath), { recursive: true });
6349
+ await fs13.mkdir(path2.dirname(fullPath), { recursive: true });
6320
6350
  await atomicWrite(fullPath, substituteVars(content, name, vars));
6321
6351
  }
6322
6352
  files.push(resolvedPath);
@@ -6742,7 +6772,7 @@ var testTool = {
6742
6772
  type: "final",
6743
6773
  output: {
6744
6774
  runner: "none",
6745
- exit_code: 1,
6775
+ exit_code: 0,
6746
6776
  tests_run: 0,
6747
6777
  passed: 0,
6748
6778
  failed: 0,
@@ -6772,14 +6802,14 @@ async function detectRunner(cwd) {
6772
6802
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
6773
6803
  for (const f of candidates) {
6774
6804
  try {
6775
- await stat10(path.join(cwd, f));
6805
+ await stat10(path2.join(cwd, f));
6776
6806
  if (f.includes("vitest")) return "vitest";
6777
6807
  if (f.includes("jest")) return "jest";
6778
6808
  if (f.includes("mocha")) return "mocha";
6779
6809
  } catch {
6780
6810
  }
6781
6811
  }
6782
- return "vitest";
6812
+ return null;
6783
6813
  }
6784
6814
  function buildArgs2(runner, input) {
6785
6815
  const args = [];
@@ -7318,7 +7348,7 @@ var treeTool = {
7318
7348
  }
7319
7349
  };
7320
7350
  async function walkDir(dir, depth, opts) {
7321
- const entries = await fs12.readdir(dir, { withFileTypes: true }).catch(() => []);
7351
+ const entries = await fs13.readdir(dir, { withFileTypes: true }).catch(() => []);
7322
7352
  const filtered = entries.filter((e) => {
7323
7353
  if (!opts.showHidden && e.name.startsWith(".")) return false;
7324
7354
  if (opts.exclude.has(e.name)) return false;
@@ -7348,7 +7378,7 @@ async function walkDir(dir, depth, opts) {
7348
7378
  opts.lines.push(opts.prefix + branch + displayName);
7349
7379
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
7350
7380
  const childPrefix = opts.prefix + connector;
7351
- await walkDir(path.join(dir, entry.name), depth + 1, {
7381
+ await walkDir(path2.join(dir, entry.name), depth + 1, {
7352
7382
  ...opts,
7353
7383
  prefix: childPrefix,
7354
7384
  isLast
@@ -7431,8 +7461,8 @@ async function findTsConfig(cwd) {
7431
7461
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
7432
7462
  for (const f of candidates) {
7433
7463
  try {
7434
- const s = await stat10(path.join(cwd, f));
7435
- if (s.isFile()) return path.join(cwd, f);
7464
+ const s = await stat10(path2.join(cwd, f));
7465
+ if (s.isFile()) return path2.join(cwd, f);
7436
7466
  } catch {
7437
7467
  }
7438
7468
  }
@@ -7468,14 +7498,14 @@ var writeTool = {
7468
7498
  let existed = false;
7469
7499
  let prev = "";
7470
7500
  try {
7471
- const stat11 = await fs12.stat(absPath);
7501
+ const stat11 = await fs13.stat(absPath);
7472
7502
  existed = stat11.isFile();
7473
7503
  if (existed) {
7474
7504
  if (!ctx.hasRead(absPath)) {
7475
- prev = await fs12.readFile(absPath, "utf8");
7505
+ prev = await fs13.readFile(absPath, "utf8");
7476
7506
  ctx.recordRead(absPath, stat11.mtimeMs);
7477
7507
  } else {
7478
- prev = await fs12.readFile(absPath, "utf8");
7508
+ prev = await fs13.readFile(absPath, "utf8");
7479
7509
  }
7480
7510
  }
7481
7511
  } catch (err) {
@@ -7486,7 +7516,7 @@ var writeTool = {
7486
7516
  await atomicWrite(absPath, input.content);
7487
7517
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
7488
7518
  + (new file, ${input.content.split("\n").length} lines)`;
7489
- const stat10 = await fs12.stat(absPath);
7519
+ const stat10 = await fs13.stat(absPath);
7490
7520
  ctx.recordRead(absPath, stat10.mtimeMs);
7491
7521
  ctx.session.recordFileChange({
7492
7522
  path: absPath,