@wrongstack/tools 0.310.1 → 0.313.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 (135) hide show
  1. package/dist/_shell-pick.d.ts +5 -10
  2. package/dist/audit.js +10 -4055
  3. package/dist/bash.js +9 -2332
  4. package/dist/batch-tool-use.js +2 -187
  5. package/dist/browser/index.js +25 -1339
  6. package/dist/builtin.js +69 -33985
  7. package/dist/chunk-2CTB3UK5.js +197 -0
  8. package/dist/chunk-2ME7ZWDD.js +1272 -0
  9. package/dist/chunk-2TFQDEPU.js +113 -0
  10. package/dist/chunk-2ZCJNKFE.js +315 -0
  11. package/dist/chunk-3PVYQFRQ.js +472 -0
  12. package/dist/chunk-3QMQBO5Z.js +89 -0
  13. package/dist/chunk-42AKKX7O.js +385 -0
  14. package/dist/chunk-42SKEETB.js +113 -0
  15. package/dist/chunk-4BHUSMNL.js +104 -0
  16. package/dist/chunk-4I4LWFOO.js +431 -0
  17. package/dist/chunk-553IQUMO.js +478 -0
  18. package/dist/chunk-55KF77OK.js +1944 -0
  19. package/dist/chunk-6HLZK37X.js +359 -0
  20. package/dist/chunk-6RNIFLG4.js +305 -0
  21. package/dist/chunk-A6BRWIXN.js +317 -0
  22. package/dist/chunk-AHHDWNXC.js +200 -0
  23. package/dist/chunk-B2CIOKWP.js +899 -0
  24. package/dist/chunk-BTSFTGIL.js +7 -0
  25. package/dist/chunk-D33S4RQG.js +222 -0
  26. package/dist/chunk-DIJKHSUO.js +134 -0
  27. package/dist/chunk-DPL47QGS.js +3515 -0
  28. package/dist/chunk-EMRGN4KM.js +170 -0
  29. package/dist/chunk-FGOABE4Z.js +955 -0
  30. package/dist/chunk-FW65TP4G.js +520 -0
  31. package/dist/chunk-GOIZNNOA.js +221 -0
  32. package/dist/chunk-H5MJUAA6.js +295 -0
  33. package/dist/chunk-H5OHH6F4.js +223 -0
  34. package/dist/chunk-HC5RM4WY.js +199 -0
  35. package/dist/chunk-HEQFMLZ7.js +190 -0
  36. package/dist/chunk-HFXSNKCV.js +196 -0
  37. package/dist/chunk-HXSBQ2SE.js +1723 -0
  38. package/dist/chunk-ILESNGNM.js +163 -0
  39. package/dist/chunk-INF5CGAO.js +137 -0
  40. package/dist/chunk-IWDFZ6XR.js +337 -0
  41. package/dist/chunk-IXMNTNW5.js +238 -0
  42. package/dist/chunk-J4SRJKBW.js +428 -0
  43. package/dist/chunk-J5SKD6LN.js +235 -0
  44. package/dist/chunk-JRJJX3L5.js +424 -0
  45. package/dist/chunk-KXNDHMUB.js +366 -0
  46. package/dist/chunk-LIJQMYYX.js +59 -0
  47. package/dist/chunk-MAN26PZ5.js +260 -0
  48. package/dist/chunk-MKRQMLZA.js +474 -0
  49. package/dist/chunk-NBDZMMG2.js +169 -0
  50. package/dist/chunk-NHA6BEIH.js +101 -0
  51. package/dist/chunk-OA6YJIWL.js +4412 -0
  52. package/dist/chunk-OZC255IC.js +2830 -0
  53. package/dist/chunk-PUR6WU2B.js +154 -0
  54. package/dist/chunk-Q5JPPCZC.js +12 -0
  55. package/dist/chunk-QBV725YG.js +128 -0
  56. package/dist/chunk-QHVW4AAM.js +892 -0
  57. package/dist/chunk-QNJ7AQ4B.js +1000 -0
  58. package/dist/chunk-RGISBZBD.js +231 -0
  59. package/dist/chunk-RJZQWEMS.js +84 -0
  60. package/dist/chunk-RP2ZA47V.js +15 -0
  61. package/dist/chunk-RXISBFR5.js +315 -0
  62. package/dist/chunk-S5CCLZL3.js +99 -0
  63. package/dist/chunk-TVTCSBJF.js +122 -0
  64. package/dist/chunk-U4D5S3VQ.js +21 -0
  65. package/dist/chunk-U4VAATKP.js +193 -0
  66. package/dist/chunk-UKU2ZX4C.js +203 -0
  67. package/dist/chunk-UNSKG6DC.js +159 -0
  68. package/dist/chunk-URFKEIHM.js +1091 -0
  69. package/dist/chunk-UTV7YAW7.js +178 -0
  70. package/dist/chunk-V43EQ6VR.js +68 -0
  71. package/dist/chunk-VB3MJGCV.js +610 -0
  72. package/dist/chunk-WHIIIREL.js +223 -0
  73. package/dist/chunk-XMIFYKUT.js +1371 -0
  74. package/dist/chunk-YF2RSV34.js +54 -0
  75. package/dist/chunk-Z6DAKQ3U.js +1 -0
  76. package/dist/chunk-ZIGAZFWE.js +522 -0
  77. package/dist/circuit-breaker.js +3 -191
  78. package/dist/codebase-index/index.js +84 -13688
  79. package/dist/codebase-index/parser-worker-script.js +8 -3995
  80. package/dist/codebase-index/project-server.js +60 -8548
  81. package/dist/codebase-index/worker.js +20 -8416
  82. package/dist/design.js +2 -299
  83. package/dist/diff.js +4 -256
  84. package/dist/document.js +4 -222
  85. package/dist/e2e.js +5 -476
  86. package/dist/edit.js +14 -10200
  87. package/dist/exec.js +16 -3131
  88. package/dist/fetch.js +8 -398
  89. package/dist/format.js +10 -3750
  90. package/dist/generic-parser-VUXDIAOZ.js +13 -0
  91. package/dist/git.js +4 -380
  92. package/dist/glob.js +6 -280
  93. package/dist/go-parser-6CA77WZ4.js +469 -0
  94. package/dist/grep.js +7 -604
  95. package/dist/index.js +318 -35423
  96. package/dist/install.js +10 -4091
  97. package/dist/json-parser-7YMDJSBJ.js +213 -0
  98. package/dist/json.js +4 -665
  99. package/dist/kanban.js +3 -2096
  100. package/dist/languages/index.js +23 -4664
  101. package/dist/legacy-bridge-7UKZT46G.js +18 -0
  102. package/dist/lint.js +10 -3706
  103. package/dist/logs.js +4 -287
  104. package/dist/memory.js +6 -215
  105. package/dist/mode.js +3 -99
  106. package/dist/outdated.js +4 -5170
  107. package/dist/pack.js +65 -33933
  108. package/dist/parser-dispatch-VPOFDO2U.js +14 -0
  109. package/dist/patch.js +13 -9935
  110. package/dist/plan.js +5 -3286
  111. package/dist/process-registry.js +8 -651
  112. package/dist/ps-slash.js +12 -1439
  113. package/dist/py-parser-GPM5VYRG.js +16 -0
  114. package/dist/read.js +13 -9816
  115. package/dist/replace.js +15 -10014
  116. package/dist/rs-parser-WVQ4KYCX.js +81 -0
  117. package/dist/scaffold.js +4 -253
  118. package/dist/search.js +5 -585
  119. package/dist/session-kanban.js +30 -972
  120. package/dist/skill.js +2 -215
  121. package/dist/task.js +5 -3410
  122. package/dist/test.js +10 -3773
  123. package/dist/todo.js +4 -2891
  124. package/dist/tool-help.js +3 -123
  125. package/dist/tool-search.js +3 -79
  126. package/dist/tool-tier.js +68 -34020
  127. package/dist/tool-use.js +2 -93
  128. package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
  129. package/dist/tree.js +4 -288
  130. package/dist/ts-parser-YH4ZMWMH.js +232 -0
  131. package/dist/typecheck.js +10 -3753
  132. package/dist/win32.js +6 -60
  133. package/dist/write.js +14 -9882
  134. package/dist/yaml-parser-GFAOKV53.js +157 -0
  135. package/package.json +5 -5
@@ -0,0 +1,366 @@
1
+ import {
2
+ todoTool
3
+ } from "./chunk-J4SRJKBW.js";
4
+ import {
5
+ projectSessionPlanToKanban
6
+ } from "./chunk-QNJ7AQ4B.js";
7
+
8
+ // src/plan.ts
9
+ import { randomUUID } from "node:crypto";
10
+ import {
11
+ addPlanItem,
12
+ clearPlan,
13
+ deriveTodosFromPlanItem,
14
+ formatPlan,
15
+ getPlanTemplate,
16
+ mutatePlan,
17
+ mutateTasks,
18
+ removePlanItem,
19
+ setPlanItemStatus
20
+ } from "@wrongstack/core/storage";
21
+ import { formatTaskList } from "@wrongstack/core/utils";
22
+ var planTool = {
23
+ name: "plan",
24
+ category: "Session",
25
+ description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
26
+ usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `action: "status"` with `open`, `in_progress`, or `done` for exact status changes.\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Unfinished plan items cannot be removed or cleared; complete them first.\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
27
+ permission: "confirm",
28
+ // WS-046: gives permission decisions something to key on.
29
+ // The action performed; plan has no single file or path subject.
30
+ subjectKey: "action",
31
+ mutating: true,
32
+ capabilities: ["fs.write"],
33
+ icon: "plan",
34
+ timeoutMs: 3e4,
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ action: {
39
+ type: "string",
40
+ enum: [
41
+ "show",
42
+ "add",
43
+ "status",
44
+ "start",
45
+ "done",
46
+ "remove",
47
+ "promote",
48
+ "template_use",
49
+ "clear",
50
+ "taskify"
51
+ ],
52
+ description: "The operation to perform on the plan board."
53
+ },
54
+ title: {
55
+ type: "string",
56
+ description: "Title of the plan item. Required for action=add."
57
+ },
58
+ details: {
59
+ type: "string",
60
+ description: "Additional details or description for a new plan item (action=add)."
61
+ },
62
+ target: {
63
+ type: "string",
64
+ description: "Identifier for the target plan item (id, 1-based index, or partial title). Required for most actions except add/show/clear."
65
+ },
66
+ status: {
67
+ type: "string",
68
+ enum: ["open", "in_progress", "done"],
69
+ description: "Exact plan item status for action=status."
70
+ },
71
+ subtasks: {
72
+ type: "array",
73
+ items: { type: "string" },
74
+ description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
75
+ },
76
+ template: {
77
+ type: "string",
78
+ description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
79
+ },
80
+ scope: {
81
+ type: "string",
82
+ enum: ["session", "project"],
83
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
84
+ }
85
+ },
86
+ required: ["action"]
87
+ },
88
+ async execute(input, ctx) {
89
+ const sessionPlanPath = ctx.meta["plan.path"];
90
+ let planPath;
91
+ if (input.scope === "project") {
92
+ if (typeof sessionPlanPath === "string") {
93
+ const lastSep = Math.max(
94
+ sessionPlanPath.lastIndexOf("/"),
95
+ sessionPlanPath.lastIndexOf("\\")
96
+ );
97
+ if (lastSep < 0) {
98
+ return {
99
+ ok: false,
100
+ message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
101
+ plan: "",
102
+ count: 0,
103
+ open: 0
104
+ };
105
+ }
106
+ planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
107
+ }
108
+ } else {
109
+ planPath = sessionPlanPath;
110
+ }
111
+ if (typeof planPath !== "string" || !planPath) {
112
+ return {
113
+ ok: false,
114
+ message: "Plan storage path is not configured for this session.",
115
+ plan: "",
116
+ count: 0,
117
+ open: 0
118
+ };
119
+ }
120
+ const sessionId = ctx.session?.id ?? "unknown";
121
+ let early = null;
122
+ const taskifyMeta = { title: "", details: "" };
123
+ let didTaskify = false;
124
+ let todosToReplace = null;
125
+ let plan;
126
+ try {
127
+ plan = await mutatePlan(planPath, sessionId, async (p) => {
128
+ switch (input.action) {
129
+ case "show":
130
+ break;
131
+ case "add": {
132
+ const title = input.title?.trim();
133
+ if (!title) {
134
+ early = mkResult(p, false, "add requires `title`.");
135
+ return p;
136
+ }
137
+ const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
138
+ return updated;
139
+ }
140
+ case "start":
141
+ case "done": {
142
+ if (!input.target) {
143
+ early = mkResult(
144
+ p,
145
+ false,
146
+ `${input.action} requires \`target\` (id|index|substring).`
147
+ );
148
+ return p;
149
+ }
150
+ const next = setPlanItemStatus(
151
+ p,
152
+ input.target,
153
+ input.action === "start" ? "in_progress" : "done"
154
+ );
155
+ if (next === p) {
156
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
157
+ return p;
158
+ }
159
+ return next;
160
+ }
161
+ case "status": {
162
+ if (!input.target || !input.status) {
163
+ early = mkResult(
164
+ p,
165
+ false,
166
+ "status requires `target` (id|index|substring) and `status`."
167
+ );
168
+ return p;
169
+ }
170
+ const next = setPlanItemStatus(p, input.target, input.status);
171
+ if (next === p) {
172
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
173
+ return p;
174
+ }
175
+ return next;
176
+ }
177
+ case "remove": {
178
+ if (!input.target) {
179
+ early = mkResult(p, false, "remove requires `target` (id|index|substring).");
180
+ return p;
181
+ }
182
+ const next = removePlanItem(p, input.target);
183
+ if (next === p) {
184
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
185
+ return p;
186
+ }
187
+ const nextIds = new Set(next.items.map((item) => item.id));
188
+ const removed = p.items.find((item) => !nextIds.has(item.id));
189
+ if (removed?.status !== "done") {
190
+ early = mkResult(
191
+ p,
192
+ false,
193
+ `Plan item "${removed?.title ?? input.target}" is not done and cannot be removed. Complete it first.`
194
+ );
195
+ return p;
196
+ }
197
+ return next;
198
+ }
199
+ case "promote": {
200
+ if (!input.target) {
201
+ early = mkResult(
202
+ p,
203
+ false,
204
+ `${input.action} requires \`target\` (id|index|substring).`
205
+ );
206
+ return p;
207
+ }
208
+ const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
209
+ if (!derived) {
210
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
211
+ return p;
212
+ }
213
+ todosToReplace = derived.todos;
214
+ early = mkResult(
215
+ derived.plan,
216
+ true,
217
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
218
+ derived.todos
219
+ );
220
+ return derived.plan;
221
+ }
222
+ case "template_use": {
223
+ const templateName = input.template?.trim();
224
+ if (!templateName) {
225
+ early = mkResult(p, false, "template_use requires `template` name.");
226
+ return p;
227
+ }
228
+ const template = getPlanTemplate(templateName);
229
+ if (!template) {
230
+ early = mkResult(p, false, `Unknown template "${templateName}".`);
231
+ return p;
232
+ }
233
+ let updated = p;
234
+ for (const item of template.items) {
235
+ ({ plan: updated } = addPlanItem(updated, item.title, item.details));
236
+ }
237
+ early = mkResult(
238
+ updated,
239
+ true,
240
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
241
+ );
242
+ return updated;
243
+ }
244
+ case "clear":
245
+ if (p.items.some((item) => item.status !== "done")) {
246
+ early = mkResult(
247
+ p,
248
+ false,
249
+ "Plan contains unfinished items and cannot be cleared. Complete them first."
250
+ );
251
+ return p;
252
+ }
253
+ return clearPlan(p);
254
+ case "taskify": {
255
+ if (!input.target) {
256
+ early = mkResult(
257
+ p,
258
+ false,
259
+ "taskify requires `target` (plan item id|index|substring)."
260
+ );
261
+ return p;
262
+ }
263
+ let itemIdx = -1;
264
+ const asNum = Number.parseInt(input.target, 10);
265
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
266
+ itemIdx = asNum - 1;
267
+ } else {
268
+ itemIdx = p.items.findIndex((it) => it.id === input.target);
269
+ if (itemIdx === -1) {
270
+ const lower = input.target.toLowerCase();
271
+ itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
272
+ }
273
+ }
274
+ if (itemIdx === -1 || !p.items[itemIdx]) {
275
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
276
+ return p;
277
+ }
278
+ const item = p.items[itemIdx];
279
+ taskifyMeta.title = item.title;
280
+ taskifyMeta.details = item.details ?? "";
281
+ didTaskify = true;
282
+ break;
283
+ }
284
+ default:
285
+ early = mkResult(p, false, `Unknown action "${input.action}".`);
286
+ return p;
287
+ }
288
+ return p;
289
+ });
290
+ } catch (err) {
291
+ return {
292
+ ok: false,
293
+ message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
294
+ plan: "",
295
+ count: 0,
296
+ open: 0
297
+ };
298
+ }
299
+ ctx.meta["plan.path.resolved"] = planPath;
300
+ await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
301
+ if (todosToReplace) {
302
+ await todoTool.execute({ todos: todosToReplace }, ctx, {
303
+ signal: AbortSignal.timeout(3e4)
304
+ });
305
+ }
306
+ if (early) return early;
307
+ if (didTaskify) {
308
+ const taskPathRaw = ctx.meta["task.path"];
309
+ if (typeof taskPathRaw !== "string" || !taskPathRaw) {
310
+ return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
311
+ }
312
+ let taskPath = taskPathRaw;
313
+ if (input.scope === "project") {
314
+ const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
315
+ taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
316
+ }
317
+ const now = (/* @__PURE__ */ new Date()).toISOString();
318
+ try {
319
+ const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
320
+ f.tasks.push({
321
+ id: `task_${randomUUID()}`,
322
+ title: taskifyMeta.title,
323
+ description: taskifyMeta.details || void 0,
324
+ type: "feature",
325
+ priority: "medium",
326
+ status: "pending",
327
+ createdAt: now,
328
+ updatedAt: now
329
+ });
330
+ return f;
331
+ });
332
+ ctx.meta["task.path.resolved"] = taskPath;
333
+ return mkResult(
334
+ plan,
335
+ true,
336
+ `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
337
+ ${formatTaskList(taskFile.tasks)}`
338
+ );
339
+ } catch (err) {
340
+ return mkResult(
341
+ plan,
342
+ false,
343
+ `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
344
+ );
345
+ }
346
+ }
347
+ return mkResult(plan, true, `Plan ${input.action} ok.`);
348
+ }
349
+ };
350
+ function mkResult(plan, ok, message, todos) {
351
+ const open = plan.items.filter((i) => i.status !== "done").length;
352
+ const result = {
353
+ ok,
354
+ message,
355
+ plan: formatPlan(plan),
356
+ count: plan.items.length,
357
+ open
358
+ };
359
+ if (todos !== void 0) result.todos = todos;
360
+ return result;
361
+ }
362
+
363
+ export {
364
+ planTool
365
+ };
366
+ //# sourceMappingURL=chunk-KXNDHMUB.js.map
@@ -0,0 +1,59 @@
1
+ // src/codebase-index/gitignore.ts
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { compileGlob } from "@wrongstack/core/utils";
5
+ function globBody(glob) {
6
+ return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
7
+ }
8
+ function compileGitignore(lines) {
9
+ const rules = [];
10
+ for (const raw of lines) {
11
+ let line = raw.replace(/\r$/, "");
12
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
13
+ line = line.trim();
14
+ let negated = false;
15
+ if (line.startsWith("!")) {
16
+ negated = true;
17
+ line = line.slice(1);
18
+ }
19
+ let dirOnly = false;
20
+ if (line.endsWith("/")) {
21
+ dirOnly = true;
22
+ line = line.slice(0, -1);
23
+ }
24
+ if (!line) continue;
25
+ const anchored = line.startsWith("/") || line.includes("/");
26
+ if (line.startsWith("/")) line = line.slice(1);
27
+ const body = globBody(line);
28
+ const prefix = anchored ? "^" : "(?:^|.*/)";
29
+ rules.push({
30
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
31
+ under: new RegExp(`${prefix}${body}/.*$`),
32
+ negated,
33
+ dirOnly
34
+ });
35
+ }
36
+ return (relPath, isDir) => {
37
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
38
+ let ignored = false;
39
+ for (const r of rules) {
40
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
41
+ if (re.test(p)) ignored = !r.negated;
42
+ }
43
+ return ignored;
44
+ };
45
+ }
46
+ async function loadGitignoreMatcher(projectRoot) {
47
+ let lines = [];
48
+ try {
49
+ const raw = await fs.readFile(path.join(projectRoot, ".gitignore"), "utf8");
50
+ lines = raw.split("\n");
51
+ } catch {
52
+ }
53
+ return compileGitignore(lines);
54
+ }
55
+
56
+ export {
57
+ loadGitignoreMatcher
58
+ };
59
+ //# sourceMappingURL=chunk-LIJQMYYX.js.map
@@ -0,0 +1,260 @@
1
+ import {
2
+ createOutputSpool,
3
+ spoolNote
4
+ } from "./chunk-2TFQDEPU.js";
5
+ import {
6
+ getProcessRegistry,
7
+ redactCommand
8
+ } from "./chunk-3PVYQFRQ.js";
9
+ import {
10
+ buildWin32CmdShimInvocation,
11
+ resolveWin32Command
12
+ } from "./chunk-V43EQ6VR.js";
13
+
14
+ // src/_spawn-stream.ts
15
+ import { spawn } from "node:child_process";
16
+ import {
17
+ emitProcessCompleted,
18
+ emitProcessOutput,
19
+ emitProcessStarted
20
+ } from "@wrongstack/core/observability";
21
+ import { buildChildEnv } from "@wrongstack/core/utils";
22
+ var isWin = process.platform === "win32";
23
+ async function* spawnStream(opts) {
24
+ const max = opts.maxBytes ?? 999999999;
25
+ const flushAt = opts.flushBytes ?? 4 * 1024;
26
+ const maxQueue = opts.maxQueueSize ?? 500;
27
+ const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
28
+ let stdout = "";
29
+ let stderr = "";
30
+ let stdoutRetainedBytes = 0;
31
+ let stderrRetainedBytes = 0;
32
+ let pending = "";
33
+ let error;
34
+ const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
35
+ const resolved = resolveWin32Command(opts.cmd);
36
+ const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
37
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, opts.args) : null;
38
+ const cmd = shim?.command ?? resolved;
39
+ const args = shim?.args ?? opts.args;
40
+ const child = spawn(cmd, args, {
41
+ cwd: opts.cwd,
42
+ env: buildChildEnv(),
43
+ stdio: ["ignore", "pipe", "pipe"],
44
+ windowsHide: true,
45
+ ...isWin ? {} : { signal: opts.signal },
46
+ ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
47
+ });
48
+ const registry = getProcessRegistry();
49
+ const pid = child.pid;
50
+ const processStartedAt = Date.now();
51
+ let stdoutBytes = 0;
52
+ let stderrBytes = 0;
53
+ let telemetryCompleted = false;
54
+ emitProcessStarted({
55
+ ...pid !== void 0 ? { pid } : {},
56
+ parentPid: process.pid,
57
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
58
+ args: redactCommand(opts.args.join(" ")).split(" ").filter(Boolean),
59
+ cwd: opts.cwd,
60
+ background: false,
61
+ startedAt: new Date(processStartedAt).toISOString()
62
+ });
63
+ if (typeof pid === "number") {
64
+ registry.register({
65
+ pid,
66
+ name: opts.cmd,
67
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
68
+ startedAt: Date.now(),
69
+ child
70
+ });
71
+ }
72
+ const queue = [];
73
+ let queuedBytes = 0;
74
+ let waiter;
75
+ let paused = false;
76
+ const wake = () => {
77
+ if (waiter) {
78
+ const w = waiter;
79
+ waiter = void 0;
80
+ w();
81
+ }
82
+ };
83
+ const resume = () => {
84
+ if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
85
+ paused = false;
86
+ child.stdout?.resume();
87
+ child.stderr?.resume();
88
+ }
89
+ };
90
+ const onOut = (c) => {
91
+ const s = c.toString();
92
+ stdoutBytes += c.byteLength;
93
+ emitProcessOutput({ pid, stream: "stdout", chunk: c });
94
+ if (stdoutRetainedBytes < max) {
95
+ const retained = utf8Prefix(s, max - stdoutRetainedBytes);
96
+ stdout += retained;
97
+ stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
98
+ }
99
+ spool.write(s);
100
+ queue.push({ kind: "out", data: s, bytes: c.byteLength });
101
+ queuedBytes += c.byteLength;
102
+ wake();
103
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
104
+ paused = true;
105
+ child.stdout?.pause();
106
+ child.stderr?.pause();
107
+ }
108
+ };
109
+ const onErr = (c) => {
110
+ const s = c.toString();
111
+ stderrBytes += c.byteLength;
112
+ emitProcessOutput({ pid, stream: "stderr", chunk: c });
113
+ if (stderrRetainedBytes < max) {
114
+ const retained = utf8Prefix(s, max - stderrRetainedBytes);
115
+ stderr += retained;
116
+ stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
117
+ }
118
+ spool.write(s);
119
+ queue.push({ kind: "err", data: s, bytes: c.byteLength });
120
+ queuedBytes += c.byteLength;
121
+ wake();
122
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
123
+ paused = true;
124
+ child.stdout?.pause();
125
+ child.stderr?.pause();
126
+ }
127
+ };
128
+ child.stdout?.on("data", onOut);
129
+ child.stderr?.on("data", onErr);
130
+ child.on("error", (e) => {
131
+ error = e.message;
132
+ const bytes = Buffer.byteLength(e.message, "utf8");
133
+ queue.push({ kind: "error", data: e.message, bytes });
134
+ queuedBytes += bytes;
135
+ wake();
136
+ });
137
+ const completeTelemetry = (code, signal, timedOut = false) => {
138
+ if (telemetryCompleted) return;
139
+ telemetryCompleted = true;
140
+ emitProcessCompleted({
141
+ ...pid !== void 0 ? { pid } : {},
142
+ exitCode: code,
143
+ ...signal ? { signal } : {},
144
+ durationMs: Date.now() - processStartedAt,
145
+ stdoutBytes,
146
+ stderrBytes,
147
+ timedOut,
148
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
149
+ });
150
+ };
151
+ child.on("close", (code, signal) => {
152
+ if (typeof pid === "number") registry.unregister(pid);
153
+ const exitCode2 = code ?? (signal ? 1 : 0);
154
+ completeTelemetry(exitCode2, signal ?? void 0);
155
+ queue.push({
156
+ kind: "close",
157
+ data: "",
158
+ bytes: 0,
159
+ code: exitCode2,
160
+ ...signal ? { signal } : {}
161
+ });
162
+ wake();
163
+ });
164
+ const onAbort = () => {
165
+ if (typeof pid === "number") {
166
+ registry.kill(pid, { force: true });
167
+ } else {
168
+ try {
169
+ child.kill("SIGKILL");
170
+ } catch {
171
+ }
172
+ }
173
+ queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
174
+ completeTelemetry(124, "SIGKILL", true);
175
+ wake();
176
+ };
177
+ if (isWin) {
178
+ if (opts.signal.aborted) onAbort();
179
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
180
+ }
181
+ let exitCode = 0;
182
+ let spawnFailed = false;
183
+ try {
184
+ for (; ; ) {
185
+ while (queue.length === 0) {
186
+ await new Promise((resolve) => {
187
+ waiter = resolve;
188
+ });
189
+ }
190
+ const chunk = queue.shift();
191
+ queuedBytes -= chunk.bytes;
192
+ resume();
193
+ if (chunk.kind === "close") {
194
+ if (!spawnFailed) exitCode = chunk.code ?? 0;
195
+ break;
196
+ }
197
+ if (chunk.kind === "error") {
198
+ spawnFailed = true;
199
+ exitCode = 1;
200
+ continue;
201
+ }
202
+ pending += chunk.data;
203
+ if (pending.length >= flushAt) {
204
+ yield { type: "partial_output", text: pending };
205
+ pending = "";
206
+ }
207
+ }
208
+ if (pending.length > 0) {
209
+ yield { type: "partial_output", text: pending };
210
+ }
211
+ const spooled = spool.finalize();
212
+ return {
213
+ // The marker rides on stdout's tail so every consumer's head+tail
214
+ // normalization keeps it without per-tool changes.
215
+ stdout: spooled ? stdout + spoolNote(spooled) : stdout,
216
+ stderr,
217
+ exitCode,
218
+ truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
219
+ error,
220
+ spoolPath: spooled?.path,
221
+ spoolBytes: spooled?.bytes
222
+ };
223
+ } finally {
224
+ spool.finalize();
225
+ if (isWin) opts.signal.removeEventListener("abort", onAbort);
226
+ child.stdout?.off("data", onOut);
227
+ child.stderr?.off("data", onErr);
228
+ child.stdout?.destroy();
229
+ child.stderr?.destroy();
230
+ if (child.exitCode === null && !child.killed) {
231
+ if (typeof pid === "number") {
232
+ registry.kill(pid, { force: true });
233
+ } else {
234
+ try {
235
+ child.kill("SIGKILL");
236
+ } catch {
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ function utf8Prefix(text, maxBytes) {
243
+ if (maxBytes <= 0) return "";
244
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
245
+ let low = 0;
246
+ let high = text.length;
247
+ while (low < high) {
248
+ const mid = Math.ceil((low + high) / 2);
249
+ if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
250
+ else high = mid - 1;
251
+ }
252
+ let end = low;
253
+ if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
254
+ return text.slice(0, end);
255
+ }
256
+
257
+ export {
258
+ spawnStream
259
+ };
260
+ //# sourceMappingURL=chunk-MAN26PZ5.js.map