@wrongstack/tools 0.318.0 → 0.319.1

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.
Files changed (101) hide show
  1. package/dist/_edit-match.d.ts +1 -3
  2. package/dist/_util.d.ts +20 -0
  3. package/dist/audit.js +4 -4
  4. package/dist/bash.js +2 -2
  5. package/dist/builtin.d.ts +10 -0
  6. package/dist/builtin.js +46 -44
  7. package/dist/{chunk-TKA4VEX2.js → chunk-2OXHS3KZ.js} +17 -5
  8. package/dist/{chunk-X5K3U6GI.js → chunk-3M7LLN7W.js} +71 -7
  9. package/dist/{chunk-4AEZIFOW.js → chunk-5EUJF3WC.js} +36 -8
  10. package/dist/{chunk-CVALEVGA.js → chunk-64VIJPFV.js} +34 -13
  11. package/dist/{chunk-ZE6KLBUO.js → chunk-6AAOT4PW.js} +58 -10
  12. package/dist/{chunk-A6BRWIXN.js → chunk-6IEBD27U.js} +20 -5
  13. package/dist/{chunk-HTYEO67J.js → chunk-AKHYMB4V.js} +36 -12
  14. package/dist/{chunk-4E42D7UF.js → chunk-ATSKPZUK.js} +2 -2
  15. package/dist/{chunk-H6AWAZ2Y.js → chunk-B4HTSV6Z.js} +57 -6
  16. package/dist/{chunk-5ZYEGP2P.js → chunk-BK732J7I.js} +72 -27
  17. package/dist/{chunk-WI3VDXHD.js → chunk-BSJSNVGA.js} +108 -19
  18. package/dist/{chunk-UKGMDCXT.js → chunk-C2DQKVLO.js} +61 -28
  19. package/dist/{chunk-CIFAIIHI.js → chunk-CFLJFDYV.js} +100 -58
  20. package/dist/{chunk-JJQB6R7Y.js → chunk-CMOUCKGC.js} +6 -6
  21. package/dist/{chunk-MX25MTDJ.js → chunk-DC4KMPJO.js} +2 -2
  22. package/dist/{chunk-VZLX2JZG.js → chunk-DP6CXMJZ.js} +3 -3
  23. package/dist/{chunk-3QQSBUXR.js → chunk-EADULOZV.js} +36 -12
  24. package/dist/{chunk-6RNIFLG4.js → chunk-ELRRTWNP.js} +38 -6
  25. package/dist/{chunk-CPSAF2AU.js → chunk-FQNXYA6W.js} +45 -6
  26. package/dist/{chunk-ZZT45EHF.js → chunk-GMZYW52L.js} +3 -3
  27. package/dist/{chunk-ZLTFLC25.js → chunk-IIF5HIVF.js} +2 -2
  28. package/dist/{chunk-EMRGN4KM.js → chunk-IU4DV2PA.js} +18 -8
  29. package/dist/{chunk-AGUSOFNI.js → chunk-IWS56OY7.js} +30 -7
  30. package/dist/{chunk-MRZOVQ5D.js → chunk-IYKXK356.js} +98 -11
  31. package/dist/{chunk-6GZPSWP3.js → chunk-JDPQOR7C.js} +14 -7
  32. package/dist/{chunk-BLCSYKNN.js → chunk-KY4PXYSO.js} +10 -15
  33. package/dist/{chunk-WJYXMB2R.js → chunk-LOBRWKCR.js} +63 -14
  34. package/dist/{chunk-J5SKD6LN.js → chunk-MNP3GE7H.js} +38 -27
  35. package/dist/{chunk-S2GBLQ5S.js → chunk-O44IMQCN.js} +31 -7
  36. package/dist/{chunk-P7ZQ2FAJ.js → chunk-OG2BEUBY.js} +69 -34
  37. package/dist/{chunk-K23JNFE3.js → chunk-OTINMOKK.js} +233 -45
  38. package/dist/{chunk-PQTONQ4P.js → chunk-QPNYDH5B.js} +19 -5
  39. package/dist/{chunk-UE3UBMCP.js → chunk-RLMX75EM.js} +19 -8
  40. package/dist/{chunk-3U4H6ZRF.js → chunk-RNWUCZ2U.js} +53 -25
  41. package/dist/{chunk-YF3D5INN.js → chunk-RTZWBNTY.js} +121 -91
  42. package/dist/{chunk-SOG7XIRF.js → chunk-TTZOK6IY.js} +2 -2
  43. package/dist/{chunk-6GVXZCQV.js → chunk-UDVKRX5R.js} +2 -2
  44. package/dist/{chunk-CPVLVQYT.js → chunk-UQQS3VNT.js} +43 -9
  45. package/dist/{chunk-VB3MJGCV.js → chunk-VD2LV5WO.js} +82 -13
  46. package/dist/{chunk-5WNDJHT6.js → chunk-VPALDFYM.js} +2 -2
  47. package/dist/{chunk-FJEQZHZN.js → chunk-W2CECGTG.js} +24 -14
  48. package/dist/{chunk-CISOLTOP.js → chunk-X5FOFGXX.js} +72 -33
  49. package/dist/{chunk-CWALNI6K.js → chunk-X6KXNHUO.js} +1 -1
  50. package/dist/{chunk-EEOFQDT6.js → chunk-XRNN44QY.js} +46 -2
  51. package/dist/{chunk-EOOFNYIT.js → chunk-YOKD4GWJ.js} +67 -27
  52. package/dist/{chunk-D33S4RQG.js → chunk-ZUFHAHTE.js} +18 -6
  53. package/dist/codebase-index/generic-parser.d.ts +8 -1
  54. package/dist/codebase-index/index.js +9 -9
  55. package/dist/codebase-index/parser-worker-script.js +3 -3
  56. package/dist/codebase-index/project-server.js +5 -5
  57. package/dist/codebase-index/worker.js +4 -4
  58. package/dist/codebase-index/writer-refs.d.ts +1 -1
  59. package/dist/design.d.ts +1 -1
  60. package/dist/design.js +1 -1
  61. package/dist/diff.js +3 -2
  62. package/dist/document.js +2 -2
  63. package/dist/e2e.js +2 -2
  64. package/dist/edit.js +8 -8
  65. package/dist/exec.js +2 -2
  66. package/dist/fetch.js +2 -2
  67. package/dist/format.js +4 -4
  68. package/dist/{generic-parser-VUXDIAOZ.js → generic-parser-WFMIRHBX.js} +2 -2
  69. package/dist/git.js +2 -2
  70. package/dist/glob.js +2 -2
  71. package/dist/grep.d.ts +2 -0
  72. package/dist/grep.js +4 -2
  73. package/dist/index.js +46 -46
  74. package/dist/install.js +4 -4
  75. package/dist/json.js +2 -2
  76. package/dist/kanban-tool-types.d.ts +2 -0
  77. package/dist/kanban.js +2 -2
  78. package/dist/languages/index.js +2 -2
  79. package/dist/{legacy-bridge-SFNXIRAP.js → legacy-bridge-3T7SR4SK.js} +4 -4
  80. package/dist/lint.js +4 -4
  81. package/dist/logs.js +2 -2
  82. package/dist/outdated.js +4 -2
  83. package/dist/pack.js +45 -45
  84. package/dist/{parser-dispatch-X62UWXJS.js → parser-dispatch-QGC4HSP5.js} +4 -4
  85. package/dist/patch.js +8 -8
  86. package/dist/plan.js +4 -4
  87. package/dist/{py-parser-TDXSQZBC.js → py-parser-RFWG2HYI.js} +3 -3
  88. package/dist/read.js +8 -8
  89. package/dist/replace.js +9 -8
  90. package/dist/scaffold.js +2 -2
  91. package/dist/search.js +1 -1
  92. package/dist/session-kanban.js +1 -1
  93. package/dist/task.js +4 -4
  94. package/dist/test.js +4 -4
  95. package/dist/todo.js +3 -3
  96. package/dist/tool-tier.js +46 -46
  97. package/dist/{tree-sitter-parser-AIERW3YI.js → tree-sitter-parser-GILYQLS4.js} +2 -2
  98. package/dist/tree.js +2 -2
  99. package/dist/typecheck.js +4 -4
  100. package/dist/write.js +8 -8
  101. package/package.json +5 -5
@@ -1,3 +1,7 @@
1
+ import {
2
+ getProcessRegistry,
3
+ redactCommand
4
+ } from "./chunk-3PVYQFRQ.js";
1
5
  import {
2
6
  buildWin32CmdShimInvocation,
3
7
  resolveWin32Command
@@ -7,10 +11,12 @@ import {
7
11
  detectPackageManager,
8
12
  normalizeCommandOutput,
9
13
  safeResolveReal
10
- } from "./chunk-D33S4RQG.js";
14
+ } from "./chunk-ZUFHAHTE.js";
11
15
 
12
16
  // src/outdated.ts
13
17
  import { spawn } from "node:child_process";
18
+ import { StringDecoder } from "node:string_decoder";
19
+ import { ToolValidationError } from "@wrongstack/core/types";
14
20
  import { buildChildEnv } from "@wrongstack/core/utils";
15
21
  var outdatedTool = {
16
22
  name: "outdated",
@@ -48,12 +54,19 @@ var outdatedTool = {
48
54
  }
49
55
  },
50
56
  async execute(input, ctx, opts) {
57
+ if (input.cwd !== void 0 && (typeof input.cwd !== "string" || !input.cwd.trim())) {
58
+ throw new ToolValidationError({
59
+ message: "outdated: cwd must be a non-empty string when provided.",
60
+ field: "cwd"
61
+ });
62
+ }
51
63
  const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
52
64
  const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
65
+ signal.throwIfAborted();
53
66
  const manager = await detectPackageManager(cwd, ctx.projectRoot);
54
67
  if (manager === "npm") {
55
68
  try {
56
- const { detectNonJsEcosystem } = await import("./legacy-bridge-SFNXIRAP.js");
69
+ const { detectNonJsEcosystem } = await import("./legacy-bridge-3T7SR4SK.js");
57
70
  const nonJs = await detectNonJsEcosystem(cwd, ctx.projectRoot);
58
71
  if (nonJs) {
59
72
  const { planLanguageOperation, executePackagePlan } = await import("./languages/index.js");
@@ -107,10 +120,23 @@ var outdatedTool = {
107
120
  }
108
121
  };
109
122
  function runOutdated(manager, args, cwd, signal) {
123
+ if (signal.aborted) {
124
+ return Promise.resolve({
125
+ exit_code: 124,
126
+ packages: [],
127
+ total: 0,
128
+ output: "Aborted",
129
+ truncated: false
130
+ });
131
+ }
110
132
  return new Promise((resolve) => {
111
133
  let stdout = "";
112
134
  let stderr = "";
135
+ let stdoutBytes = 0;
136
+ let stderrBytes = 0;
113
137
  const MAX = 1e5;
138
+ const stdoutDecoder = new StringDecoder("utf8");
139
+ const stderrDecoder = new StringDecoder("utf8");
114
140
  const resolved = resolveWin32Command(manager);
115
141
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
116
142
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
@@ -124,17 +150,78 @@ function runOutdated(manager, args, cwd, signal) {
124
150
  windowsHide: true,
125
151
  ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
126
152
  });
153
+ const registry = getProcessRegistry();
154
+ if (typeof child.pid === "number") {
155
+ registry.register({
156
+ pid: child.pid,
157
+ name: manager,
158
+ command: redactCommand(`${spawnCmd} ${args.join(" ")}`),
159
+ startedAt: Date.now(),
160
+ child
161
+ });
162
+ }
163
+ const onAbort = () => {
164
+ if (typeof child.pid === "number") {
165
+ registry.kill(child.pid, { force: true });
166
+ } else {
167
+ try {
168
+ child.kill("SIGKILL");
169
+ } catch {
170
+ }
171
+ }
172
+ };
173
+ if (signal) {
174
+ if (signal.aborted) onAbort();
175
+ else signal.addEventListener("abort", onAbort, { once: true });
176
+ }
127
177
  child.stdout?.on("data", (c) => {
128
- if (stdout.length < MAX) stdout += c.toString();
178
+ stdoutBytes += c.byteLength;
179
+ if (stdout.length < MAX) {
180
+ const text = stdoutDecoder.write(c);
181
+ stdout += text.slice(0, MAX - stdout.length);
182
+ }
129
183
  });
130
184
  child.stderr?.on("data", (c) => {
131
- if (stderr.length < MAX) stderr += c.toString();
185
+ stderrBytes += c.byteLength;
186
+ if (stderr.length < MAX) {
187
+ const text = stderrDecoder.write(c);
188
+ stderr += text.slice(0, MAX - stderr.length);
189
+ }
132
190
  });
133
191
  child.on("close", (code) => {
134
- const result = parseOutdatedOutput(stdout, code ?? 0);
192
+ if (signal) signal.removeEventListener("abort", onAbort);
193
+ if (typeof child.pid === "number") registry.unregister(child.pid);
194
+ if (signal.aborted) {
195
+ resolve({
196
+ exit_code: 124,
197
+ packages: [],
198
+ total: 0,
199
+ output: "Aborted",
200
+ truncated: false
201
+ });
202
+ return;
203
+ }
204
+ const restOut = stdoutDecoder.end();
205
+ if (restOut && stdout.length < MAX) {
206
+ stdout += restOut.slice(0, MAX - stdout.length);
207
+ }
208
+ const isTruncated = stdoutBytes > MAX || stderrBytes > MAX || Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
209
+ const result = parseOutdatedOutput(stdout, code ?? 0, stderr, isTruncated);
135
210
  resolve(result);
136
211
  });
137
212
  child.on("error", (e) => {
213
+ if (signal) signal.removeEventListener("abort", onAbort);
214
+ if (typeof child.pid === "number") registry.unregister(child.pid);
215
+ if (signal.aborted || e.name === "AbortError") {
216
+ resolve({
217
+ exit_code: 124,
218
+ packages: [],
219
+ total: 0,
220
+ output: "Aborted",
221
+ truncated: false
222
+ });
223
+ return;
224
+ }
138
225
  resolve({
139
226
  exit_code: 1,
140
227
  packages: [],
@@ -145,18 +232,18 @@ function runOutdated(manager, args, cwd, signal) {
145
232
  });
146
233
  });
147
234
  }
148
- function parseOutdatedOutput(json, exitCode) {
235
+ function parseOutdatedOutput(json, exitCode, stderr = "", truncated = false) {
149
236
  const packages = [];
150
237
  if (!json) {
151
238
  return {
152
239
  exit_code: exitCode,
153
240
  packages: [],
154
241
  total: 0,
155
- output: exitCode === 0 ? "All packages up to date" : "Could not check outdated packages",
156
- truncated: false
242
+ output: stderr || (exitCode === 0 ? "All packages up to date" : "Could not check outdated packages"),
243
+ truncated
157
244
  };
158
245
  }
159
- const truncated = json.length >= 1e5 || Buffer.byteLength(json, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
246
+ const isTruncated = truncated || json.length >= 1e5 || Buffer.byteLength(json, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
160
247
  let parsedOk = false;
161
248
  try {
162
249
  const data = JSON.parse(json);
@@ -188,11 +275,11 @@ Note: exit code 1 from \`outdated\` means outdated packages were found (expected
188
275
  packages,
189
276
  total: packages.length,
190
277
  output,
191
- truncated
278
+ truncated: isTruncated
192
279
  };
193
280
  }
194
281
 
195
282
  export {
196
283
  outdatedTool
197
284
  };
198
- //# sourceMappingURL=chunk-MRZOVQ5D.js.map
285
+ //# sourceMappingURL=chunk-IYKXK356.js.map
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  assertRealInsideRoot,
9
9
  safeResolveReal
10
- } from "./chunk-D33S4RQG.js";
10
+ } from "./chunk-ZUFHAHTE.js";
11
11
 
12
12
  // src/glob.ts
13
13
  import * as fs from "node:fs/promises";
@@ -52,9 +52,9 @@ var globTool = {
52
52
  required: ["pattern"]
53
53
  },
54
54
  async execute(input, ctx, opts) {
55
- if (!input?.pattern) {
55
+ if (!input?.pattern || typeof input.pattern !== "string" || !input.pattern.trim()) {
56
56
  throw new ToolValidationError({
57
- message: "glob: pattern is required",
57
+ message: "glob: pattern is required and cannot be empty",
58
58
  field: "pattern"
59
59
  });
60
60
  }
@@ -63,10 +63,12 @@ var globTool = {
63
63
  return { files: [], truncated: true };
64
64
  }
65
65
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
66
- const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
66
+ const rawLimit = typeof input.limit === "number" && !Number.isNaN(input.limit) ? input.limit : 1e3;
67
+ const limit = Math.max(1, Math.min(Math.floor(rawLimit), 5e3));
67
68
  const isGitIgnored = await loadGitignoreMatcher(base);
68
69
  const re = compileGlob(input.pattern);
69
70
  const results = [];
71
+ const visitedRealDirs = /* @__PURE__ */ new Set();
70
72
  let truncated = false;
71
73
  const pushResult = async (full) => {
72
74
  if (signal?.aborted || truncated || results.length >= limit) {
@@ -89,6 +91,9 @@ var globTool = {
89
91
  truncated = true;
90
92
  return;
91
93
  }
94
+ const realDir = await fs.realpath(dir).catch(() => dir);
95
+ if (visitedRealDirs.has(realDir)) return;
96
+ visitedRealDirs.add(realDir);
92
97
  if (results.length >= limit) {
93
98
  truncated = true;
94
99
  return;
@@ -111,7 +116,8 @@ var globTool = {
111
116
  subdirs.push({ full, rel });
112
117
  } else if (e.isFile()) {
113
118
  if (isGitIgnored(rel, false)) continue;
114
- if (re.test(rel) || re.test(name)) {
119
+ re.lastIndex = 0;
120
+ if (re.test(rel) || (re.lastIndex = 0, re.test(name))) {
115
121
  matchedFiles.push(full);
116
122
  }
117
123
  } else if (e.isSymbolicLink()) {
@@ -126,7 +132,8 @@ var globTool = {
126
132
  if (isGitIgnored(rel, false)) continue;
127
133
  const real = await fs.realpath(full);
128
134
  await assertRealInsideRoot(real, ctx);
129
- if (re.test(rel) || re.test(name)) matchedFiles.push(full);
135
+ re.lastIndex = 0;
136
+ if (re.test(rel) || (re.lastIndex = 0, re.test(name))) matchedFiles.push(full);
130
137
  }
131
138
  } catch {
132
139
  }
@@ -151,4 +158,4 @@ var globTool = {
151
158
  export {
152
159
  globTool
153
160
  };
154
- //# sourceMappingURL=chunk-6GZPSWP3.js.map
161
+ //# sourceMappingURL=chunk-JDPQOR7C.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseFilesContent
3
- } from "./chunk-JJQB6R7Y.js";
3
+ } from "./chunk-CMOUCKGC.js";
4
4
  import {
5
5
  INDEXABLE_EXTENSIONS,
6
6
  LANG_FAMILY_ENTRIES,
@@ -514,7 +514,9 @@ function encodeVector(vec) {
514
514
  function decodeVector(buf) {
515
515
  if (buf.byteLength === 0) return new Float32Array(0);
516
516
  if (buf.byteLength % 4 !== 0) {
517
- throw new Error(`decodeVector: invalid vector byteLength ${buf.byteLength} (must be a multiple of 4)`);
517
+ throw new Error(
518
+ `decodeVector: invalid vector byteLength ${buf.byteLength} (must be a multiple of 4)`
519
+ );
518
520
  }
519
521
  const copy = new Float32Array(buf.byteLength / 4);
520
522
  new Uint8Array(copy.buffer).set(buf);
@@ -636,10 +638,7 @@ function bulkInsertSymbolsWithStatement(stmt, maxSqlVars, rows) {
636
638
  for (const take of ladder) {
637
639
  const chunk = rows.slice(cursor, cursor + take);
638
640
  cursor += take;
639
- const placeholders2 = getRowPlaceholders(
640
- "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
641
- chunk.length
642
- );
641
+ const placeholders2 = getRowPlaceholders("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", chunk.length);
643
642
  const insert = stmt(
644
643
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope)
645
644
  VALUES ${placeholders2}`
@@ -697,10 +696,7 @@ function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
697
696
  for (const take of ladder) {
698
697
  const chunk = refs.slice(cursor, cursor + take);
699
698
  cursor += take;
700
- const placeholders2 = getRowPlaceholders(
701
- "(?, ?, ?, ?, ?, ?, ?, ?)",
702
- chunk.length
703
- );
699
+ const placeholders2 = getRowPlaceholders("(?, ?, ?, ?, ?, ?, ?, ?)", chunk.length);
704
700
  const insert = stmt(
705
701
  `INSERT INTO refs(from_id, to_name, to_id, call_type, line, lang, module, to_file)
706
702
  VALUES ${placeholders2}`
@@ -1769,9 +1765,7 @@ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvail
1769
1765
  if (vectorsAvailable) {
1770
1766
  vectorRows.push({
1771
1767
  id,
1772
- vector: encodeVector(
1773
- embedText(buildIndexableText(s.name, s.signature, s.docComment))
1774
- )
1768
+ vector: encodeVector(embedText(buildIndexableText(s.name, s.signature, s.docComment)))
1775
1769
  });
1776
1770
  }
1777
1771
  const inserted = { ...s, id };
@@ -1923,6 +1917,7 @@ var SYMBOL_VECTORS_TABLE_SQL = `
1923
1917
  // src/codebase-index/writer-refs.ts
1924
1918
  var FAMILY_MATCH_SQL = `(
1925
1919
  sym.lang = refs.lang
1920
+ OR refs.lang = ''
1926
1921
  OR EXISTS (
1927
1922
  SELECT 1 FROM lang_family lf1
1928
1923
  JOIN lang_family lf2 ON lf1.family = lf2.family
@@ -3824,7 +3819,7 @@ var ParserWorkerPool = class {
3824
3819
  */
3825
3820
  async reparseInline(batch, orphaned, workerId) {
3826
3821
  try {
3827
- const { parseFileContent } = await import("./parser-dispatch-X62UWXJS.js");
3822
+ const { parseFileContent } = await import("./parser-dispatch-QGC4HSP5.js");
3828
3823
  for (const item of orphaned) {
3829
3824
  if (batch.settled) return;
3830
3825
  try {
@@ -4613,4 +4608,4 @@ export {
4613
4608
  incomingCallsService,
4614
4609
  outgoingCallsService
4615
4610
  };
4616
- //# sourceMappingURL=chunk-BLCSYKNN.js.map
4611
+ //# sourceMappingURL=chunk-KY4PXYSO.js.map
@@ -1,15 +1,16 @@
1
1
  import {
2
2
  tryLegacyCodeOperation
3
- } from "./chunk-4E42D7UF.js";
3
+ } from "./chunk-ATSKPZUK.js";
4
4
  import {
5
5
  spawnStream
6
6
  } from "./chunk-ZHH3BEWU.js";
7
7
  import {
8
8
  normalizeCommandOutput,
9
9
  safeResolveReal
10
- } from "./chunk-D33S4RQG.js";
10
+ } from "./chunk-ZUFHAHTE.js";
11
11
 
12
12
  // src/lint.ts
13
+ import { ToolValidationError } from "@wrongstack/core/types";
13
14
  var lintTool = {
14
15
  name: "lint",
15
16
  category: "Code Quality",
@@ -49,6 +50,14 @@ var lintTool = {
49
50
  async *executeStream(input, ctx, opts) {
50
51
  const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
51
52
  const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
53
+ signal.throwIfAborted();
54
+ const VALID_LINTERS = /* @__PURE__ */ new Set(["biome", "eslint", "tslint", "auto"]);
55
+ if (input.linter !== void 0 && !VALID_LINTERS.has(input.linter)) {
56
+ throw new ToolValidationError({
57
+ message: `lint: unsupported linter "${input.linter}". Allowed linters: biome, eslint, tslint, auto`,
58
+ field: "linter"
59
+ });
60
+ }
52
61
  const linter = input.linter ?? "auto";
53
62
  if (linter === "auto" && !input.files) {
54
63
  const bridge = await tryLegacyCodeOperation("lint", {
@@ -90,24 +99,49 @@ var lintTool = {
90
99
  return;
91
100
  }
92
101
  yield { type: "log", text: `Running ${detected}\u2026`, data: { linter: detected } };
93
- const args = ["lint"];
94
- if (input.fix) args.push("--write");
95
- if (input.files) {
96
- const files = Array.isArray(input.files) ? input.files : input.files.split(",");
97
- args.push("--", ...files.map((f) => f.trim()));
102
+ const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim().replace(/\\/g, "/")).filter(Boolean) : [];
103
+ const args = [];
104
+ if (detected === "eslint") {
105
+ if (input.fix) args.push("--fix");
106
+ if (files.length) args.push(...files);
107
+ } else {
108
+ args.push("lint");
109
+ if (input.fix) args.push("--write");
110
+ if (files.length) args.push("--", ...files);
98
111
  }
99
112
  const cmd = detected === "biome" ? "biome" : detected;
100
113
  const result = yield* spawnStream({ cmd, args, cwd, signal, maxBytes: 1e5 });
101
- const errors = [...result.stdout.matchAll(/\berror\b/gi)].length;
102
- const warnings = [...result.stdout.matchAll(/\bwarning\b/gi)].length;
114
+ const combined = `${result.stdout}
115
+ ${result.stderr}`;
116
+ let errors = 0;
117
+ let warnings = 0;
118
+ const biomeSummary = combined.match(/Found\s+(\d+)\s+errors?(?:\s+and\s+(\d+)\s+warnings?)?/i);
119
+ const eslintSummary = combined.match(
120
+ /(\d+)\s+problems?\s+\((\d+)\s+errors?,\s*(\d+)\s+warnings?\)/i
121
+ );
122
+ if (biomeSummary) {
123
+ errors = Number.parseInt(biomeSummary[1] ?? "0", 10);
124
+ warnings = Number.parseInt(biomeSummary[2] ?? "0", 10);
125
+ } else if (eslintSummary) {
126
+ errors = Number.parseInt(eslintSummary[2] ?? "0", 10);
127
+ warnings = Number.parseInt(eslintSummary[3] ?? "0", 10);
128
+ } else {
129
+ errors = [...combined.matchAll(/\berror\b/gi)].length;
130
+ warnings = [...combined.matchAll(/\bwarning\b/gi)].length;
131
+ }
132
+ if (errors === 0 && result.exitCode !== 0) {
133
+ errors = 1;
134
+ }
135
+ const rawOutput = result.stdout && result.stderr ? `${result.stdout}
136
+ ${result.stderr}` : result.stdout || result.stderr || result.error || "";
103
137
  yield {
104
138
  type: "final",
105
139
  output: {
106
140
  linter: detected,
107
- files_checked: input.files ? Array.isArray(input.files) ? input.files.length : input.files.split(",").length : 0,
141
+ files_checked: files.length,
108
142
  errors,
109
143
  warnings,
110
- output: normalizeCommandOutput(result.stdout),
144
+ output: normalizeCommandOutput(rawOutput),
111
145
  fix_applied: input.fix ?? false,
112
146
  truncated: result.truncated
113
147
  }
@@ -116,10 +150,25 @@ var lintTool = {
116
150
  };
117
151
  async function detectLinter(cwd) {
118
152
  const { stat } = await import("node:fs/promises");
119
- const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
153
+ const checks = [
154
+ "biome.json",
155
+ "biome.jsonc",
156
+ "eslint.config.js",
157
+ "eslint.config.mjs",
158
+ "eslint.config.cjs",
159
+ "eslint.config.ts",
160
+ ".eslintrc.json",
161
+ ".eslintrc.js",
162
+ ".eslintrc.cjs",
163
+ ".eslintrc.yaml",
164
+ ".eslintrc.yml",
165
+ "tslint.json",
166
+ "tsconfig.json"
167
+ ];
120
168
  for (const f of checks) {
121
169
  try {
122
- await stat(`${cwd}/${f}`);
170
+ const { join } = await import("node:path");
171
+ await stat(join(cwd, f));
123
172
  if (f.includes("biome")) return "biome";
124
173
  if (f.includes("eslint")) return "eslint";
125
174
  if (f.includes("tslint")) return "tslint";
@@ -132,4 +181,4 @@ async function detectLinter(cwd) {
132
181
  export {
133
182
  lintTool
134
183
  };
135
- //# sourceMappingURL=chunk-WJYXMB2R.js.map
184
+ //# sourceMappingURL=chunk-LOBRWKCR.js.map
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  safeResolveReal
3
- } from "./chunk-D33S4RQG.js";
3
+ } from "./chunk-ZUFHAHTE.js";
4
4
 
5
5
  // src/tree.ts
6
6
  import * as fs from "node:fs/promises";
7
7
  import * as path from "node:path";
8
- import { DEFAULT_WALK_IGNORE_DIRS, compileGlob, expectDefined } from "@wrongstack/core/utils";
8
+ import { compileGlob, DEFAULT_WALK_IGNORE_DIRS, expectDefined } from "@wrongstack/core/utils";
9
9
  var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
10
10
  ...DEFAULT_WALK_IGNORE_DIRS,
11
11
  ".wrongstack",
@@ -80,14 +80,20 @@ var treeTool = {
80
80
  },
81
81
  async *executeStream(input, ctx, opts) {
82
82
  const basePath = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
83
- const maxDepth = input.depth ?? 3;
83
+ const maxDepth = typeof input.depth === "number" && Number.isFinite(input.depth) && input.depth >= 0 ? Math.floor(input.depth) : 3;
84
84
  const showFiles = input.show_files ?? true;
85
85
  const showDirs = input.show_dirs ?? true;
86
86
  const showHidden = input.show_hidden ?? false;
87
- const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...input.exclude ?? []]);
87
+ const exclude = /* @__PURE__ */ new Set([
88
+ ...DEFAULT_IGNORE,
89
+ ...(input.exclude ?? []).map(
90
+ (s) => s.trim().replace(/[\\/]+$/, "").replace(/^\.\//, "")
91
+ ).filter(Boolean)
92
+ ]);
88
93
  const globRe = input.glob ? compileGlob(input.glob) : void 0;
89
- const maxEntries = input.max_entries ?? DEFAULT_MAX_ENTRIES;
94
+ const maxEntries = typeof input.max_entries === "number" && Number.isFinite(input.max_entries) && input.max_entries > 0 ? Math.floor(input.max_entries) : DEFAULT_MAX_ENTRIES;
90
95
  const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
96
+ signal.throwIfAborted();
91
97
  const lines = [basePath];
92
98
  const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };
93
99
  const retention = {
@@ -100,6 +106,7 @@ var treeTool = {
100
106
  const queue = [];
101
107
  const FLUSH_EVERY = 200;
102
108
  let lastEmittedTotal = 0;
109
+ let wakeWaiter;
103
110
  const tickProgress = () => {
104
111
  const seen = totals.totalFiles.value + totals.totalDirs.value;
105
112
  if (seen - lastEmittedTotal >= FLUSH_EVERY) {
@@ -109,6 +116,7 @@ var treeTool = {
109
116
  data: { files: totals.totalFiles.value, dirs: totals.totalDirs.value }
110
117
  });
111
118
  lastEmittedTotal = seen;
119
+ wakeWaiter?.();
112
120
  }
113
121
  };
114
122
  const walkPromise = walkDir(basePath, 0, {
@@ -129,27 +137,20 @@ var treeTool = {
129
137
  signal
130
138
  });
131
139
  let walkDone = false;
132
- void walkPromise.then(
133
- () => {
134
- walkDone = true;
135
- },
136
- () => {
137
- walkDone = true;
138
- }
139
- );
140
+ void walkPromise.catch(() => {
141
+ }).finally(() => {
142
+ walkDone = true;
143
+ wakeWaiter?.();
144
+ });
140
145
  while (!walkDone || queue.length > 0) {
141
146
  if (queue.length > 0) {
142
147
  yield expectDefined(queue.shift());
143
148
  } else {
144
- let pollTimer;
145
- const poll = new Promise((r) => {
146
- pollTimer = setTimeout(r, 50);
149
+ await new Promise((resolve) => {
150
+ wakeWaiter = resolve;
151
+ if (walkDone || queue.length > 0) resolve();
147
152
  });
148
- try {
149
- await Promise.race([walkPromise, poll]).catch(() => void 0);
150
- } finally {
151
- if (pollTimer) clearTimeout(pollTimer);
152
- }
153
+ wakeWaiter = void 0;
153
154
  }
154
155
  }
155
156
  await walkPromise;
@@ -166,10 +167,17 @@ var treeTool = {
166
167
  }
167
168
  };
168
169
  function matchesTreeGlob(globRe, fileName, absPath, basePath) {
169
- if (globRe.test(fileName)) return true;
170
+ globRe.lastIndex = 0;
171
+ if (globRe.test(fileName)) {
172
+ globRe.lastIndex = 0;
173
+ return true;
174
+ }
175
+ globRe.lastIndex = 0;
170
176
  const rel = path.relative(basePath, absPath);
171
177
  const posixRel = !rel || rel.startsWith("..") || path.isAbsolute(rel) ? absPath.split(path.sep).join("/") : rel.split(path.sep).join("/");
172
- return globRe.test(posixRel);
178
+ const matches = globRe.test(posixRel);
179
+ globRe.lastIndex = 0;
180
+ return matches;
173
181
  }
174
182
  async function walkDir(dir, depth, opts) {
175
183
  opts.signal.throwIfAborted();
@@ -178,6 +186,8 @@ async function walkDir(dir, depth, opts) {
178
186
  const filtered = entries.filter((e) => {
179
187
  if (!opts.showHidden && e.name.startsWith(".")) return false;
180
188
  if (opts.exclude.has(e.name)) return false;
189
+ const rel = path.relative(opts.basePath, path.join(dir, e.name)).split(path.sep).join("/");
190
+ if (opts.exclude.has(rel)) return false;
181
191
  if (e.isFile() && opts.globRe) {
182
192
  const abs = path.join(dir, e.name);
183
193
  if (!matchesTreeGlob(opts.globRe, e.name, abs, opts.basePath)) return false;
@@ -204,11 +214,12 @@ async function walkDir(dir, depth, opts) {
204
214
  const entry = items[i];
205
215
  if (!entry) continue;
206
216
  const isLast = i === items.length - 1;
207
- const connector = opts.isLast ? " " : "\u2502 ";
217
+ const connector = isLast ? " " : "\u2502 ";
208
218
  const branch = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
209
219
  const displayName = entry.name + (entry.isDirectory() ? "/" : "");
210
- if (!opts.showDirs && entry.isDirectory()) continue;
211
- if (!opts.showFiles && entry.isFile()) continue;
220
+ const isDir = entry.isDirectory();
221
+ if (!opts.showDirs && isDir) continue;
222
+ if (!opts.showFiles && !isDir) continue;
212
223
  const line = opts.prefix + branch + displayName;
213
224
  const lineBytes = Buffer.byteLength(line, "utf8") + 1;
214
225
  if (opts.retention.entries >= opts.retention.maxEntries || opts.retention.outputBytes + lineBytes > opts.retention.maxOutputBytes) {
@@ -232,4 +243,4 @@ async function walkDir(dir, depth, opts) {
232
243
  export {
233
244
  treeTool
234
245
  };
235
- //# sourceMappingURL=chunk-J5SKD6LN.js.map
246
+ //# sourceMappingURL=chunk-MNP3GE7H.js.map
@@ -3,16 +3,16 @@ import {
3
3
  } from "./chunk-FQZOHLG3.js";
4
4
  import {
5
5
  enqueueReindex
6
- } from "./chunk-VZLX2JZG.js";
6
+ } from "./chunk-DP6CXMJZ.js";
7
7
  import {
8
8
  safeResolveReal,
9
9
  sha256hex,
10
10
  truncateDiffPayload
11
- } from "./chunk-D33S4RQG.js";
11
+ } from "./chunk-ZUFHAHTE.js";
12
12
 
13
13
  // src/write.ts
14
14
  import * as fs from "node:fs/promises";
15
- import { ToolValidationError } from "@wrongstack/core/types";
15
+ import { FsError, ToolValidationError } from "@wrongstack/core/types";
16
16
  import {
17
17
  atomicWrite,
18
18
  detectNewlineStyle,
@@ -72,6 +72,15 @@ var writeTool = {
72
72
  yield { type: "final", output: await finishWrite(input, ctx, prepared, signal) };
73
73
  }
74
74
  };
75
+ function countLines(text) {
76
+ if (!text) return 0;
77
+ const trimmed = text.endsWith("\n") ? text.slice(0, -1) : text;
78
+ let count = 1;
79
+ for (let i = 0; i < trimmed.length; i++) {
80
+ if (trimmed.charCodeAt(i) === 10) count++;
81
+ }
82
+ return count;
83
+ }
75
84
  async function writeFile(input, ctx, signal) {
76
85
  signal?.throwIfAborted();
77
86
  return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
@@ -94,6 +103,14 @@ async function prepareWrite(input, ctx) {
94
103
  let prev = "";
95
104
  try {
96
105
  const stat2 = await fs.stat(absPath);
106
+ if (stat2.isDirectory()) {
107
+ throw new FsError({
108
+ message: `write: "${input.path}" is an existing directory and cannot be overwritten`,
109
+ code: "FS_WRITE_FAILED",
110
+ path: absPath,
111
+ context: { reason: "is-directory" }
112
+ });
113
+ }
97
114
  existed = stat2.isFile();
98
115
  if (existed) {
99
116
  prev = await fs.readFile(absPath, "utf8");
@@ -116,11 +133,13 @@ async function finishWrite(input, ctx, prepared, signal) {
116
133
  enqueueReindex({ projectRoot: ctx.projectRoot, files: [prepared.absPath] });
117
134
  } catch {
118
135
  }
119
- const rawDiff = prepared.existed ? unifiedDiff(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
120
- + (new file, ${content.split("\n").length} lines)`;
136
+ const isIdentical = prepared.existed && prepared.prev === content;
137
+ const rawDiff = prepared.existed ? isIdentical ? "(no-op: file content is identical to existing content)" : unifiedDiff(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
138
+ + (new file, ${countLines(content)} lines)`;
121
139
  const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES);
140
+ const contentHash = sha256hex(content);
122
141
  const stat2 = await fs.stat(prepared.absPath);
123
- ctx.recordRead(prepared.absPath, stat2.mtimeMs, "write", sha256hex(content));
142
+ ctx.recordRead(prepared.absPath, stat2.mtimeMs, "write", contentHash);
124
143
  ctx.session?.recordFileChange?.({
125
144
  path: prepared.absPath,
126
145
  action: prepared.existed ? "modified" : "created",
@@ -134,6 +153,11 @@ async function finishWrite(input, ctx, prepared, signal) {
134
153
  ).catch(() => void 0);
135
154
  const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
136
155
  const notes = [];
156
+ if (isIdentical) {
157
+ notes.push(
158
+ "Write completed with no content changes (new content is identical to existing content)."
159
+ );
160
+ }
137
161
  if (diffTruncated) {
138
162
  notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
139
163
  }
@@ -155,4 +179,4 @@ async function finishWrite(input, ctx, prepared, signal) {
155
179
  export {
156
180
  writeTool
157
181
  };
158
- //# sourceMappingURL=chunk-S2GBLQ5S.js.map
182
+ //# sourceMappingURL=chunk-O44IMQCN.js.map