@wrongstack/tools 0.313.0 → 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 (134) hide show
  1. package/dist/audit.js +10 -4055
  2. package/dist/bash.js +9 -2308
  3. package/dist/batch-tool-use.js +2 -187
  4. package/dist/browser/index.js +25 -1339
  5. package/dist/builtin.js +69 -33949
  6. package/dist/chunk-2CTB3UK5.js +197 -0
  7. package/dist/chunk-2ME7ZWDD.js +1272 -0
  8. package/dist/chunk-2TFQDEPU.js +113 -0
  9. package/dist/chunk-2ZCJNKFE.js +315 -0
  10. package/dist/chunk-3PVYQFRQ.js +472 -0
  11. package/dist/chunk-3QMQBO5Z.js +89 -0
  12. package/dist/chunk-42AKKX7O.js +385 -0
  13. package/dist/chunk-42SKEETB.js +113 -0
  14. package/dist/chunk-4BHUSMNL.js +104 -0
  15. package/dist/chunk-4I4LWFOO.js +431 -0
  16. package/dist/chunk-553IQUMO.js +478 -0
  17. package/dist/chunk-55KF77OK.js +1944 -0
  18. package/dist/chunk-6HLZK37X.js +359 -0
  19. package/dist/chunk-6RNIFLG4.js +305 -0
  20. package/dist/chunk-A6BRWIXN.js +317 -0
  21. package/dist/chunk-AHHDWNXC.js +200 -0
  22. package/dist/chunk-B2CIOKWP.js +899 -0
  23. package/dist/chunk-BTSFTGIL.js +7 -0
  24. package/dist/chunk-D33S4RQG.js +222 -0
  25. package/dist/chunk-DIJKHSUO.js +134 -0
  26. package/dist/chunk-DPL47QGS.js +3515 -0
  27. package/dist/chunk-EMRGN4KM.js +170 -0
  28. package/dist/chunk-FGOABE4Z.js +955 -0
  29. package/dist/chunk-FW65TP4G.js +520 -0
  30. package/dist/chunk-GOIZNNOA.js +221 -0
  31. package/dist/chunk-H5MJUAA6.js +295 -0
  32. package/dist/chunk-H5OHH6F4.js +223 -0
  33. package/dist/chunk-HC5RM4WY.js +199 -0
  34. package/dist/chunk-HEQFMLZ7.js +190 -0
  35. package/dist/chunk-HFXSNKCV.js +196 -0
  36. package/dist/chunk-HXSBQ2SE.js +1723 -0
  37. package/dist/chunk-ILESNGNM.js +163 -0
  38. package/dist/chunk-INF5CGAO.js +137 -0
  39. package/dist/chunk-IWDFZ6XR.js +337 -0
  40. package/dist/chunk-IXMNTNW5.js +238 -0
  41. package/dist/chunk-J4SRJKBW.js +428 -0
  42. package/dist/chunk-J5SKD6LN.js +235 -0
  43. package/dist/chunk-JRJJX3L5.js +424 -0
  44. package/dist/chunk-KXNDHMUB.js +366 -0
  45. package/dist/chunk-LIJQMYYX.js +59 -0
  46. package/dist/chunk-MAN26PZ5.js +260 -0
  47. package/dist/chunk-MKRQMLZA.js +474 -0
  48. package/dist/chunk-NBDZMMG2.js +169 -0
  49. package/dist/chunk-NHA6BEIH.js +101 -0
  50. package/dist/chunk-OA6YJIWL.js +4412 -0
  51. package/dist/chunk-OZC255IC.js +2830 -0
  52. package/dist/chunk-PUR6WU2B.js +154 -0
  53. package/dist/chunk-Q5JPPCZC.js +12 -0
  54. package/dist/chunk-QBV725YG.js +128 -0
  55. package/dist/chunk-QHVW4AAM.js +892 -0
  56. package/dist/chunk-QNJ7AQ4B.js +1000 -0
  57. package/dist/chunk-RGISBZBD.js +231 -0
  58. package/dist/chunk-RJZQWEMS.js +84 -0
  59. package/dist/chunk-RP2ZA47V.js +15 -0
  60. package/dist/chunk-RXISBFR5.js +315 -0
  61. package/dist/chunk-S5CCLZL3.js +99 -0
  62. package/dist/chunk-TVTCSBJF.js +122 -0
  63. package/dist/chunk-U4D5S3VQ.js +21 -0
  64. package/dist/chunk-U4VAATKP.js +193 -0
  65. package/dist/chunk-UKU2ZX4C.js +203 -0
  66. package/dist/chunk-UNSKG6DC.js +159 -0
  67. package/dist/chunk-URFKEIHM.js +1091 -0
  68. package/dist/chunk-UTV7YAW7.js +178 -0
  69. package/dist/chunk-V43EQ6VR.js +68 -0
  70. package/dist/chunk-VB3MJGCV.js +610 -0
  71. package/dist/chunk-WHIIIREL.js +223 -0
  72. package/dist/chunk-XMIFYKUT.js +1371 -0
  73. package/dist/chunk-YF2RSV34.js +54 -0
  74. package/dist/chunk-Z6DAKQ3U.js +1 -0
  75. package/dist/chunk-ZIGAZFWE.js +522 -0
  76. package/dist/circuit-breaker.js +3 -191
  77. package/dist/codebase-index/index.js +84 -13688
  78. package/dist/codebase-index/parser-worker-script.js +8 -3995
  79. package/dist/codebase-index/project-server.js +60 -8548
  80. package/dist/codebase-index/worker.js +20 -8416
  81. package/dist/design.js +2 -299
  82. package/dist/diff.js +4 -256
  83. package/dist/document.js +4 -222
  84. package/dist/e2e.js +5 -476
  85. package/dist/edit.js +14 -10200
  86. package/dist/exec.js +16 -3131
  87. package/dist/fetch.js +8 -398
  88. package/dist/format.js +10 -3750
  89. package/dist/generic-parser-VUXDIAOZ.js +13 -0
  90. package/dist/git.js +4 -380
  91. package/dist/glob.js +6 -280
  92. package/dist/go-parser-6CA77WZ4.js +469 -0
  93. package/dist/grep.js +7 -604
  94. package/dist/index.js +318 -35387
  95. package/dist/install.js +10 -4091
  96. package/dist/json-parser-7YMDJSBJ.js +213 -0
  97. package/dist/json.js +4 -665
  98. package/dist/kanban.js +3 -2096
  99. package/dist/languages/index.js +23 -4664
  100. package/dist/legacy-bridge-7UKZT46G.js +18 -0
  101. package/dist/lint.js +10 -3706
  102. package/dist/logs.js +4 -287
  103. package/dist/memory.js +6 -215
  104. package/dist/mode.js +3 -99
  105. package/dist/outdated.js +4 -5170
  106. package/dist/pack.js +65 -33897
  107. package/dist/parser-dispatch-VPOFDO2U.js +14 -0
  108. package/dist/patch.js +13 -9935
  109. package/dist/plan.js +5 -3286
  110. package/dist/process-registry.js +8 -651
  111. package/dist/ps-slash.js +12 -1439
  112. package/dist/py-parser-GPM5VYRG.js +16 -0
  113. package/dist/read.js +13 -9816
  114. package/dist/replace.js +15 -10014
  115. package/dist/rs-parser-WVQ4KYCX.js +81 -0
  116. package/dist/scaffold.js +4 -253
  117. package/dist/search.js +5 -585
  118. package/dist/session-kanban.js +30 -972
  119. package/dist/skill.js +2 -215
  120. package/dist/task.js +5 -3410
  121. package/dist/test.js +10 -3773
  122. package/dist/todo.js +4 -2891
  123. package/dist/tool-help.js +3 -123
  124. package/dist/tool-search.js +3 -79
  125. package/dist/tool-tier.js +68 -33984
  126. package/dist/tool-use.js +2 -93
  127. package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
  128. package/dist/tree.js +4 -288
  129. package/dist/ts-parser-YH4ZMWMH.js +232 -0
  130. package/dist/typecheck.js +10 -3753
  131. package/dist/win32.js +6 -60
  132. package/dist/write.js +14 -9882
  133. package/dist/yaml-parser-GFAOKV53.js +157 -0
  134. package/package.json +5 -5
package/dist/install.js CHANGED
@@ -1,4095 +1,14 @@
1
- // src/install.ts
2
- import { join as join8 } from "node:path";
3
1
  import {
4
- detectEcosystem as detectPackageEcosystem,
5
- recordPackageAction
6
- } from "@wrongstack/core/coordination";
7
-
8
- // src/_spawn-stream.ts
9
- import { spawn as spawn2 } from "node:child_process";
10
- import {
11
- emitProcessCompleted,
12
- emitProcessOutput,
13
- emitProcessStarted
14
- } from "@wrongstack/core/observability";
15
- import { buildChildEnv } from "@wrongstack/core/utils";
16
-
17
- // src/_output-spool.ts
18
- import { createWriteStream, mkdirSync } from "node:fs";
19
- import * as fsp from "node:fs/promises";
20
- import * as path from "node:path";
21
- import { wstackGlobalRoot } from "@wrongstack/core/utils";
22
- var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
23
- var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
24
- var sweepStarted = false;
25
- function toolOutputDir() {
26
- return path.join(wstackGlobalRoot(), "tool-output");
27
- }
28
- function sweepOldSpoolFiles(dir) {
29
- if (sweepStarted) return;
30
- sweepStarted = true;
31
- void (async () => {
32
- try {
33
- const now = Date.now();
34
- for (const name of await fsp.readdir(dir)) {
35
- if (!name.endsWith(".log")) continue;
36
- const p = path.join(dir, name);
37
- try {
38
- const st = await fsp.stat(p);
39
- if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fsp.unlink(p);
40
- } catch {
41
- }
42
- }
43
- } catch {
44
- }
45
- })();
46
- }
47
- function spoolNote(info) {
48
- const dropped = info.droppedBytes > 0 ? `, ~${info.droppedBytes} bytes dropped under backpressure` : "";
49
- return `
50
- [output truncated \u2014 full ${info.bytes} bytes at ${info.path}${dropped}; read/grep that file selectively instead of re-running with more output]`;
51
- }
52
- function createOutputSpool(opts) {
53
- const threshold = opts.thresholdBytes ?? 32768;
54
- const safeTool = opts.tool.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
55
- let head = "";
56
- let headBytes = 0;
57
- let totalBytes = 0;
58
- let droppedBytes = 0;
59
- let stream = null;
60
- let filePath = null;
61
- let failed = false;
62
- let finalized = false;
63
- const open = () => {
64
- if (stream || failed) return;
65
- try {
66
- const dir = toolOutputDir();
67
- mkdirSync(dir, { recursive: true });
68
- sweepOldSpoolFiles(dir);
69
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
70
- const rand = Math.random().toString(36).slice(2, 6);
71
- filePath = path.join(dir, `${stamp}-${safeTool}-${rand}.log`);
72
- stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
73
- stream.on("error", () => {
74
- failed = true;
75
- stream = null;
76
- filePath = null;
77
- });
78
- stream.write(head);
79
- } catch {
80
- failed = true;
81
- stream = null;
82
- filePath = null;
83
- }
84
- };
85
- return {
86
- write(text) {
87
- if (finalized || !text) return;
88
- const textBytes = Buffer.byteLength(text, "utf8");
89
- totalBytes += textBytes;
90
- if (!stream && !failed) {
91
- if (headBytes + textBytes <= threshold) {
92
- head += text;
93
- headBytes += textBytes;
94
- return;
95
- }
96
- head += text;
97
- open();
98
- head = "";
99
- return;
100
- }
101
- if (stream) {
102
- if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
103
- droppedBytes += textBytes;
104
- return;
105
- }
106
- stream.write(text);
107
- }
108
- },
109
- finalize() {
110
- if (finalized) {
111
- return filePath ? { path: filePath, bytes: totalBytes, droppedBytes } : null;
112
- }
113
- finalized = true;
114
- head = "";
115
- if (!stream || !filePath) return null;
116
- try {
117
- stream.end();
118
- } catch {
119
- }
120
- return { path: filePath, bytes: totalBytes, droppedBytes };
121
- }
122
- };
123
- }
124
-
125
- // src/_win32-resolve.ts
126
- import * as fs from "node:fs";
127
- import * as path2 from "node:path";
128
- function resolveWin32Command(cmd) {
129
- if (process.platform !== "win32") return cmd;
130
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
131
- return cmd;
132
- }
133
- const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
134
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
135
- for (const dir of pathDirs) {
136
- const base = path2.join(dir, cmd);
137
- for (const ext of pathext) {
138
- const full = `${base}${ext}`;
139
- try {
140
- fs.accessSync(full, fs.constants.X_OK);
141
- return full;
142
- } catch {
143
- }
144
- }
145
- }
146
- return cmd;
147
- }
148
- var WIN32_SHELL_META = /[&|<>"%\r\n\0]/;
149
- function assertSafeWin32ShellArgs(args) {
150
- for (const arg of args) {
151
- if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
152
- throw new Error(
153
- 'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > " %, or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
154
- );
155
- }
156
- }
157
- }
158
- function buildWin32CmdShimInvocation(command, args = []) {
159
- assertSafeWin32ShellArgs([command, ...args]);
160
- const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
161
- return {
162
- command: process.env["COMSPEC"] ?? "cmd.exe",
163
- args: ["/d", "/c", line],
164
- windowsVerbatimArguments: true
165
- };
166
- }
167
- function quoteWin32CmdArg(arg) {
168
- return `"${arg}"`;
169
- }
170
-
171
- // src/process-registry.ts
172
- import { spawn } from "node:child_process";
173
- import * as os from "node:os";
174
-
175
- // src/circuit-breaker.ts
176
- var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
177
- var DEFAULT_SLOW_CALL_THRESHOLD_MS = 18e4;
178
- var DEFAULT_MAX_SLOW_CALLS = 3;
179
- var DEFAULT_WINDOW_MS = 6e4;
180
- var DEFAULT_MAX_CALLS_PER_WINDOW = 30;
181
- var DEFAULT_COOLDOWN_MS = 3e4;
182
- var CircuitBreaker = class {
183
- maxConsecutiveFailures;
184
- slowCallThresholdMs;
185
- maxSlowCalls;
186
- windowMs;
187
- maxCallsPerWindow;
188
- cooldownMs;
189
- state = "closed";
190
- consecutiveFailures = 0;
191
- window = [];
192
- lastFailureAt = null;
193
- lastSlowAt = null;
194
- /** Timestamp when the breaker was opened (for cooldown calculation). */
195
- openedAt = null;
196
- /**
197
- * Master enable flag. When false the breaker is bypassed: `beforeCall`
198
- * always returns true and `afterCall` records nothing. The class itself
199
- * defaults to enabled (so the standalone unit tests exercise tripping); the
200
- * ProcessRegistry flips this off until the user opts in via `/settings`.
201
- */
202
- enabled = true;
203
- /**
204
- * Fired (best-effort) when the breaker transitions into the `open` state.
205
- * The registry uses this to arm its auto kill/reset countdown.
206
- */
207
- onTrip;
208
- /**
209
- * Fired (best-effort) when the breaker returns to `closed` after having been
210
- * open/half-open. The registry uses this to cancel a pending kill/reset.
211
- */
212
- onReset;
213
- constructor(config = {}) {
214
- this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
215
- this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
216
- this.maxSlowCalls = config.maxSlowCalls ?? DEFAULT_MAX_SLOW_CALLS;
217
- this.windowMs = config.windowMs ?? DEFAULT_WINDOW_MS;
218
- this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
219
- this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
220
- }
221
- /** Toggle the master enable. Disabling resets to a clean `closed` state. */
222
- setEnabled(enabled) {
223
- if (this.enabled === enabled) return;
224
- this.enabled = enabled;
225
- if (!enabled) this._reset();
226
- }
227
- get isEnabled() {
228
- return this.enabled;
229
- }
230
- /**
231
- * Returns true if the circuit allows a new call to proceed.
232
- * When false, callers should abort the tool call and return a
233
- * circuit-breaker error instead of spawning a process.
234
- */
235
- get canProceed() {
236
- if (!this.enabled) return true;
237
- this._checkStateTransition();
238
- return this.state !== "open";
239
- }
240
- /**
241
- * Snapshot of the current breaker state for observability (`/kill`).
242
- */
243
- snapshot() {
244
- this._checkStateTransition();
245
- const now = Date.now();
246
- let cooldownRemaining = null;
247
- if (this.openedAt !== null && this.state === "open") {
248
- const elapsed = now - this.openedAt;
249
- cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);
250
- }
251
- return {
252
- state: this.state,
253
- consecutiveFailures: this.consecutiveFailures,
254
- slowCallsInWindow: this.window.filter((c) => c.slow).length,
255
- callsInWindow: this.window.length,
256
- windowMs: this.windowMs,
257
- cooldownRemainingMs: cooldownRemaining,
258
- lastFailureAt: this.lastFailureAt,
259
- lastSlowAt: this.lastSlowAt
260
- };
261
- }
262
- /**
263
- * Call this BEFORE spawning a bash/exec process.
264
- * Returns true if the call is allowed; false if the breaker is open.
265
- * When false, callers MUST NOT spawn a process.
266
- *
267
- * @param bypass - If true, skip the circuit breaker check entirely.
268
- * Use for background/fire-and-forget processes that should
269
- * not affect breaker state.
270
- */
271
- beforeCall(bypass = false) {
272
- if (bypass || !this.enabled) return true;
273
- this._checkStateTransition();
274
- if (this.state === "open") return false;
275
- return true;
276
- }
277
- /**
278
- * Call this AFTER a bash/exec process finishes (success or failure).
279
- * `durationMs` is the wall-clock time the process ran.
280
- * `failed` is true when the process returned a non-zero exit code or
281
- * threw an exception before spawning.
282
- *
283
- * @param bypass - If true, do not update breaker state.
284
- * Use for background/fire-and-forget processes.
285
- */
286
- afterCall(durationMs, failed, bypass = false) {
287
- if (bypass || !this.enabled) return;
288
- const now = Date.now();
289
- if (this.state === "half-open") {
290
- if (failed) {
291
- this._trip();
292
- return;
293
- }
294
- this._reset();
295
- return;
296
- }
297
- this._pruneWindow(now);
298
- const slow = durationMs >= this.slowCallThresholdMs;
299
- this.window.push({ at: now, failed, slow });
300
- if (failed) {
301
- this.consecutiveFailures++;
302
- this.lastFailureAt = now;
303
- if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
304
- this._trip();
305
- }
306
- return;
307
- }
308
- this.consecutiveFailures = 0;
309
- if (slow) {
310
- this.lastSlowAt = now;
311
- const slowCount = this.window.filter((c) => c.slow).length;
312
- if (slowCount >= this.maxSlowCalls) {
313
- this._trip();
314
- }
315
- }
316
- const callCount = this.window.length;
317
- if (callCount >= this.maxCallsPerWindow) {
318
- this._trip();
319
- }
320
- }
321
- /** Force the breaker open. Used by /kill force and Ctrl+C. */
322
- forceOpen() {
323
- this._trip();
324
- }
325
- /** Force a reset to closed. Used by tests and /kill reset. */
326
- forceReset() {
327
- this._reset();
328
- }
329
- _trip() {
330
- if (this.state === "open") return;
331
- this.state = "open";
332
- this.openedAt = Date.now();
333
- this.window = [];
334
- try {
335
- this.onTrip?.();
336
- } catch {
337
- }
338
- }
339
- _reset() {
340
- const wasRecovering = this.state !== "closed";
341
- this.state = "closed";
342
- this.consecutiveFailures = 0;
343
- this.window = [];
344
- this.openedAt = null;
345
- if (wasRecovering) {
346
- try {
347
- this.onReset?.();
348
- } catch {
349
- }
350
- }
351
- }
352
- /** Transition from open → half-open when cooldown elapses. */
353
- _checkStateTransition() {
354
- if (this.state !== "open" || this.openedAt === null) return;
355
- const elapsed = Date.now() - this.openedAt;
356
- if (elapsed >= this.cooldownMs) {
357
- this.state = "half-open";
358
- this.openedAt = null;
359
- }
360
- }
361
- _pruneWindow(now) {
362
- const cutoff = now - this.windowMs;
363
- this.window = this.window.filter((c) => c.at >= cutoff);
364
- }
365
- };
366
-
367
- // src/_redact-command.ts
368
- import { expectDefined } from "@wrongstack/core/utils";
369
- var SENSITIVE_FLAG_PATTERNS = [
370
- // --flag=value or --flag "value" (value captured up to next space or comma)
371
- /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
372
- // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
373
- // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
374
- // The value must be token-like (>= 8 chars) so ordinary combined flags such
375
- // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
376
- // redacted, not just the first.
377
- // NOTE: synced with @wrongstack/core observability/redact-command.ts.
378
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
379
- // -p|-password|-a (redis auth) short flags: attached + separated + =value.
380
- // Same token-start anchor; over-redaction is an accepted tradeoff for a
381
- // redaction function. Synced with core copy.
382
- /(?<![-\w])-(?:password|p|a)(?:[=\s]+)?[^\s,-]+/gi,
383
- // env var–style secrets: TOKEN=x, API_KEY=y, etc.
384
- /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD|PASSPHRASE)\s*[=:]\s*[^\s,]+/gi,
385
- // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
386
- // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
387
- // every such flag in the command line is redacted, not just the first.
388
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
389
- ];
390
- function redactCommand(cmd) {
391
- let result = cmd;
392
- for (const pattern of SENSITIVE_FLAG_PATTERNS) {
393
- result = result.replace(pattern, (match) => {
394
- const eq = match.indexOf("=");
395
- const sp = match.search(/\s/);
396
- const delim = eq !== -1 ? "=" : sp !== -1 ? match[sp] : null;
397
- if (delim !== null) {
398
- const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);
399
- return `${flag}[REDACTED]`;
400
- }
401
- if (match.startsWith("--")) {
402
- return match;
403
- }
404
- return `${match.slice(0, 2)}[REDACTED]`;
405
- });
406
- }
407
- return result;
408
- }
409
-
410
- // src/process-registry.ts
411
- var DEFAULT_GRACE_MS = 2e3;
412
- var WIN32_TASKKILL_TIMEOUT_MS = 5e3;
413
- function killWin32Tree(pid, opts = {}) {
414
- try {
415
- const child = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
416
- stdio: "ignore",
417
- windowsHide: true
418
- });
419
- let settled = false;
420
- let timeout;
421
- const settle = () => {
422
- if (settled) return;
423
- settled = true;
424
- if (timeout) clearTimeout(timeout);
425
- try {
426
- opts.onSettled?.();
427
- } catch {
428
- }
429
- };
430
- child.on("error", settle);
431
- child.on("close", settle);
432
- timeout = setTimeout(
433
- () => {
434
- try {
435
- child.kill();
436
- } catch {
437
- }
438
- settle();
439
- },
440
- Math.max(1, opts.timeoutMs ?? WIN32_TASKKILL_TIMEOUT_MS)
441
- );
442
- timeout.unref?.();
443
- child.unref();
444
- return true;
445
- } catch {
446
- return false;
447
- }
448
- }
449
- var ProcessRegistryImpl = class {
450
- processes = /* @__PURE__ */ new Map();
451
- breaker;
452
- /**
453
- * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
454
- * a countdown is armed; on expiry all tracked processes are killed and the
455
- * breaker is reset to closed (forced recovery). Zero means manual recovery
456
- * only (`/kill reset`).
457
- */
458
- autoKillResetMs = 0;
459
- autoKillTimer = null;
460
- autoKillArmedAt = null;
461
- breakerCountdownListeners = [];
462
- constructor(breakerConfig) {
463
- this.breaker = new CircuitBreaker(breakerConfig);
464
- this.breaker.onTrip = () => this._armAutoKillReset();
465
- this.breaker.onReset = () => this._cancelAutoKillReset();
466
- this.breaker.setEnabled(false);
467
- }
468
- register(info) {
469
- this.processes.set(info.pid, {
470
- ...info,
471
- killed: false,
472
- protected: info.protected ?? false,
473
- background: info.background ?? false
474
- });
475
- }
476
- _isSafeSignalPid(pid) {
477
- return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
478
- }
479
- _canSignalProcessGroup(p) {
480
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
481
- }
482
- _killChildDirect(p, signal) {
483
- try {
484
- if (p.child) {
485
- p.child.kill(signal);
486
- return;
487
- }
488
- if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
489
- } catch {
490
- }
491
- }
492
- _killPosix(p, signal) {
493
- if (this._canSignalProcessGroup(p)) {
494
- try {
495
- process.kill(-p.pid, signal);
496
- return;
497
- } catch {
498
- }
499
- }
500
- this._killChildDirect(p, signal);
501
- }
502
- /** Unregister a process by PID. Called on 'close' / 'exit' events. */
503
- unregister(pid) {
504
- this.processes.delete(pid);
505
- }
506
- /** Get a single process by PID. */
507
- get(pid) {
508
- this._pruneStale(pid);
509
- return this.processes.get(pid);
510
- }
511
- /** Get all tracked processes. */
512
- list() {
513
- this._pruneAllStale();
514
- return Array.from(this.processes.values());
515
- }
516
- /** Get processes filtered by name (e.g. 'bash', 'exec'). */
517
- byName(name) {
518
- return this.list().filter((p) => p.name === name);
519
- }
520
- /** Get processes filtered by session. */
521
- bySession(sessionId) {
522
- return this.list().filter((p) => p.sessionId === sessionId);
523
- }
524
- /** Count of active (non-killed) processes. */
525
- get activeCount() {
526
- let n = 0;
527
- for (const p of this.processes.values()) {
528
- if (!p.killed) n++;
529
- }
530
- return n;
531
- }
532
- /** Count of active jobs explicitly launched in background mode. */
533
- get activeBackgroundCount() {
534
- let n = 0;
535
- for (const p of this.processes.values()) {
536
- if (p.background && !p.killed) n++;
537
- }
538
- return n;
539
- }
540
- /**
541
- * Combined stats for observability — used by /ps and the TUI status bar.
542
- */
543
- stats() {
544
- this._pruneAllStale();
545
- return {
546
- activeCount: this.activeCount,
547
- backgroundCount: this.activeBackgroundCount,
548
- totalCount: this.processes.size,
549
- breaker: this.breaker.snapshot()
550
- };
551
- }
552
- /**
553
- * Returns true if the circuit allows a new bash/exec call to proceed.
554
- * When false, callers MUST NOT spawn a process.
555
- */
556
- get canProceed() {
557
- return this.breaker.canProceed;
558
- }
559
- /**
560
- * Called before spawning a process. Returns true if allowed; false if
561
- * the circuit breaker is open.
562
- *
563
- * @param bypass - If true, skip circuit breaker check (for background processes).
564
- */
565
- beforeCall(bypass = false) {
566
- return this.breaker.beforeCall(bypass);
567
- }
568
- /**
569
- * Called after a process finishes. `durationMs` is wall-clock time;
570
- * `failed` is true for non-zero exit codes.
571
- *
572
- * @param bypass - If true, do not update circuit breaker state (for background processes).
573
- */
574
- afterCall(durationMs, failed, bypass = false) {
575
- this.breaker.afterCall(durationMs, failed, bypass);
576
- }
577
- /** Force-open the circuit breaker (Ctrl+C, /kill force). */
578
- forceBreakerOpen() {
579
- this.breaker.forceOpen();
580
- }
581
- /** Force-reset the circuit breaker to closed (/kill reset). */
582
- forceBreakerReset() {
583
- this.breaker.forceReset();
584
- }
585
- /**
586
- * Configure circuit-breaker protection at runtime. Called from `/settings`
587
- * (instant, all modes) and on TUI mount (applies persisted config).
588
- *
589
- * - `enabled` toggles whether the breaker gates `bash`/`exec`.
590
- * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
591
- * trips (0 = manual recovery only).
592
- *
593
- * Re-applies cleanly on every call: cancels a pending countdown when the
594
- * timeout is cleared or protection disabled, and re-arms if the breaker is
595
- * currently open under the new settings.
596
- */
597
- setBreakerConfig(cfg) {
598
- if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
599
- if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
600
- if (this.autoKillResetMs <= 0) {
601
- this._cancelAutoKillReset();
602
- return;
603
- }
604
- if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
605
- this._armAutoKillReset();
606
- }
607
- }
608
- /**
609
- * Live countdown to the next auto kill/reset, or null when nothing is armed.
610
- * The TUI polls this on a 1s tick while armed so the statusline decrements.
611
- */
612
- getBreakerCountdown() {
613
- if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
614
- const elapsed = Date.now() - this.autoKillArmedAt;
615
- return {
616
- remainingMs: Math.max(0, this.autoKillResetMs - elapsed),
617
- totalMs: this.autoKillResetMs
618
- };
619
- }
620
- /**
621
- * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
622
- * Use {@link getBreakerCountdown} for the live ticking value between events.
623
- */
624
- onBreakerCountdownChange(listener) {
625
- this.breakerCountdownListeners.push(listener);
626
- return () => {
627
- this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
628
- };
629
- }
630
- _emitBreakerCountdown() {
631
- const snap = this.getBreakerCountdown();
632
- for (const l of this.breakerCountdownListeners) {
633
- try {
634
- l(snap);
635
- } catch {
636
- }
637
- }
638
- }
639
- /**
640
- * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
641
- * (a fresh trip after a failed half-open probe restarts the clock). No-op
642
- * when protection is off or no timeout is configured.
643
- */
644
- _armAutoKillReset() {
645
- if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
646
- this._clearAutoKillTimer();
647
- this.autoKillArmedAt = Date.now();
648
- this.autoKillTimer = setTimeout(() => {
649
- this.autoKillTimer = null;
650
- this.autoKillArmedAt = null;
651
- this.killAll({ force: false, preserveBackground: true });
652
- this.breaker.forceReset();
653
- this._emitBreakerCountdown();
654
- }, this.autoKillResetMs);
655
- this.autoKillTimer.unref?.();
656
- this._emitBreakerCountdown();
657
- }
658
- _cancelAutoKillReset() {
659
- const wasArmed = this.autoKillArmedAt !== null;
660
- this._clearAutoKillTimer();
661
- if (wasArmed) {
662
- this.autoKillArmedAt = null;
663
- this._emitBreakerCountdown();
664
- }
665
- }
666
- _clearAutoKillTimer() {
667
- if (this.autoKillTimer !== null) {
668
- clearTimeout(this.autoKillTimer);
669
- this.autoKillTimer = null;
670
- }
671
- }
672
- /** Kill a single process by PID.
673
- *
674
- * On POSIX: sends SIGTERM to the *process group* (-pid) so that
675
- * runaway grandchild processes (`sleep 9999 & disown`) are also killed.
676
- * After `graceMs` a SIGKILL is sent if the process hasn't exited.
677
- *
678
- * On Windows: `child.kill()` maps to TerminateProcess — process groups
679
- * are not meaningfully supported. A second `force=true` call sends
680
- * SIGKILL (which maps to TerminateProcess again — the distinction is
681
- * in the exit code, not the signal).
682
- *
683
- * Returns true if the process was found and kill was attempted.
684
- */
685
- kill(pid, opts = {}) {
686
- this._pruneStale(pid);
687
- const p = this.processes.get(pid);
688
- if (!p) return false;
689
- if (p.killed && opts.force !== true) return true;
690
- if (p.protected && opts.includeProtected !== true) return false;
691
- if (opts.preserveBackground && p.background) return false;
692
- const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
693
- const isWin2 = os.platform() === "win32";
694
- if (isWin2) {
695
- const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
696
- const directFallback = () => {
697
- if (p.child && p.child.exitCode === null) {
698
- try {
699
- p.child.kill("SIGKILL");
700
- } catch {
701
- }
702
- }
703
- };
704
- if (liveRealChild && killWin32Tree(pid, {
705
- timeoutMs: Math.max(graceMs, WIN32_TASKKILL_TIMEOUT_MS),
706
- onSettled: directFallback
707
- })) {
708
- } else {
709
- this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
710
- }
711
- p.killed = true;
712
- return true;
713
- }
714
- try {
715
- if (force) {
716
- this._killPosix(p, "SIGKILL");
717
- } else {
718
- this._killPosix(p, "SIGTERM");
719
- const timer = setTimeout(() => {
720
- if (this.processes.has(pid) && !p.child?.killed) {
721
- this._killPosix(p, "SIGKILL");
722
- }
723
- }, graceMs);
724
- timer.unref?.();
725
- }
726
- } catch {
727
- }
728
- p.killed = true;
729
- return true;
730
- }
731
- /**
732
- * Kill all tracked processes.
733
- * Returns the PIDs that were kill()ed.
734
- */
735
- killAll(opts = {}) {
736
- const pids = Array.from(this.processes.keys());
737
- const killed = [];
738
- const includeProtected = opts.includeProtected === true;
739
- for (const pid of pids) {
740
- const p = this.processes.get(pid);
741
- if (!p) continue;
742
- if (p.protected && !includeProtected) continue;
743
- if (opts.preserveBackground && p.background) continue;
744
- if (this.kill(pid, opts)) killed.push(pid);
745
- }
746
- return killed;
747
- }
748
- /**
749
- * Kill all processes for a specific session.
750
- * Returns the PIDs that were kill()ed.
751
- */
752
- killSession(sessionId, opts = {}) {
753
- const pids = this.bySession(sessionId).map((p) => p.pid);
754
- const killed = [];
755
- for (const pid of pids) {
756
- if (this.kill(pid, opts)) killed.push(pid);
757
- }
758
- return killed;
759
- }
760
- /**
761
- * Check whether a tracked process entry is stale — the child has exited
762
- * (exitCode !== null) AND it's been in the registry long enough that the
763
- * OS may have reused the PID for a new, unrelated process.
764
- *
765
- * P3 #24 (before-release.md): on POSIX, PIDs are reused after process
766
- * exit. If a tracked process exits but its 'close' event hasn't fired yet
767
- * (or was missed), the registry still holds the entry. A new process
768
- * gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
769
- * may incorrectly protect or target the wrong process.
770
- *
771
- * The 60s threshold is conservative — the OS typically waits much longer
772
- * before reusing a PID, but we want to clean up before that becomes a risk.
773
- */
774
- _isStaleEntry(entry) {
775
- if (entry.child === null) {
776
- if (Date.now() - entry.startedAt <= 6e4) return false;
777
- if (os.platform() === "win32") return false;
778
- try {
779
- process.kill(entry.pid, 0);
780
- return false;
781
- } catch (err) {
782
- return err.code !== "EPERM";
783
- }
784
- }
785
- return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
786
- }
787
- /**
788
- * Remove a stale entry for a specific PID before any PID-based lookup.
789
- * This prevents PID reuse from causing the registry to act on a dead
790
- * process that has been replaced by a new one with the same PID.
791
- */
792
- _pruneStale(pid) {
793
- const entry = this.processes.get(pid);
794
- if (entry && this._isStaleEntry(entry)) {
795
- this.processes.delete(pid);
796
- }
797
- }
798
- /**
799
- * Remove every stale entry, not just one PID. `list()`/`stats()` — the
800
- * surfaces the TUI status bar and `/ps` poll — must prune too: a child
801
- * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
802
- * open) would otherwise linger in the registry until someone looks up its
803
- * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
804
- * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
805
- */
806
- _pruneAllStale() {
807
- for (const [pid, entry] of this.processes) {
808
- if (this._isStaleEntry(entry)) this.processes.delete(pid);
809
- }
810
- }
811
- };
812
- var _registry;
813
- function getProcessRegistry() {
814
- if (!_registry) {
815
- _registry = new ProcessRegistryImpl();
816
- }
817
- return _registry;
818
- }
819
-
820
- // src/_spawn-stream.ts
821
- var isWin = process.platform === "win32";
822
- async function* spawnStream(opts) {
823
- const max = opts.maxBytes ?? 999999999;
824
- const flushAt = opts.flushBytes ?? 4 * 1024;
825
- const maxQueue = opts.maxQueueSize ?? 500;
826
- const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
827
- let stdout = "";
828
- let stderr = "";
829
- let stdoutRetainedBytes = 0;
830
- let stderrRetainedBytes = 0;
831
- let pending = "";
832
- let error;
833
- const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
834
- const resolved = resolveWin32Command(opts.cmd);
835
- const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
836
- const shim = needsShell ? buildWin32CmdShimInvocation(resolved, opts.args) : null;
837
- const cmd = shim?.command ?? resolved;
838
- const args = shim?.args ?? opts.args;
839
- const child = spawn2(cmd, args, {
840
- cwd: opts.cwd,
841
- env: buildChildEnv(),
842
- stdio: ["ignore", "pipe", "pipe"],
843
- windowsHide: true,
844
- ...isWin ? {} : { signal: opts.signal },
845
- ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
846
- });
847
- const registry = getProcessRegistry();
848
- const pid = child.pid;
849
- const processStartedAt = Date.now();
850
- let stdoutBytes = 0;
851
- let stderrBytes = 0;
852
- let telemetryCompleted = false;
853
- emitProcessStarted({
854
- ...pid !== void 0 ? { pid } : {},
855
- parentPid: process.pid,
856
- command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
857
- args: redactCommand(opts.args.join(" ")).split(" ").filter(Boolean),
858
- cwd: opts.cwd,
859
- background: false,
860
- startedAt: new Date(processStartedAt).toISOString()
861
- });
862
- if (typeof pid === "number") {
863
- registry.register({
864
- pid,
865
- name: opts.cmd,
866
- command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
867
- startedAt: Date.now(),
868
- child
869
- });
870
- }
871
- const queue = [];
872
- let queuedBytes = 0;
873
- let waiter;
874
- let paused = false;
875
- const wake = () => {
876
- if (waiter) {
877
- const w = waiter;
878
- waiter = void 0;
879
- w();
880
- }
881
- };
882
- const resume = () => {
883
- if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
884
- paused = false;
885
- child.stdout?.resume();
886
- child.stderr?.resume();
887
- }
888
- };
889
- const onOut = (c) => {
890
- const s = c.toString();
891
- stdoutBytes += c.byteLength;
892
- emitProcessOutput({ pid, stream: "stdout", chunk: c });
893
- if (stdoutRetainedBytes < max) {
894
- const retained = utf8Prefix(s, max - stdoutRetainedBytes);
895
- stdout += retained;
896
- stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
897
- }
898
- spool.write(s);
899
- queue.push({ kind: "out", data: s, bytes: c.byteLength });
900
- queuedBytes += c.byteLength;
901
- wake();
902
- if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
903
- paused = true;
904
- child.stdout?.pause();
905
- child.stderr?.pause();
906
- }
907
- };
908
- const onErr = (c) => {
909
- const s = c.toString();
910
- stderrBytes += c.byteLength;
911
- emitProcessOutput({ pid, stream: "stderr", chunk: c });
912
- if (stderrRetainedBytes < max) {
913
- const retained = utf8Prefix(s, max - stderrRetainedBytes);
914
- stderr += retained;
915
- stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
916
- }
917
- spool.write(s);
918
- queue.push({ kind: "err", data: s, bytes: c.byteLength });
919
- queuedBytes += c.byteLength;
920
- wake();
921
- if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
922
- paused = true;
923
- child.stdout?.pause();
924
- child.stderr?.pause();
925
- }
926
- };
927
- child.stdout?.on("data", onOut);
928
- child.stderr?.on("data", onErr);
929
- child.on("error", (e) => {
930
- error = e.message;
931
- const bytes = Buffer.byteLength(e.message, "utf8");
932
- queue.push({ kind: "error", data: e.message, bytes });
933
- queuedBytes += bytes;
934
- wake();
935
- });
936
- const completeTelemetry = (code, signal, timedOut = false) => {
937
- if (telemetryCompleted) return;
938
- telemetryCompleted = true;
939
- emitProcessCompleted({
940
- ...pid !== void 0 ? { pid } : {},
941
- exitCode: code,
942
- ...signal ? { signal } : {},
943
- durationMs: Date.now() - processStartedAt,
944
- stdoutBytes,
945
- stderrBytes,
946
- timedOut,
947
- endedAt: (/* @__PURE__ */ new Date()).toISOString()
948
- });
949
- };
950
- child.on("close", (code, signal) => {
951
- if (typeof pid === "number") registry.unregister(pid);
952
- const exitCode2 = code ?? (signal ? 1 : 0);
953
- completeTelemetry(exitCode2, signal ?? void 0);
954
- queue.push({
955
- kind: "close",
956
- data: "",
957
- bytes: 0,
958
- code: exitCode2,
959
- ...signal ? { signal } : {}
960
- });
961
- wake();
962
- });
963
- const onAbort = () => {
964
- if (typeof pid === "number") {
965
- registry.kill(pid, { force: true });
966
- } else {
967
- try {
968
- child.kill("SIGKILL");
969
- } catch {
970
- }
971
- }
972
- queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
973
- completeTelemetry(124, "SIGKILL", true);
974
- wake();
975
- };
976
- if (isWin) {
977
- if (opts.signal.aborted) onAbort();
978
- else opts.signal.addEventListener("abort", onAbort, { once: true });
979
- }
980
- let exitCode = 0;
981
- let spawnFailed = false;
982
- try {
983
- for (; ; ) {
984
- while (queue.length === 0) {
985
- await new Promise((resolve6) => {
986
- waiter = resolve6;
987
- });
988
- }
989
- const chunk = queue.shift();
990
- queuedBytes -= chunk.bytes;
991
- resume();
992
- if (chunk.kind === "close") {
993
- if (!spawnFailed) exitCode = chunk.code ?? 0;
994
- break;
995
- }
996
- if (chunk.kind === "error") {
997
- spawnFailed = true;
998
- exitCode = 1;
999
- continue;
1000
- }
1001
- pending += chunk.data;
1002
- if (pending.length >= flushAt) {
1003
- yield { type: "partial_output", text: pending };
1004
- pending = "";
1005
- }
1006
- }
1007
- if (pending.length > 0) {
1008
- yield { type: "partial_output", text: pending };
1009
- }
1010
- const spooled = spool.finalize();
1011
- return {
1012
- // The marker rides on stdout's tail so every consumer's head+tail
1013
- // normalization keeps it without per-tool changes.
1014
- stdout: spooled ? stdout + spoolNote(spooled) : stdout,
1015
- stderr,
1016
- exitCode,
1017
- truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
1018
- error,
1019
- spoolPath: spooled?.path,
1020
- spoolBytes: spooled?.bytes
1021
- };
1022
- } finally {
1023
- spool.finalize();
1024
- if (isWin) opts.signal.removeEventListener("abort", onAbort);
1025
- child.stdout?.off("data", onOut);
1026
- child.stderr?.off("data", onErr);
1027
- child.stdout?.destroy();
1028
- child.stderr?.destroy();
1029
- if (child.exitCode === null && !child.killed) {
1030
- if (typeof pid === "number") {
1031
- registry.kill(pid, { force: true });
1032
- } else {
1033
- try {
1034
- child.kill("SIGKILL");
1035
- } catch {
1036
- }
1037
- }
1038
- }
1039
- }
1040
- }
1041
- function utf8Prefix(text, maxBytes) {
1042
- if (maxBytes <= 0) return "";
1043
- if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
1044
- let low = 0;
1045
- let high = text.length;
1046
- while (low < high) {
1047
- const mid = Math.ceil((low + high) / 2);
1048
- if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
1049
- else high = mid - 1;
1050
- }
1051
- let end = low;
1052
- if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
1053
- return text.slice(0, end);
1054
- }
1055
-
1056
- // src/_util.ts
1057
- import * as fsp2 from "node:fs/promises";
1058
- import * as path3 from "node:path";
1059
- import * as Core from "@wrongstack/core/utils";
1060
- async function detectPackageManager(cwd, stopAt) {
1061
- let dir = path3.resolve(cwd);
1062
- const stop = stopAt ? path3.resolve(stopAt) : dir;
1063
- for (; ; ) {
1064
- const found = await detectPackageManagerInDir(dir);
1065
- if (found) return found;
1066
- if (dir === stop) break;
1067
- const parent = path3.dirname(dir);
1068
- const relParent = path3.relative(stop, parent);
1069
- if (parent === dir || relParent.startsWith("..") || path3.isAbsolute(relParent)) break;
1070
- dir = parent;
1071
- }
1072
- return "npm";
1073
- }
1074
- async function detectPackageManagerInDir(dir) {
1075
- const fs6 = await import("node:fs/promises");
1076
- try {
1077
- const raw = await fs6.readFile(path3.join(dir, "package.json"), "utf8");
1078
- const declared = JSON.parse(raw).packageManager;
1079
- if (typeof declared === "string") {
1080
- const name = declared.split("@")[0] ?? "";
1081
- if (name === "pnpm" || name === "yarn") return name;
1082
- if (name === "npm" || name === "bun") return "npm";
1083
- }
1084
- } catch {
1085
- }
1086
- const lockfiles = [
1087
- ["pnpm-lock.yaml", "pnpm"],
1088
- ["yarn.lock", "yarn"],
1089
- ["bun.lockb", "npm"],
1090
- ["bun.lock", "npm"],
1091
- ["package-lock.json", "npm"],
1092
- ["npm-shrinkwrap.json", "npm"]
1093
- ];
1094
- for (const [file, manager] of lockfiles) {
1095
- try {
1096
- await fs6.stat(`${dir}/${file}`);
1097
- return manager;
1098
- } catch {
1099
- }
1100
- }
1101
- return null;
1102
- }
1103
- function resolvePath(input, ctx) {
1104
- return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
1105
- }
1106
- function allowedRoots(ctx) {
1107
- return [path3.resolve(ctx.projectRoot), path3.resolve(Core.wstackGlobalRoot())];
1108
- }
1109
- function isInsideAny(target, roots) {
1110
- return roots.some((root) => {
1111
- const rel = path3.relative(root, target);
1112
- return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
1113
- });
1114
- }
1115
- function ensureInsideRoot(absPath, ctx) {
1116
- const target = path3.resolve(absPath);
1117
- if (ctx.allowOutsideProjectRoot) return target;
1118
- if (isInsideAny(target, allowedRoots(ctx))) return target;
1119
- throw new Error(`Path "${absPath}" is outside project root "${path3.resolve(ctx.projectRoot)}"`);
1120
- }
1121
- function safeResolve(input, ctx) {
1122
- return ensureInsideRoot(resolvePath(input, ctx), ctx);
1123
- }
1124
- async function resolveRealInsideRoot(absPath, ctx) {
1125
- if (ctx.allowOutsideProjectRoot) return absPath;
1126
- const realRoots = await Promise.all(
1127
- allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
1128
- );
1129
- let probe = absPath;
1130
- const pendingTail = [];
1131
- for (; ; ) {
1132
- let real;
1133
- try {
1134
- real = await fsp2.realpath(probe);
1135
- } catch (err) {
1136
- if (err.code === "ENOENT") {
1137
- const parent = path3.dirname(probe);
1138
- if (parent === probe) return absPath;
1139
- pendingTail.unshift(path3.basename(probe));
1140
- probe = parent;
1141
- continue;
1142
- }
1143
- throw err;
1144
- }
1145
- const candidate = pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
1146
- if (isInsideAny(candidate, realRoots)) {
1147
- return candidate;
1148
- }
1149
- throw new Error(
1150
- `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
1151
- );
1152
- }
1153
- }
1154
- async function safeResolveReal(input, ctx) {
1155
- const abs = safeResolve(input, ctx);
1156
- return await resolveRealInsideRoot(abs, ctx);
1157
- }
1158
- var COMMAND_OUTPUT_MAX_BYTES = 32768;
1159
- var REPEAT_RUN_THRESHOLD = 3;
1160
- function collapseCarriageReturns(text) {
1161
- const lf = text.replace(/\r\n/g, "\n");
1162
- if (!lf.includes("\r")) return lf;
1163
- return lf.split("\n").map((line) => line.includes("\r") ? line.slice(line.lastIndexOf("\r") + 1) : line).join("\n");
1164
- }
1165
- function collapseConsecutiveDuplicates(text, minRun = REPEAT_RUN_THRESHOLD) {
1166
- const lines = text.split("\n");
1167
- const out = [];
1168
- let i = 0;
1169
- while (i < lines.length) {
1170
- let j = i + 1;
1171
- while (j < lines.length && lines[j] === lines[i]) j++;
1172
- const run = j - i;
1173
- if (run >= minRun) {
1174
- out.push(lines[i], `\u2026 \u27E8repeated ${run}\xD7\u27E9`);
1175
- } else {
1176
- for (let k = i; k < j; k++) out.push(lines[k]);
1177
- }
1178
- i = j;
1179
- }
1180
- return out.join("\n");
1181
- }
1182
- function takeHeadBytes(s, maxBytes) {
1183
- if (maxBytes <= 0) return "";
1184
- if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
1185
- let lo = 0;
1186
- let hi = s.length;
1187
- while (lo < hi) {
1188
- const mid = Math.ceil((lo + hi) / 2);
1189
- if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes) lo = mid;
1190
- else hi = mid - 1;
1191
- }
1192
- return s.slice(0, lo);
1193
- }
1194
- function takeTailBytes(s, maxBytes) {
1195
- if (maxBytes <= 0) return "";
1196
- if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
1197
- let lo = 0;
1198
- let hi = s.length;
1199
- while (lo < hi) {
1200
- const mid = Math.ceil((lo + hi) / 2);
1201
- if (Buffer.byteLength(s.slice(s.length - mid), "utf8") <= maxBytes) lo = mid;
1202
- else hi = mid - 1;
1203
- }
1204
- return s.slice(s.length - lo);
1205
- }
1206
- function truncateHeadTail(s, maxBytes) {
1207
- const total = Buffer.byteLength(s, "utf8");
1208
- if (total <= maxBytes) return s;
1209
- const MARKER_RESERVE = 64;
1210
- const avail = Math.max(0, maxBytes - MARKER_RESERVE);
1211
- const headBudget = Math.floor(avail * 0.45);
1212
- const head = takeHeadBytes(s, headBudget);
1213
- const tail = takeTailBytes(s, avail - Buffer.byteLength(head, "utf8"));
1214
- const kept = Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
1215
- return `${head}
1216
- \u2026[truncated ${total - kept} bytes]\u2026
1217
- ${tail}`;
1218
- }
1219
- function normalizeCommandOutput(raw, opts = {}) {
1220
- if (!raw) return raw;
1221
- let text = Core.stripAnsi(raw);
1222
- text = collapseCarriageReturns(text);
1223
- text = text.replace(/[ \t]+$/gm, "");
1224
- text = collapseConsecutiveDuplicates(text);
1225
- text = text.replace(/\n{3,}/g, "\n\n");
1226
- return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
1227
- }
1228
-
1229
- // src/languages/legacy-bridge.ts
1230
- import * as fs5 from "node:fs/promises";
1231
- import * as path8 from "node:path";
1232
-
1233
- // src/languages/detect.ts
1234
- import { createHash } from "node:crypto";
1235
- import * as fs2 from "node:fs/promises";
1236
- import * as path4 from "node:path";
1237
-
1238
- // src/languages/profile-helpers.ts
1239
- var DEFAULT_TIMEOUT_MS = 12e4;
1240
- var DEFAULT_OUTPUT_LIMIT_BYTES = 2e5;
1241
- function processPlan(ctx, operation, command, args, options) {
1242
- return {
1243
- profileId: ctx.workspace.language,
1244
- workspaceId: ctx.workspace.id,
1245
- operation,
1246
- kind: "process",
1247
- command,
1248
- args: Object.freeze([...args]),
1249
- cwd: ctx.workspace.root,
1250
- env: Object.freeze({}),
1251
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
1252
- outputLimitBytes: DEFAULT_OUTPUT_LIMIT_BYTES,
1253
- mutating: options.mutating ?? false,
1254
- network: options.network ?? false,
1255
- executesProjectCode: options.executesProjectCode ?? false,
1256
- reason: options.reason,
1257
- evidence: ctx.workspace.evidence,
1258
- parser: options.parser
1259
- };
1260
- }
1261
- function internalPlan(ctx, operation, parser, reason) {
1262
- return {
1263
- profileId: ctx.workspace.language,
1264
- workspaceId: ctx.workspace.id,
1265
- operation,
1266
- kind: "internal",
1267
- command: null,
1268
- args: Object.freeze([]),
1269
- cwd: ctx.workspace.root,
1270
- env: Object.freeze({}),
1271
- timeoutMs: 5e3,
1272
- outputLimitBytes: 32768,
1273
- mutating: false,
1274
- network: false,
1275
- executesProjectCode: false,
1276
- reason,
1277
- evidence: ctx.workspace.evidence,
1278
- parser
1279
- };
1280
- }
1281
- function unavailable(ctx, operation, reason) {
1282
- return {
1283
- status: "unavailable",
1284
- profileId: ctx.workspace.language,
1285
- workspaceId: ctx.workspace.id,
1286
- operation,
1287
- reason
1288
- };
1289
- }
1290
- function packageNames(ctx) {
1291
- return ctx.options.packages ?? [];
1292
- }
1293
-
1294
- // src/languages/profiles/additional.ts
1295
- var IGNORES = Object.freeze([
1296
- ".git",
1297
- ".wrongstack",
1298
- "node_modules",
1299
- "vendor",
1300
- "dist",
1301
- "build",
1302
- "coverage"
1303
- ]);
1304
- function pythonProfile() {
1305
- return {
1306
- id: "python",
1307
- displayName: "Python",
1308
- extensions: Object.freeze([".py", ".pyi"]),
1309
- lspLanguageIds: Object.freeze(["python"]),
1310
- detectors: Object.freeze([
1311
- { kind: "config", filename: "pyproject.toml", weight: 90 },
1312
- { kind: "config", filename: "setup.py", weight: 70 },
1313
- { kind: "config", filename: "setup.cfg", weight: 60 },
1314
- { kind: "manifest", filename: "requirements.txt", weight: 55 },
1315
- { kind: "manifest", filename: "Pipfile", weight: 50 },
1316
- { kind: "lockfile", filename: "poetry.lock", weight: 30 },
1317
- { kind: "lockfile", filename: "Pipfile.lock", weight: 30 },
1318
- { kind: "lockfile", filename: "uv.lock", weight: 30 }
1319
- ]),
1320
- ignoredDirectories: IGNORES,
1321
- packageManagers: Object.freeze(["pip", "poetry", "pipenv", "uv"]),
1322
- executables: Object.freeze([
1323
- "python",
1324
- "python3",
1325
- "pip",
1326
- "pip3",
1327
- "poetry",
1328
- "pipenv",
1329
- "uv",
1330
- "ruff",
1331
- "black",
1332
- "pytest",
1333
- "mypy",
1334
- "pyright"
1335
- ]),
1336
- operations: Object.freeze({
1337
- syntax: async (ctx) => {
1338
- const py = ctx.options.target ? "python3" : "python3";
1339
- return ctx.options.target ? processPlan(ctx, "syntax", py, ["-m", "py_compile", ctx.options.target], {
1340
- parser: "python",
1341
- reason: "Compile the target file to check for syntax errors."
1342
- }) : unavailable(ctx, "syntax", "Python syntax check requires an explicit target file.");
1343
- },
1344
- semantic: async (ctx) => processPlan(ctx, "semantic", "mypy", [".", "--no-error-summary"], {
1345
- parser: "mypy",
1346
- reason: "Run mypy type checking on the workspace.",
1347
- executesProjectCode: true
1348
- }),
1349
- lint: async (ctx) => processPlan(ctx, "lint", "ruff", ["check", "."], {
1350
- parser: "ruff",
1351
- reason: "Run the Ruff linter on the workspace."
1352
- }),
1353
- "format-check": async (ctx) => processPlan(ctx, "format-check", "ruff", ["format", "--check", "."], {
1354
- parser: "ruff",
1355
- reason: "Check Python formatting without writing files."
1356
- }),
1357
- "format-write": async (ctx) => processPlan(ctx, "format-write", "ruff", ["format", "."], {
1358
- parser: "ruff",
1359
- reason: "Format Python source files.",
1360
- mutating: true
1361
- }),
1362
- test: async (ctx) => processPlan(
1363
- ctx,
1364
- "test",
1365
- "pytest",
1366
- [...ctx.options.filter ? ["-k", ctx.options.filter] : [], "."],
1367
- {
1368
- parser: "pytest",
1369
- reason: ctx.options.filter ? "Run filtered Python tests." : "Run all Python tests.",
1370
- mutating: true,
1371
- executesProjectCode: true
1372
- }
1373
- ),
1374
- build: async (ctx) => unavailable(ctx, "build", "Python is interpreted; use semantic or test instead."),
1375
- "debug-compile": async (ctx) => processPlan(ctx, "debug-compile", "mypy", [".", "--no-error-summary"], {
1376
- parser: "mypy",
1377
- reason: "Collect mypy type diagnostics.",
1378
- executesProjectCode: true
1379
- }),
1380
- "package-install": async (ctx) => processPlan(ctx, "package-install", "pip", ["install", "--no-cache-dir"], {
1381
- parser: "package-text",
1382
- reason: "Install dependencies from requirements.",
1383
- mutating: true,
1384
- network: true
1385
- }),
1386
- "package-add": async (ctx) => {
1387
- const names = packageNames(ctx);
1388
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one package name is required.") : processPlan(ctx, "package-add", "pip", ["install", ...names], {
1389
- parser: "package-text",
1390
- reason: "Install specified Python packages.",
1391
- mutating: true,
1392
- network: true
1393
- });
1394
- },
1395
- "package-remove": async (ctx) => {
1396
- const names = packageNames(ctx);
1397
- return names.length === 0 ? unavailable(ctx, "package-remove", "At least one package name is required.") : processPlan(ctx, "package-remove", "pip", ["uninstall", "--yes", ...names], {
1398
- parser: "package-text",
1399
- reason: "Remove validated Python packages.",
1400
- mutating: true
1401
- });
1402
- },
1403
- "package-audit": async (ctx) => processPlan(ctx, "package-audit", "pip", ["audit"], {
1404
- parser: "pip-audit",
1405
- reason: "Audit Python dependencies for vulnerabilities.",
1406
- network: true
1407
- }),
1408
- run: async (ctx) => {
1409
- const entries = ["main.py", "app.py", "__main__.py", "manage.py"];
1410
- const first = (await Promise.all(
1411
- entries.map((name) => ctx.pathExists(name).then((ok) => ok ? name : void 0))
1412
- )).find(Boolean);
1413
- const args = first ? [first] : [];
1414
- return processPlan(ctx, "run", "python3", args, {
1415
- parser: "command-text",
1416
- reason: first ? `Run the Python entry point ${first}.` : "Run the Python project (no common entry point detected \u2014 add the module path manually).",
1417
- executesProjectCode: true,
1418
- mutating: true
1419
- });
1420
- }
1421
- })
1422
- };
1423
- }
1424
- function hasGradleEvidence(ctx) {
1425
- return ctx.workspace.evidence.some(
1426
- (e) => e.kind === "manifest" && (e.value === "build.gradle" || e.value === "build.gradle.kts" || e.value === "settings.gradle") || e.kind === "lockfile" && e.value === "gradle.lockfile"
1427
- );
1428
- }
1429
- async function gradleRunner(ctx) {
1430
- if (await ctx.pathExists("gradlew")) return "gradlew";
1431
- return "gradle";
1432
- }
1433
- function javaProfile() {
1434
- return {
1435
- id: "java",
1436
- displayName: "Java / Kotlin",
1437
- extensions: Object.freeze([".java", ".kt", ".kts", ".scala"]),
1438
- lspLanguageIds: Object.freeze(["java", "kotlin"]),
1439
- detectors: Object.freeze([
1440
- { kind: "manifest", filename: "pom.xml", weight: 90 },
1441
- { kind: "manifest", filename: "build.gradle", weight: 85 },
1442
- { kind: "manifest", filename: "build.gradle.kts", weight: 85 },
1443
- { kind: "manifest", filename: "settings.gradle", weight: 50 },
1444
- { kind: "lockfile", filename: "gradle.lockfile", weight: 30 }
1445
- ]),
1446
- ignoredDirectories: IGNORES,
1447
- packageManagers: Object.freeze(["maven", "gradle"]),
1448
- executables: Object.freeze(["mvn", "gradle", "gradlew", "java", "kotlinc"]),
1449
- operations: Object.freeze({
1450
- semantic: async (ctx) => {
1451
- const isGradle = hasGradleEvidence(ctx);
1452
- const runner = isGradle ? await gradleRunner(ctx) : "mvn";
1453
- const args = isGradle ? ["compileJava"] : ["compile", "-q"];
1454
- const reason = isGradle ? "Compile the Gradle project." : "Compile the Maven project.";
1455
- return processPlan(ctx, "semantic", runner, args, {
1456
- parser: isGradle ? "gradle" : "maven",
1457
- reason,
1458
- mutating: true,
1459
- executesProjectCode: true
1460
- });
1461
- },
1462
- lint: async (ctx) => unavailable(
1463
- ctx,
1464
- "lint",
1465
- "Java linting requires a configured checkstyle or spotbugs plugin."
1466
- ),
1467
- "format-check": async (ctx) => unavailable(
1468
- ctx,
1469
- "format-check",
1470
- "Java formatting check requires a configured spotless or google-java-format plugin."
1471
- ),
1472
- test: async (ctx) => {
1473
- const isGradle = hasGradleEvidence(ctx);
1474
- const runner = isGradle ? await gradleRunner(ctx) : "mvn";
1475
- const args = isGradle ? ["test"] : ["test", "-q"];
1476
- const reason = isGradle ? "Run Gradle tests." : "Run Maven tests.";
1477
- return processPlan(ctx, "test", runner, args, {
1478
- parser: isGradle ? "gradle" : "maven",
1479
- reason,
1480
- mutating: true,
1481
- executesProjectCode: true
1482
- });
1483
- },
1484
- build: async (ctx) => {
1485
- const isGradle = hasGradleEvidence(ctx);
1486
- const runner = isGradle ? await gradleRunner(ctx) : "mvn";
1487
- const args = isGradle ? ["build"] : ["package", "-q", "-DskipTests"];
1488
- const reason = isGradle ? "Build the Gradle project." : "Build the Maven project, skipping tests.";
1489
- return processPlan(ctx, "build", runner, args, {
1490
- parser: isGradle ? "gradle" : "maven",
1491
- reason,
1492
- mutating: true,
1493
- executesProjectCode: true
1494
- });
1495
- },
1496
- run: async (ctx) => {
1497
- if (!hasGradleEvidence(ctx))
1498
- return unavailable(
1499
- ctx,
1500
- "run",
1501
- "Maven run requires exec-maven-plugin configuration. Use `mvn exec:java -q` manually."
1502
- );
1503
- const runner = await gradleRunner(ctx);
1504
- return processPlan(ctx, "run", runner, ["run"], {
1505
- parser: "command-text",
1506
- reason: `Run the Gradle project entry point via ${runner}.`,
1507
- executesProjectCode: true,
1508
- mutating: true
1509
- });
1510
- },
1511
- "package-install": async (ctx) => unavailable(
1512
- ctx,
1513
- "package-install",
1514
- "JVM dependency installation happens through build or dependency:get."
1515
- ),
1516
- "package-add": async (ctx) => {
1517
- const names = packageNames(ctx);
1518
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one dependency coordinate is required.") : processPlan(ctx, "package-add", "mvn", ["dependency:get", `-Dartifact=${names[0]}`], {
1519
- parser: "maven",
1520
- reason: "Resolve and fetch a Maven dependency.",
1521
- mutating: true,
1522
- network: true
1523
- });
1524
- }
1525
- })
1526
- };
1527
- }
1528
- function rubyProfile() {
1529
- return {
1530
- id: "ruby",
1531
- displayName: "Ruby",
1532
- extensions: Object.freeze([".rb"]),
1533
- lspLanguageIds: Object.freeze(["ruby"]),
1534
- detectors: Object.freeze([
1535
- { kind: "manifest", filename: "Gemfile", weight: 90 },
1536
- { kind: "config", filename: ".ruby-version", weight: 40 },
1537
- { kind: "lockfile", filename: "Gemfile.lock", weight: 30 }
1538
- ]),
1539
- ignoredDirectories: IGNORES,
1540
- packageManagers: Object.freeze(["gem", "bundler"]),
1541
- executables: Object.freeze(["ruby", "gem", "bundle", "rubocop", "rspec"]),
1542
- operations: Object.freeze({
1543
- syntax: async (ctx) => ctx.options.target ? processPlan(ctx, "syntax", "ruby", ["-c", ctx.options.target], {
1544
- parser: "ruby",
1545
- reason: "Check the target Ruby file for syntax errors."
1546
- }) : unavailable(ctx, "syntax", "Ruby syntax check requires an explicit target file."),
1547
- lint: async (ctx) => processPlan(ctx, "lint", "rubocop", ["--format=json"], {
1548
- parser: "rubocop",
1549
- reason: "Run RuboCop linter.",
1550
- executesProjectCode: true
1551
- }),
1552
- "format-write": async (ctx) => processPlan(ctx, "format-write", "rubocop", ["--auto-correct"], {
1553
- parser: "rubocop",
1554
- reason: "Auto-correct RuboCop violations.",
1555
- mutating: true,
1556
- executesProjectCode: true
1557
- }),
1558
- test: async (ctx) => processPlan(ctx, "test", "rspec", [], {
1559
- parser: "rspec",
1560
- reason: "Run the RSpec test suite.",
1561
- mutating: true,
1562
- executesProjectCode: true
1563
- }),
1564
- "package-install": async (ctx) => processPlan(ctx, "package-install", "bundle", ["install"], {
1565
- parser: "package-text",
1566
- reason: "Install Ruby gem dependencies.",
1567
- mutating: true,
1568
- network: true,
1569
- executesProjectCode: true
1570
- }),
1571
- "package-add": async (ctx) => {
1572
- const names = packageNames(ctx);
1573
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one gem name is required.") : processPlan(ctx, "package-add", "gem", ["install", ...names], {
1574
- parser: "package-text",
1575
- reason: "Install specified Ruby gems.",
1576
- mutating: true,
1577
- network: true
1578
- });
1579
- },
1580
- "package-audit": async (ctx) => processPlan(ctx, "package-audit", "bundle", ["audit", "--format=json"], {
1581
- parser: "bundler-audit",
1582
- reason: "Audit Ruby gems for vulnerabilities.",
1583
- network: true
1584
- }),
1585
- run: async (ctx) => {
1586
- const entries = ["main.rb", "app.rb", "server.rb", "config.ru"];
1587
- const first = (await Promise.all(
1588
- entries.map((name) => ctx.pathExists(name).then((ok) => ok ? name : void 0))
1589
- )).find(Boolean);
1590
- const hasGemfile = await ctx.pathExists("Gemfile");
1591
- const cmd = hasGemfile ? "bundle" : "ruby";
1592
- const args = hasGemfile ? ["exec", "ruby", first ?? ""] : [first ?? ""];
1593
- return processPlan(ctx, "run", cmd, args, {
1594
- parser: "command-text",
1595
- reason: first ? `Run the Ruby entry point ${first}.` : "Run the Ruby project (no common entry point detected \u2014 add the file path manually).",
1596
- executesProjectCode: true,
1597
- mutating: true
1598
- });
1599
- }
1600
- })
1601
- };
1602
- }
1603
- function cProfile() {
1604
- return {
1605
- id: "c",
1606
- displayName: "C / C++",
1607
- extensions: Object.freeze([".c", ".h"]),
1608
- lspLanguageIds: Object.freeze(["c"]),
1609
- detectors: Object.freeze([
1610
- { kind: "manifest", filename: "CMakeLists.txt", weight: 85 },
1611
- { kind: "manifest", filename: "Makefile", weight: 60 },
1612
- { kind: "config", suffix: ".cmake", weight: 50 }
1613
- ]),
1614
- ignoredDirectories: IGNORES,
1615
- packageManagers: Object.freeze([]),
1616
- executables: Object.freeze(["cc", "gcc", "clang", "cmake", "make"]),
1617
- operations: Object.freeze({
1618
- semantic: async (ctx) => processPlan(ctx, "semantic", "cmake", ["--build", ".", "--target", "all"], {
1619
- parser: "cmake",
1620
- reason: "Build C project to collect compiler diagnostics.",
1621
- mutating: true,
1622
- executesProjectCode: true
1623
- }),
1624
- test: async (ctx) => unavailable(
1625
- ctx,
1626
- "test",
1627
- "C test execution requires a configured test runner (ctest, etc.)."
1628
- ),
1629
- build: async (ctx) => processPlan(ctx, "build", "cmake", ["--build", "."], {
1630
- parser: "cmake",
1631
- reason: "Build the C project.",
1632
- mutating: true,
1633
- executesProjectCode: true
1634
- })
1635
- })
1636
- };
1637
- }
1638
- function cppProfile() {
1639
- return {
1640
- ...cProfile(),
1641
- id: "cpp",
1642
- displayName: "C++",
1643
- extensions: Object.freeze([".cpp", ".cc", ".cxx", ".hpp", ".hxx"]),
1644
- lspLanguageIds: Object.freeze(["cpp"]),
1645
- detectors: Object.freeze([
1646
- { kind: "manifest", filename: "CMakeLists.txt", weight: 85 },
1647
- { kind: "manifest", filename: "Makefile", weight: 50 }
1648
- ]),
1649
- executables: Object.freeze(["c++", "g++", "clang++", "cmake", "make"])
1650
- };
1651
- }
1652
- function swiftProfile() {
1653
- return {
1654
- id: "swift",
1655
- displayName: "Swift",
1656
- extensions: Object.freeze([".swift"]),
1657
- lspLanguageIds: Object.freeze(["swift"]),
1658
- detectors: Object.freeze([{ kind: "manifest", filename: "Package.swift", weight: 90 }]),
1659
- ignoredDirectories: IGNORES,
1660
- packageManagers: Object.freeze(["swift"]),
1661
- executables: Object.freeze(["swift", "swiftc"]),
1662
- operations: Object.freeze({
1663
- semantic: async (ctx) => processPlan(ctx, "semantic", "swift", ["build"], {
1664
- parser: "swift",
1665
- reason: "Build Swift package to collect compiler diagnostics.",
1666
- mutating: true,
1667
- executesProjectCode: true
1668
- }),
1669
- test: async (ctx) => processPlan(ctx, "test", "swift", ["test"], {
1670
- parser: "swift-test",
1671
- reason: "Run Swift tests.",
1672
- mutating: true,
1673
- executesProjectCode: true
1674
- }),
1675
- build: async (ctx) => processPlan(ctx, "build", "swift", ["build", "-c", "release"], {
1676
- parser: "swift",
1677
- reason: "Build the Swift package in release mode.",
1678
- mutating: true,
1679
- executesProjectCode: true
1680
- }),
1681
- run: async (ctx) => processPlan(ctx, "run", "swift", ["run"], {
1682
- parser: "command-text",
1683
- reason: "Run the Swift package entry point.",
1684
- executesProjectCode: true
1685
- })
1686
- })
1687
- };
1688
- }
1689
- function dartProfile() {
1690
- return {
1691
- id: "dart",
1692
- displayName: "Dart / Flutter",
1693
- extensions: Object.freeze([".dart"]),
1694
- lspLanguageIds: Object.freeze(["dart"]),
1695
- detectors: Object.freeze([
1696
- { kind: "manifest", filename: "pubspec.yaml", weight: 90 },
1697
- { kind: "lockfile", filename: "pubspec.lock", weight: 30 }
1698
- ]),
1699
- ignoredDirectories: IGNORES,
1700
- packageManagers: Object.freeze(["pub"]),
1701
- executables: Object.freeze(["dart", "flutter"]),
1702
- operations: Object.freeze({
1703
- semantic: async (ctx) => processPlan(ctx, "semantic", "dart", ["analyze"], {
1704
- parser: "dart-analyze",
1705
- reason: "Run Dart static analysis."
1706
- }),
1707
- "format-check": async (ctx) => processPlan(
1708
- ctx,
1709
- "format-check",
1710
- "dart",
1711
- ["format", "--output=none", "--set-exit-if-changed", "."],
1712
- {
1713
- parser: "dart-format",
1714
- reason: "Check Dart formatting without writing files."
1715
- }
1716
- ),
1717
- "format-write": async (ctx) => processPlan(ctx, "format-write", "dart", ["format", "."], {
1718
- parser: "dart-format",
1719
- reason: "Format Dart source files.",
1720
- mutating: true
1721
- }),
1722
- test: async (ctx) => processPlan(ctx, "test", "dart", ["test"], {
1723
- parser: "dart-test",
1724
- reason: "Run Dart tests.",
1725
- mutating: true,
1726
- executesProjectCode: true
1727
- }),
1728
- "package-install": async (ctx) => processPlan(ctx, "package-install", "dart", ["pub", "get"], {
1729
- parser: "package-text",
1730
- reason: "Fetch Dart package dependencies.",
1731
- mutating: true,
1732
- network: true
1733
- }),
1734
- "package-outdated": async (ctx) => processPlan(ctx, "package-outdated", "dart", ["pub", "outdated"], {
1735
- parser: "dart-outdated",
1736
- reason: "Check for outdated Dart packages.",
1737
- network: true
1738
- }),
1739
- run: async (ctx) => processPlan(ctx, "run", "dart", ["run"], {
1740
- parser: "command-text",
1741
- reason: "Run the Dart / Flutter project entry point.",
1742
- executesProjectCode: true
1743
- })
1744
- })
1745
- };
1746
- }
1747
- function denoProfile() {
1748
- return {
1749
- id: "deno",
1750
- displayName: "Deno",
1751
- extensions: Object.freeze([".ts", ".tsx", ".js", ".jsx"]),
1752
- lspLanguageIds: Object.freeze(["typescript", "javascript"]),
1753
- detectors: Object.freeze([
1754
- { kind: "config", filename: "deno.json", weight: 90 },
1755
- { kind: "config", filename: "deno.jsonc", weight: 90 },
1756
- { kind: "config", filename: "import_map.json", weight: 60 }
1757
- ]),
1758
- ignoredDirectories: IGNORES,
1759
- packageManagers: Object.freeze([]),
1760
- executables: Object.freeze(["deno"]),
1761
- // .ts/.js extensions are shared with the TypeScript/JavaScript profiles;
1762
- // only a deno.json(c)/import_map.json detector hit may establish a workspace.
1763
- sourceFallback: false,
1764
- operations: Object.freeze({
1765
- test: async (ctx) => processPlan(ctx, "test", "deno", ["test"], {
1766
- parser: "deno-test",
1767
- reason: "Run Deno tests.",
1768
- mutating: true,
1769
- executesProjectCode: true
1770
- }),
1771
- run: async (ctx) => processPlan(ctx, "run", "deno", ["run", "--allow-all", "main.ts"], {
1772
- parser: "command-text",
1773
- reason: "Run the Deno project entry point with full permission.",
1774
- executesProjectCode: true,
1775
- mutating: true
1776
- })
1777
- })
1778
- };
1779
- }
1780
- function elixirProfile() {
1781
- return {
1782
- id: "elixir",
1783
- displayName: "Elixir",
1784
- extensions: Object.freeze([".ex", ".exs"]),
1785
- lspLanguageIds: Object.freeze(["elixir"]),
1786
- detectors: Object.freeze([
1787
- { kind: "manifest", filename: "mix.exs", weight: 90 },
1788
- { kind: "lockfile", filename: "mix.lock", weight: 30 }
1789
- ]),
1790
- ignoredDirectories: IGNORES,
1791
- packageManagers: Object.freeze(["mix"]),
1792
- executables: Object.freeze(["mix", "elixir"]),
1793
- operations: Object.freeze({
1794
- semantic: async (ctx) => unavailable(ctx, "semantic", "Elixir compilation is handled by the build operation."),
1795
- lint: async (ctx) => unavailable(ctx, "lint", "Elixir linting requires the credo package."),
1796
- test: async (ctx) => processPlan(ctx, "test", "mix", ["test"], {
1797
- parser: "mix-test",
1798
- reason: "Run ExUnit tests.",
1799
- mutating: true,
1800
- executesProjectCode: true
1801
- }),
1802
- build: async (ctx) => processPlan(ctx, "build", "mix", ["compile"], {
1803
- parser: "mix",
1804
- reason: "Compile the Mix project.",
1805
- mutating: true,
1806
- executesProjectCode: true
1807
- }),
1808
- run: async (ctx) => processPlan(ctx, "run", "mix", ["run"], {
1809
- parser: "command-text",
1810
- reason: "Run the Mix project entry point.",
1811
- executesProjectCode: true
1812
- }),
1813
- "package-install": async (ctx) => processPlan(ctx, "package-install", "mix", ["deps.get"], {
1814
- parser: "package-text",
1815
- reason: "Fetch Elixir dependencies.",
1816
- mutating: true,
1817
- network: true
1818
- })
1819
- })
1820
- };
1821
- }
1822
- function shellProfile() {
1823
- return {
1824
- id: "shell",
1825
- displayName: "Shell",
1826
- extensions: Object.freeze([".sh", ".bash"]),
1827
- lspLanguageIds: Object.freeze(["shellscript"]),
1828
- detectors: Object.freeze([{ kind: "config", filename: "ShellCheckrc", weight: 50 }]),
1829
- ignoredDirectories: IGNORES,
1830
- packageManagers: Object.freeze([]),
1831
- executables: Object.freeze(["bash", "sh", "shellcheck", "shfmt"]),
1832
- operations: Object.freeze({
1833
- syntax: async (ctx) => ctx.options.target ? processPlan(ctx, "syntax", "bash", ["-n", ctx.options.target], {
1834
- parser: "shell",
1835
- reason: "Check the shell script for syntax errors."
1836
- }) : unavailable(ctx, "syntax", "Shell syntax check requires an explicit target file."),
1837
- lint: async (ctx) => ctx.options.target ? processPlan(ctx, "lint", "shellcheck", ["--format=json", ctx.options.target], {
1838
- parser: "shellcheck",
1839
- reason: "Run ShellCheck on the target script."
1840
- }) : unavailable(ctx, "lint", "Shell linting requires an explicit target file."),
1841
- "format-check": async (ctx) => ctx.options.target ? processPlan(ctx, "format-check", "shfmt", ["-d", ctx.options.target], {
1842
- parser: "shell",
1843
- reason: "Check shell formatting without writing."
1844
- }) : unavailable(
1845
- ctx,
1846
- "format-check",
1847
- "Shell formatting check requires an explicit target file."
1848
- ),
1849
- "format-write": async (ctx) => ctx.options.target ? processPlan(ctx, "format-write", "shfmt", ["-w", ctx.options.target], {
1850
- parser: "shell",
1851
- reason: "Format the shell script.",
1852
- mutating: true
1853
- }) : unavailable(ctx, "format-write", "Shell formatting requires an explicit target file.")
1854
- })
1855
- };
1856
- }
1857
- var ADDITIONAL_LANGUAGE_PROFILES = Object.freeze([
1858
- pythonProfile(),
1859
- javaProfile(),
1860
- rubyProfile(),
1861
- cProfile(),
1862
- cppProfile(),
1863
- swiftProfile(),
1864
- dartProfile(),
1865
- denoProfile(),
1866
- elixirProfile(),
1867
- shellProfile()
1868
- ]);
1869
-
1870
- // src/languages/profiles/primary.ts
1871
- var COMMON_IGNORES = Object.freeze([
1872
- ".git",
1873
- ".wrongstack",
1874
- "node_modules",
1875
- "vendor",
1876
- "target",
1877
- "bin",
1878
- "obj",
1879
- "dist",
1880
- "build",
1881
- "coverage"
1882
- ]);
1883
- function nodeManager(ctx) {
1884
- const lockfiles = new Set(
1885
- ctx.workspace.evidence.filter((evidence) => evidence.kind === "lockfile").map((evidence) => evidence.value.toLowerCase())
1886
- );
1887
- if (lockfiles.size > 1 && !ctx.workspace.packageManager) return void 0;
1888
- return ctx.workspace.packageManager ?? "npm";
1889
- }
1890
- function scriptPlan(ctx, operation, script) {
1891
- const manager = nodeManager(ctx);
1892
- if (!manager)
1893
- return unavailable(
1894
- ctx,
1895
- operation,
1896
- "Conflicting Node lockfiles make the package manager ambiguous."
1897
- );
1898
- const args = manager === "npm" ? ["run", script] : [script];
1899
- return processPlan(ctx, operation, manager, args, {
1900
- parser: "command-text",
1901
- reason: `Run the detected ${manager} ${script} script for this workspace.`,
1902
- mutating: true,
1903
- executesProjectCode: true
1904
- });
1905
- }
1906
- function nodeExec(ctx, executable, args) {
1907
- const manager = ctx.workspace.packageManager ?? "npm";
1908
- if (manager === "pnpm") return { command: "pnpm", args: ["exec", executable, ...args] };
1909
- if (manager === "yarn") return { command: "yarn", args: ["exec", executable, ...args] };
1910
- if (manager === "bun") return { command: "bun", args: ["x", executable, ...args] };
1911
- return { command: "npx", args: ["--no-install", executable, ...args] };
1912
- }
1913
- function typescriptProfile() {
1914
- return {
1915
- id: "typescript",
1916
- displayName: "TypeScript",
1917
- extensions: Object.freeze([".ts", ".tsx", ".mts", ".cts"]),
1918
- lspLanguageIds: Object.freeze(["typescript", "typescriptreact"]),
1919
- detectors: Object.freeze([
1920
- { kind: "config", filename: "tsconfig.json", weight: 90 },
1921
- { kind: "manifest", filename: "package.json", weight: 55 },
1922
- { kind: "lockfile", filename: "pnpm-lock.yaml", weight: 30 },
1923
- { kind: "lockfile", filename: "yarn.lock", weight: 30 },
1924
- { kind: "lockfile", filename: "package-lock.json", weight: 30 },
1925
- { kind: "lockfile", filename: "bun.lock", weight: 30 },
1926
- { kind: "lockfile", filename: "bun.lockb", weight: 30 }
1927
- ]),
1928
- ignoredDirectories: COMMON_IGNORES,
1929
- packageManagers: Object.freeze(["pnpm", "yarn", "bun", "npm"]),
1930
- executables: Object.freeze(["pnpm", "yarn", "bun", "npx", "npm"]),
1931
- operations: Object.freeze({
1932
- syntax: async (ctx) => internalPlan(
1933
- ctx,
1934
- "syntax",
1935
- "typescript-parser",
1936
- "Parse the target with the TypeScript compiler API."
1937
- ),
1938
- semantic: async (ctx) => {
1939
- const run = nodeExec(ctx, "tsc", ["--noEmit", "--pretty", "false"]);
1940
- return processPlan(ctx, "semantic", run.command, run.args, {
1941
- parser: "typescript",
1942
- reason: "Run the workspace TypeScript compiler without emitting files.",
1943
- executesProjectCode: true
1944
- });
1945
- },
1946
- lint: async (ctx) => {
1947
- const run = nodeExec(ctx, "biome", ["lint", "."]);
1948
- return processPlan(ctx, "lint", run.command, run.args, {
1949
- parser: "biome",
1950
- reason: "Run the project-local Biome linter.",
1951
- executesProjectCode: true
1952
- });
1953
- },
1954
- "format-check": async (ctx) => {
1955
- const run = nodeExec(ctx, "biome", ["format", "--check", "."]);
1956
- return processPlan(ctx, "format-check", run.command, run.args, {
1957
- parser: "biome",
1958
- reason: "Check formatting with the project-local Biome formatter.",
1959
- executesProjectCode: true
1960
- });
1961
- },
1962
- "format-write": async (ctx) => {
1963
- const run = nodeExec(ctx, "biome", ["format", "--write", "."]);
1964
- return processPlan(ctx, "format-write", run.command, run.args, {
1965
- parser: "biome",
1966
- reason: "Format the workspace with the project-local Biome formatter.",
1967
- mutating: true,
1968
- executesProjectCode: true
1969
- });
1970
- },
1971
- test: async (ctx) => ctx.options.filter || ctx.options.coverage ? unavailable(
1972
- ctx,
1973
- "test",
1974
- "The detected package script does not expose deterministic filter or coverage adapters."
1975
- ) : scriptPlan(ctx, "test", "test"),
1976
- build: async (ctx) => scriptPlan(ctx, "build", "build"),
1977
- run: async (ctx) => scriptPlan(ctx, "run", "dev"),
1978
- "debug-compile": async (ctx) => {
1979
- const run = nodeExec(ctx, "tsc", ["--noEmit", "--pretty", "false"]);
1980
- return processPlan(ctx, "debug-compile", run.command, run.args, {
1981
- parser: "typescript",
1982
- reason: "Collect deterministic TypeScript compiler diagnostics.",
1983
- executesProjectCode: true
1984
- });
1985
- },
1986
- "package-install": async (ctx) => {
1987
- const manager = ctx.workspace.packageManager ?? "npm";
1988
- const args = manager === "yarn" ? ["install", "--ignore-scripts"] : ["install", "--ignore-scripts"];
1989
- return processPlan(ctx, "package-install", manager, args, {
1990
- parser: "package-text",
1991
- reason: `Restore declared dependencies with ${manager} and lifecycle scripts disabled.`,
1992
- mutating: true,
1993
- network: true,
1994
- executesProjectCode: false
1995
- });
1996
- },
1997
- "package-add": async (ctx) => {
1998
- const names = packageNames(ctx);
1999
- if (names.length === 0)
2000
- return unavailable(ctx, "package-add", "At least one package name is required.");
2001
- const manager = ctx.workspace.packageManager ?? "npm";
2002
- const args = manager === "npm" ? ["install", "--ignore-scripts", ...names] : ["add", "--ignore-scripts", ...names];
2003
- return processPlan(ctx, "package-add", manager, args, {
2004
- parser: "package-text",
2005
- reason: `Add validated packages with ${manager} and lifecycle scripts disabled.`,
2006
- mutating: true,
2007
- network: true
2008
- });
2009
- },
2010
- "package-remove": async (ctx) => {
2011
- const names = packageNames(ctx);
2012
- if (names.length === 0)
2013
- return unavailable(ctx, "package-remove", "At least one package name is required.");
2014
- const manager = ctx.workspace.packageManager ?? "npm";
2015
- const args = manager === "npm" ? ["uninstall", "--ignore-scripts", ...names] : ["remove", "--ignore-scripts", ...names];
2016
- return processPlan(ctx, "package-remove", manager, args, {
2017
- parser: "package-text",
2018
- reason: `Remove validated packages with ${manager} and lifecycle scripts disabled.`,
2019
- mutating: true,
2020
- network: true
2021
- });
2022
- },
2023
- "package-audit": async (ctx) => {
2024
- const manager = ctx.workspace.packageManager ?? "npm";
2025
- return processPlan(ctx, "package-audit", manager, ["audit", "--json"], {
2026
- parser: "npm-audit",
2027
- reason: `Audit dependencies with the detected ${manager} package manager.`,
2028
- network: true
2029
- });
2030
- },
2031
- "package-outdated": async (ctx) => {
2032
- const manager = ctx.workspace.packageManager ?? "npm";
2033
- return processPlan(ctx, "package-outdated", manager, ["outdated", "--json"], {
2034
- parser: "npm-outdated",
2035
- reason: `Check outdated dependencies with ${manager}.`,
2036
- network: true
2037
- });
2038
- }
2039
- })
2040
- };
2041
- }
2042
- function javascriptProfile() {
2043
- const ts = typescriptProfile();
2044
- return {
2045
- ...ts,
2046
- id: "javascript",
2047
- displayName: "JavaScript",
2048
- extensions: Object.freeze([".js", ".jsx", ".mjs", ".cjs"]),
2049
- lspLanguageIds: Object.freeze(["javascript", "javascriptreact"]),
2050
- detectors: Object.freeze([
2051
- { kind: "config", filename: "jsconfig.json", weight: 90 },
2052
- { kind: "manifest", filename: "package.json", weight: 70 },
2053
- { kind: "lockfile", filename: "pnpm-lock.yaml", weight: 30 },
2054
- { kind: "lockfile", filename: "yarn.lock", weight: 30 },
2055
- { kind: "lockfile", filename: "package-lock.json", weight: 30 },
2056
- { kind: "lockfile", filename: "bun.lock", weight: 30 },
2057
- { kind: "lockfile", filename: "bun.lockb", weight: 30 }
2058
- ]),
2059
- operations: Object.freeze({
2060
- ...ts.operations,
2061
- syntax: async (ctx) => internalPlan(
2062
- ctx,
2063
- "syntax",
2064
- "typescript-parser",
2065
- "Parse JavaScript with the TypeScript compiler API."
2066
- )
2067
- })
2068
- };
2069
- }
2070
- var goProfile = {
2071
- id: "go",
2072
- displayName: "Go",
2073
- extensions: Object.freeze([".go"]),
2074
- lspLanguageIds: Object.freeze(["go"]),
2075
- detectors: Object.freeze([
2076
- { kind: "manifest", filename: "go.mod", weight: 90 },
2077
- { kind: "manifest", filename: "go.work", weight: 95 },
2078
- { kind: "lockfile", filename: "go.sum", weight: 30 }
2079
- ]),
2080
- ignoredDirectories: COMMON_IGNORES,
2081
- packageManagers: Object.freeze(["go"]),
2082
- executables: Object.freeze(["go", "gofmt"]),
2083
- operations: Object.freeze({
2084
- syntax: async (ctx) => {
2085
- if (!ctx.target)
2086
- return unavailable(ctx, "syntax", "Go syntax planning requires a target file.");
2087
- return processPlan(ctx, "syntax", "gofmt", ["-e", "-d", ctx.target], {
2088
- parser: "gofmt",
2089
- reason: "Parse the target and report syntax errors without writing it."
2090
- });
2091
- },
2092
- semantic: async (ctx) => processPlan(ctx, "semantic", "go", ["test", "-run", "^$", "./..."], {
2093
- parser: "go-test",
2094
- reason: "Compile all Go packages without selecting tests.",
2095
- mutating: true,
2096
- executesProjectCode: true
2097
- }),
2098
- lint: async (ctx) => processPlan(ctx, "lint", "go", ["vet", "./..."], {
2099
- parser: "go-compiler",
2100
- reason: "Run the standard Go vet checks.",
2101
- mutating: true,
2102
- executesProjectCode: true
2103
- }),
2104
- "format-check": async (ctx) => ctx.target ? processPlan(ctx, "format-check", "gofmt", ["-d", ctx.target], {
2105
- parser: "gofmt",
2106
- reason: "Report Go formatting differences for the target without writing it."
2107
- }) : unavailable(ctx, "format-check", "Go formatting requires an explicit target file."),
2108
- "format-write": async (ctx) => ctx.target ? processPlan(ctx, "format-write", "gofmt", ["-w", ctx.target], {
2109
- parser: "gofmt",
2110
- reason: "Format the target Go source file.",
2111
- mutating: true
2112
- }) : unavailable(ctx, "format-write", "Go formatting requires an explicit target file."),
2113
- test: async (ctx) => processPlan(
2114
- ctx,
2115
- "test",
2116
- "go",
2117
- ["test", ...ctx.options.filter ? ["-run", ctx.options.filter] : [], "./..."],
2118
- {
2119
- parser: "go-test",
2120
- reason: ctx.options.filter ? "Run filtered Go tests." : "Run all Go tests.",
2121
- mutating: true,
2122
- executesProjectCode: true
2123
- }
2124
- ),
2125
- build: async (ctx) => processPlan(ctx, "build", "go", ["build", "./..."], {
2126
- parser: "go-compiler",
2127
- reason: "Build all Go packages.",
2128
- mutating: true,
2129
- executesProjectCode: true
2130
- }),
2131
- run: async (ctx) => processPlan(ctx, "run", "go", ["run", "."], {
2132
- parser: "command-text",
2133
- reason: "Run the Go module entry point.",
2134
- executesProjectCode: true
2135
- }),
2136
- "debug-race": async (ctx) => processPlan(ctx, "debug-race", "go", ["test", "-race", "./..."], {
2137
- parser: "go-test",
2138
- reason: "Collect Go race-detector evidence.",
2139
- mutating: true,
2140
- executesProjectCode: true
2141
- }),
2142
- "package-install": async (ctx) => processPlan(ctx, "package-install", "go", ["mod", "download"], {
2143
- parser: "go-module",
2144
- reason: "Download the dependencies declared by go.mod.",
2145
- mutating: true,
2146
- network: true
2147
- }),
2148
- "package-add": async (ctx) => {
2149
- const names = packageNames(ctx);
2150
- if (names.length === 0)
2151
- return unavailable(ctx, "package-add", "At least one Go module is required.");
2152
- return processPlan(ctx, "package-add", "go", ["get", ...names], {
2153
- parser: "go-module",
2154
- reason: "Add validated Go modules.",
2155
- mutating: true,
2156
- network: true
2157
- });
2158
- },
2159
- "package-update": async (ctx) => processPlan(ctx, "package-update", "go", ["get", "-u", "./..."], {
2160
- parser: "go-module",
2161
- reason: "Update dependencies of all Go packages.",
2162
- mutating: true,
2163
- network: true
2164
- })
2165
- })
2166
- };
2167
- var rustProfile = {
2168
- id: "rust",
2169
- displayName: "Rust",
2170
- extensions: Object.freeze([".rs"]),
2171
- lspLanguageIds: Object.freeze(["rust"]),
2172
- detectors: Object.freeze([
2173
- { kind: "manifest", filename: "Cargo.toml", weight: 90 },
2174
- { kind: "lockfile", filename: "Cargo.lock", weight: 30 }
2175
- ]),
2176
- ignoredDirectories: COMMON_IGNORES,
2177
- packageManagers: Object.freeze(["cargo"]),
2178
- executables: Object.freeze(["cargo"]),
2179
- operations: Object.freeze({
2180
- syntax: async (ctx) => processPlan(ctx, "syntax", "cargo", ["check", "--message-format=json"], {
2181
- parser: "cargo-json",
2182
- reason: "Check Rust syntax and semantics with Cargo.",
2183
- mutating: true,
2184
- executesProjectCode: true
2185
- }),
2186
- semantic: async (ctx) => processPlan(ctx, "semantic", "cargo", ["check", "--message-format=json"], {
2187
- parser: "cargo-json",
2188
- reason: "Collect Rust compiler diagnostics with Cargo check.",
2189
- mutating: true,
2190
- executesProjectCode: true
2191
- }),
2192
- lint: async (ctx) => processPlan(ctx, "lint", "cargo", ["clippy", "--message-format=json"], {
2193
- parser: "cargo-json",
2194
- reason: "Run Clippy for this crate.",
2195
- mutating: true,
2196
- executesProjectCode: true
2197
- }),
2198
- "format-check": async (ctx) => processPlan(ctx, "format-check", "cargo", ["fmt", "--check"], {
2199
- parser: "cargo-fmt",
2200
- reason: "Check Rust formatting without writing files."
2201
- }),
2202
- "format-write": async (ctx) => processPlan(ctx, "format-write", "cargo", ["fmt"], {
2203
- parser: "cargo-fmt",
2204
- reason: "Format Rust source files in the workspace.",
2205
- mutating: true
2206
- }),
2207
- "test-compile": async (ctx) => processPlan(ctx, "test-compile", "cargo", ["test", "--no-run", "--message-format=json"], {
2208
- parser: "cargo-json",
2209
- reason: "Compile Rust tests without running them.",
2210
- mutating: true,
2211
- executesProjectCode: true
2212
- }),
2213
- test: async (ctx) => processPlan(
2214
- ctx,
2215
- "test",
2216
- "cargo",
2217
- ["test", ...ctx.options.filter ? [ctx.options.filter] : []],
2218
- {
2219
- parser: "cargo-test",
2220
- reason: ctx.options.filter ? "Run filtered Rust tests." : "Run Rust tests.",
2221
- mutating: true,
2222
- executesProjectCode: true
2223
- }
2224
- ),
2225
- build: async (ctx) => processPlan(ctx, "build", "cargo", ["build", "--message-format=json"], {
2226
- parser: "cargo-json",
2227
- reason: "Build the Rust workspace.",
2228
- mutating: true,
2229
- executesProjectCode: true
2230
- }),
2231
- run: async (ctx) => processPlan(ctx, "run", "cargo", ["run"], {
2232
- parser: "command-text",
2233
- reason: "Run the Rust workspace entry point.",
2234
- executesProjectCode: true
2235
- }),
2236
- "package-install": async (ctx) => processPlan(ctx, "package-install", "cargo", ["fetch", "--locked"], {
2237
- parser: "cargo-json",
2238
- reason: "Fetch locked Rust dependencies.",
2239
- mutating: true,
2240
- network: true
2241
- }),
2242
- "package-add": async (ctx) => {
2243
- const names = packageNames(ctx);
2244
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one crate is required.") : processPlan(ctx, "package-add", "cargo", ["add", ...names], {
2245
- parser: "cargo-text",
2246
- reason: "Add validated Rust crates.",
2247
- mutating: true,
2248
- network: true
2249
- });
2250
- },
2251
- "package-remove": async (ctx) => {
2252
- const names = packageNames(ctx);
2253
- return names.length === 0 ? unavailable(ctx, "package-remove", "At least one crate is required.") : processPlan(ctx, "package-remove", "cargo", ["remove", ...names], {
2254
- parser: "cargo-text",
2255
- reason: "Remove validated Rust crates.",
2256
- mutating: true
2257
- });
2258
- },
2259
- "package-update": async (ctx) => processPlan(ctx, "package-update", "cargo", ["update"], {
2260
- parser: "cargo-text",
2261
- reason: "Update the Cargo lockfile.",
2262
- mutating: true,
2263
- network: true
2264
- }),
2265
- "package-audit": async (ctx) => processPlan(ctx, "package-audit", "cargo", ["audit", "--json"], {
2266
- parser: "cargo-audit",
2267
- reason: "Audit Rust dependencies when cargo-audit is installed.",
2268
- network: true
2269
- })
2270
- })
2271
- };
2272
- var phpProfile = {
2273
- id: "php",
2274
- displayName: "PHP",
2275
- extensions: Object.freeze([".php"]),
2276
- lspLanguageIds: Object.freeze(["php"]),
2277
- detectors: Object.freeze([
2278
- { kind: "manifest", filename: "composer.json", weight: 90 },
2279
- { kind: "lockfile", filename: "composer.lock", weight: 30 },
2280
- { kind: "config", filename: "phpunit.xml", weight: 25 },
2281
- { kind: "config", filename: "phpunit.xml.dist", weight: 25 }
2282
- ]),
2283
- ignoredDirectories: COMMON_IGNORES,
2284
- packageManagers: Object.freeze(["composer"]),
2285
- executables: Object.freeze(["php", "composer"]),
2286
- operations: Object.freeze({
2287
- syntax: async (ctx) => ctx.target ? processPlan(ctx, "syntax", "php", ["-l", ctx.target], {
2288
- parser: "php-lint",
2289
- reason: "Lint the target PHP file without executing it."
2290
- }) : unavailable(ctx, "syntax", "PHP syntax planning requires a target file."),
2291
- semantic: async (ctx) => unavailable(
2292
- ctx,
2293
- "semantic",
2294
- "No configured PHPStan or Psalm adapter was detected in Phase 1."
2295
- ),
2296
- test: async (ctx) => processPlan(
2297
- ctx,
2298
- "test",
2299
- "php",
2300
- ["vendor/bin/phpunit", ...ctx.options.filter ? ["--filter", ctx.options.filter] : []],
2301
- {
2302
- parser: "phpunit",
2303
- reason: ctx.options.filter ? "Run filtered tests with the project-local PHPUnit runner." : "Run the project-local PHPUnit test runner.",
2304
- executesProjectCode: true
2305
- }
2306
- ),
2307
- "package-install": async (ctx) => processPlan(
2308
- ctx,
2309
- "package-install",
2310
- "composer",
2311
- ["install", "--no-interaction", "--no-scripts"],
2312
- {
2313
- parser: "composer",
2314
- reason: "Restore Composer dependencies without scripts.",
2315
- mutating: true,
2316
- network: true
2317
- }
2318
- ),
2319
- "package-add": async (ctx) => {
2320
- const names = packageNames(ctx);
2321
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one Composer package is required.") : processPlan(
2322
- ctx,
2323
- "package-add",
2324
- "composer",
2325
- ["require", "--no-interaction", "--no-scripts", ...names],
2326
- {
2327
- parser: "composer",
2328
- reason: "Add validated Composer packages without scripts.",
2329
- mutating: true,
2330
- network: true
2331
- }
2332
- );
2333
- },
2334
- "package-remove": async (ctx) => {
2335
- const names = packageNames(ctx);
2336
- return names.length === 0 ? unavailable(ctx, "package-remove", "At least one Composer package is required.") : processPlan(
2337
- ctx,
2338
- "package-remove",
2339
- "composer",
2340
- ["remove", "--no-interaction", "--no-scripts", ...names],
2341
- {
2342
- parser: "composer",
2343
- reason: "Remove validated Composer packages without scripts.",
2344
- mutating: true
2345
- }
2346
- );
2347
- },
2348
- "package-update": async (ctx) => processPlan(
2349
- ctx,
2350
- "package-update",
2351
- "composer",
2352
- ["update", "--no-interaction", "--no-scripts"],
2353
- {
2354
- parser: "composer",
2355
- reason: "Update Composer dependencies without scripts.",
2356
- mutating: true,
2357
- network: true
2358
- }
2359
- ),
2360
- "package-audit": async (ctx) => processPlan(ctx, "package-audit", "composer", ["audit", "--format=json"], {
2361
- parser: "composer-audit",
2362
- reason: "Audit Composer dependencies.",
2363
- network: true
2364
- }),
2365
- "package-outdated": async (ctx) => processPlan(ctx, "package-outdated", "composer", ["outdated", "--format=json"], {
2366
- parser: "composer-outdated",
2367
- reason: "Check outdated Composer dependencies.",
2368
- network: true
2369
- })
2370
- })
2371
- };
2372
- var csharpProfile = {
2373
- id: "csharp",
2374
- displayName: "C# / .NET",
2375
- extensions: Object.freeze([".cs"]),
2376
- lspLanguageIds: Object.freeze(["csharp"]),
2377
- detectors: Object.freeze([
2378
- { kind: "config", filename: "global.json", weight: 25 },
2379
- { kind: "manifest", suffix: ".slnx", weight: 95 },
2380
- { kind: "manifest", suffix: ".sln", weight: 95 },
2381
- { kind: "manifest", suffix: ".csproj", weight: 90 },
2382
- { kind: "manifest", suffix: ".fsproj", weight: 90 },
2383
- { kind: "lockfile", filename: "packages.lock.json", weight: 30 }
2384
- ]),
2385
- ignoredDirectories: COMMON_IGNORES,
2386
- packageManagers: Object.freeze(["dotnet"]),
2387
- executables: Object.freeze(["dotnet"]),
2388
- operations: Object.freeze({
2389
- syntax: async (ctx) => processPlan(ctx, "syntax", "dotnet", ["build", "--no-restore"], {
2390
- parser: "dotnet-build",
2391
- reason: "Use the nearest project or solution to collect C# syntax diagnostics.",
2392
- mutating: true,
2393
- executesProjectCode: true
2394
- }),
2395
- semantic: async (ctx) => processPlan(ctx, "semantic", "dotnet", ["build", "--no-restore"], {
2396
- parser: "dotnet-build",
2397
- reason: "Build without restoring to collect .NET compiler diagnostics.",
2398
- mutating: true,
2399
- executesProjectCode: true
2400
- }),
2401
- lint: async (ctx) => processPlan(ctx, "lint", "dotnet", ["format", "--verify-no-changes", "--no-restore"], {
2402
- parser: "dotnet-format",
2403
- reason: "Verify .NET formatting and analyzers without writing source files.",
2404
- mutating: true,
2405
- executesProjectCode: true
2406
- }),
2407
- "format-check": async (ctx) => processPlan(
2408
- ctx,
2409
- "format-check",
2410
- "dotnet",
2411
- ["format", "--verify-no-changes", "--no-restore"],
2412
- {
2413
- parser: "dotnet-format",
2414
- reason: "Verify .NET formatting without source writes.",
2415
- mutating: true,
2416
- executesProjectCode: true
2417
- }
2418
- ),
2419
- "format-write": async (ctx) => processPlan(ctx, "format-write", "dotnet", ["format", "--no-restore"], {
2420
- parser: "dotnet-format",
2421
- reason: "Format .NET source files without restoring packages.",
2422
- mutating: true,
2423
- executesProjectCode: true
2424
- }),
2425
- test: async (ctx) => processPlan(
2426
- ctx,
2427
- "test",
2428
- "dotnet",
2429
- ["test", "--no-restore", ...ctx.options.filter ? ["--filter", ctx.options.filter] : []],
2430
- {
2431
- parser: "dotnet-test",
2432
- reason: ctx.options.filter ? "Run filtered .NET tests without restoring packages." : "Run .NET tests without restoring packages.",
2433
- mutating: true,
2434
- executesProjectCode: true
2435
- }
2436
- ),
2437
- build: async (ctx) => processPlan(ctx, "build", "dotnet", ["build", "--no-restore"], {
2438
- parser: "dotnet-build",
2439
- reason: "Build the nearest .NET project or solution without restoring packages.",
2440
- mutating: true,
2441
- executesProjectCode: true
2442
- }),
2443
- run: async (ctx) => processPlan(ctx, "run", "dotnet", ["run", "--no-restore"], {
2444
- parser: "command-text",
2445
- reason: "Run the .NET project entry point.",
2446
- mutating: true,
2447
- executesProjectCode: true
2448
- }),
2449
- "package-install": async (ctx) => processPlan(ctx, "package-install", "dotnet", ["restore", "--locked-mode"], {
2450
- parser: "dotnet-restore",
2451
- reason: "Restore locked .NET dependencies.",
2452
- mutating: true,
2453
- network: true,
2454
- executesProjectCode: true
2455
- }),
2456
- "package-add": async (ctx) => {
2457
- const names = packageNames(ctx);
2458
- const [spec] = names;
2459
- const versionAt = spec?.lastIndexOf("@") ?? -1;
2460
- const packageName = versionAt > 0 ? spec?.slice(0, versionAt) : spec;
2461
- const packageVersion = versionAt > 0 ? spec?.slice(versionAt + 1) : void 0;
2462
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one NuGet package is required.") : names.length > 1 ? unavailable(ctx, "package-add", "NuGet package changes run one package at a time.") : processPlan(
2463
- ctx,
2464
- "package-add",
2465
- "dotnet",
2466
- [
2467
- "add",
2468
- "package",
2469
- packageName ?? "",
2470
- ...packageVersion ? ["--version", packageVersion] : []
2471
- ],
2472
- {
2473
- parser: "dotnet-package",
2474
- reason: "Add validated NuGet packages.",
2475
- mutating: true,
2476
- network: true,
2477
- executesProjectCode: true
2478
- }
2479
- );
2480
- },
2481
- "package-remove": async (ctx) => {
2482
- const names = packageNames(ctx);
2483
- return names.length === 0 ? unavailable(ctx, "package-remove", "At least one NuGet package is required.") : processPlan(ctx, "package-remove", "dotnet", ["remove", "package", ...names], {
2484
- parser: "dotnet-package",
2485
- reason: "Remove validated NuGet packages.",
2486
- mutating: true
2487
- });
2488
- },
2489
- "package-audit": async (ctx) => processPlan(
2490
- ctx,
2491
- "package-audit",
2492
- "dotnet",
2493
- ["list", "package", "--vulnerable", "--format", "json"],
2494
- { parser: "dotnet-package", reason: "List vulnerable NuGet dependencies.", network: true }
2495
- ),
2496
- "package-outdated": async (ctx) => processPlan(
2497
- ctx,
2498
- "package-outdated",
2499
- "dotnet",
2500
- ["list", "package", "--outdated", "--format", "json"],
2501
- { parser: "dotnet-package", reason: "List outdated NuGet dependencies.", network: true }
2502
- )
2503
- })
2504
- };
2505
- var PRIMARY_LANGUAGE_PROFILES = Object.freeze([
2506
- Object.freeze(typescriptProfile()),
2507
- Object.freeze(javascriptProfile()),
2508
- Object.freeze(goProfile),
2509
- Object.freeze(rustProfile),
2510
- Object.freeze(phpProfile),
2511
- Object.freeze(csharpProfile)
2512
- ]);
2513
-
2514
- // src/languages/registry.ts
2515
- var PROFILE_ID_RE = /^[a-z][a-z0-9-]{0,63}$/;
2516
- var EXECUTABLE_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
2517
- var KNOWN_OPERATIONS = /* @__PURE__ */ new Set([
2518
- "syntax",
2519
- "semantic",
2520
- "lint",
2521
- "format-check",
2522
- "format-write",
2523
- "test-compile",
2524
- "test",
2525
- "build",
2526
- "run",
2527
- "debug-compile",
2528
- "debug-test",
2529
- "debug-runtime",
2530
- "debug-race",
2531
- "package-install",
2532
- "package-add",
2533
- "package-remove",
2534
- "package-update",
2535
- "package-audit",
2536
- "package-outdated"
2537
- ]);
2538
- var LanguageProfileRegistry = class {
2539
- #profiles = /* @__PURE__ */ new Map();
2540
- constructor(profiles = []) {
2541
- for (const profile of profiles) this.register(profile);
2542
- }
2543
- register(profile) {
2544
- const errors = validateLanguageProfile(profile);
2545
- if (errors.length > 0) {
2546
- throw new Error(`Invalid language profile "${profile.id}": ${errors.join("; ")}`);
2547
- }
2548
- if (this.#profiles.has(profile.id)) {
2549
- throw new Error(`Language profile "${profile.id}" is already registered`);
2550
- }
2551
- this.#profiles.set(profile.id, freezeProfile(profile));
2552
- }
2553
- get(id) {
2554
- return this.#profiles.get(id);
2555
- }
2556
- list() {
2557
- return Object.freeze([...this.#profiles.values()]);
2558
- }
2559
- };
2560
- function validateLanguageProfile(profile) {
2561
- const errors = [];
2562
- if (!PROFILE_ID_RE.test(profile.id)) errors.push("id must be a lowercase stable identifier");
2563
- if (!profile.displayName.trim()) errors.push("displayName is required");
2564
- if (profile.extensions.length === 0) errors.push("at least one extension is required");
2565
- for (const ext of profile.extensions) {
2566
- if (!/^\.[a-z0-9+.-]+$/i.test(ext)) errors.push(`invalid extension "${ext}"`);
2567
- }
2568
- if (profile.detectors.length === 0) errors.push("at least one detector is required");
2569
- for (const detector of profile.detectors) {
2570
- if ((detector.filename ? 1 : 0) + (detector.suffix ? 1 : 0) !== 1) {
2571
- errors.push("each detector must declare exactly one of filename or suffix");
2572
- }
2573
- if (!Number.isFinite(detector.weight) || detector.weight <= 0 || detector.weight > 100) {
2574
- errors.push("detector weights must be in the range 1..100");
2575
- }
2576
- const marker = detector.filename ?? detector.suffix ?? "";
2577
- if (marker.includes("/") || marker.includes("\\") || marker.includes("\0")) {
2578
- errors.push(`detector marker "${marker}" must be a basename or suffix`);
2579
- }
2580
- }
2581
- if (profile.executables.length === 0) errors.push("at least one executable is required");
2582
- for (const executable of profile.executables) {
2583
- if (!EXECUTABLE_RE.test(executable)) errors.push(`invalid executable token "${executable}"`);
2584
- }
2585
- for (const operation of Object.keys(profile.operations)) {
2586
- if (!KNOWN_OPERATIONS.has(operation)) {
2587
- errors.push(`unknown operation "${operation}"`);
2588
- }
2589
- if (typeof profile.operations[operation] !== "function") {
2590
- errors.push(`operation "${operation}" must be a resolver function`);
2591
- }
2592
- }
2593
- return [...new Set(errors)];
2594
- }
2595
- function freezeProfile(profile) {
2596
- const detectors = Object.freeze(profile.detectors.map((item) => Object.freeze({ ...item })));
2597
- const operations = Object.freeze({ ...profile.operations });
2598
- return Object.freeze({
2599
- ...profile,
2600
- extensions: Object.freeze([...profile.extensions]),
2601
- lspLanguageIds: Object.freeze([...profile.lspLanguageIds]),
2602
- detectors,
2603
- ignoredDirectories: Object.freeze([...profile.ignoredDirectories]),
2604
- packageManagers: Object.freeze([...profile.packageManagers]),
2605
- executables: Object.freeze([...profile.executables]),
2606
- operations
2607
- });
2608
- }
2609
- var languageProfileRegistry = new LanguageProfileRegistry([
2610
- ...PRIMARY_LANGUAGE_PROFILES,
2611
- ...ADDITIONAL_LANGUAGE_PROFILES
2612
- ]);
2613
-
2614
- // src/languages/detect.ts
2615
- var DEFAULT_LIMITS = { maxDepth: 6, maxEntries: 5e3 };
2616
- var SOURCE_WEIGHT = 5;
2617
- var SOURCE_WEIGHT_CAP = 25;
2618
- var TARGET_WEIGHT = 100;
2619
- var GLOBAL_IGNORES = /* @__PURE__ */ new Set([
2620
- ".git",
2621
- ".wrongstack",
2622
- "node_modules",
2623
- "vendor",
2624
- "target",
2625
- "bin",
2626
- "obj",
2627
- "dist",
2628
- "build",
2629
- "out",
2630
- "coverage",
2631
- ".cache",
2632
- ".idea",
2633
- ".vscode",
2634
- ".next",
2635
- ".nuxt"
2636
- ]);
2637
- async function detectLanguageWorkspaces(options) {
2638
- const projectRoot = await canonicalDirectory(options.projectRoot);
2639
- const cwdInput = options.cwd ? path4.isAbsolute(options.cwd) ? options.cwd : path4.resolve(projectRoot, options.cwd) : projectRoot;
2640
- const cwd = await canonicalInside(cwdInput, projectRoot, "cwd");
2641
- const target = options.target ? await canonicalInside(resolveFrom(cwd, options.target), projectRoot, "target") : void 0;
2642
- const profiles = (options.profiles ?? languageProfileRegistry.list()).filter((profile) => !options.language || profile.id === options.language).slice().sort((a, b) => a.id.localeCompare(b.id));
2643
- const limits = normalizeLimits(options.limits);
2644
- const extraIgnores = new Set(options.ignoredDirectories ?? []);
2645
- const state = {
2646
- entries: 0,
2647
- truncated: false,
2648
- sourcePaths: /* @__PURE__ */ new Map(),
2649
- candidates: /* @__PURE__ */ new Map()
2650
- };
2651
- await scanDirectory(projectRoot, 0, profiles, limits, state, extraIgnores, options.signal);
2652
- addSourceFallbacks(projectRoot, profiles, state);
2653
- if (target) addTargetEvidence(target, projectRoot, profiles, state);
2654
- const workspaces = await Promise.all(
2655
- [...state.candidates.values()].map((candidate) => finalizeCandidate(candidate, projectRoot))
2656
- );
2657
- workspaces.sort(compareWorkspaces);
2658
- return {
2659
- projectRoot,
2660
- scannedEntries: state.entries,
2661
- truncated: state.truncated,
2662
- workspaces
2663
- };
2664
- }
2665
- async function scanDirectory(directory, depth, profiles, limits, state, extraIgnores, signal) {
2666
- signal?.throwIfAborted();
2667
- if (depth > limits.maxDepth || state.entries >= limits.maxEntries) {
2668
- state.truncated = true;
2669
- return;
2670
- }
2671
- let entries;
2672
- try {
2673
- entries = await fs2.readdir(directory, { withFileTypes: true });
2674
- } catch {
2675
- return;
2676
- }
2677
- entries.sort((a, b) => a.name.localeCompare(b.name));
2678
- for (const entry of entries) {
2679
- signal?.throwIfAborted();
2680
- if (state.entries >= limits.maxEntries) {
2681
- state.truncated = true;
2682
- return;
2683
- }
2684
- state.entries++;
2685
- const fullPath = path4.join(directory, entry.name);
2686
- if (entry.isSymbolicLink()) continue;
2687
- if (entry.isDirectory()) {
2688
- if (shouldIgnoreDirectory(entry.name, profiles, extraIgnores)) continue;
2689
- if (depth >= limits.maxDepth) {
2690
- state.truncated = true;
2691
- continue;
2692
- }
2693
- await scanDirectory(fullPath, depth + 1, profiles, limits, state, extraIgnores, signal);
2694
- continue;
2695
- }
2696
- if (!entry.isFile()) continue;
2697
- collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2698
- }
2699
- }
2700
- function collectFileEvidence(directory, fullPath, basename5, profiles, state) {
2701
- const lower = basename5.toLowerCase();
2702
- const extension = path4.extname(lower);
2703
- for (const profile of profiles) {
2704
- const detector = profile.detectors.find(
2705
- (rule) => rule.filename ? lower === rule.filename.toLowerCase() : lower.endsWith(rule.suffix.toLowerCase())
2706
- );
2707
- if (detector) {
2708
- const candidate = getCandidate(state, profile, directory);
2709
- candidate.evidence.push({
2710
- kind: detector.kind,
2711
- path: fullPath,
2712
- value: basename5,
2713
- weight: detector.weight
2714
- });
2715
- if (detector.kind === "manifest" || detector.kind === "config") {
2716
- candidate.manifests.push(fullPath);
2717
- }
2718
- }
2719
- if (profile.extensions.includes(extension)) {
2720
- const paths = state.sourcePaths.get(profile.id) ?? [];
2721
- if (paths.length < SOURCE_WEIGHT_CAP / SOURCE_WEIGHT) paths.push(fullPath);
2722
- state.sourcePaths.set(profile.id, paths);
2723
- }
2724
- }
2725
- }
2726
- function addSourceFallbacks(projectRoot, profiles, state) {
2727
- for (const profile of profiles) {
2728
- const sources = state.sourcePaths.get(profile.id) ?? [];
2729
- if (sources.length === 0) continue;
2730
- const profileCandidates = [...state.candidates.values()].filter(
2731
- (item) => item.profile.id === profile.id
2732
- );
2733
- for (const source of sources) {
2734
- const containing = profileCandidates.filter((candidate2) => isInside(source, candidate2.root)).sort(
2735
- (a, b) => pathDepth(b.root, projectRoot) - pathDepth(a.root, projectRoot) || a.root.localeCompare(b.root)
2736
- );
2737
- const candidate = containing[0] ?? (profile.sourceFallback === false ? void 0 : getCandidate(state, profile, projectRoot));
2738
- if (!candidate) continue;
2739
- candidate.evidence.push({
2740
- kind: "source",
2741
- path: source,
2742
- value: path4.extname(source),
2743
- weight: SOURCE_WEIGHT
2744
- });
2745
- }
2746
- }
2747
- }
2748
- function addTargetEvidence(target, projectRoot, profiles, state) {
2749
- const extension = path4.extname(target).toLowerCase();
2750
- for (const profile of profiles) {
2751
- if (!profile.extensions.includes(extension)) continue;
2752
- const candidates = [...state.candidates.values()].filter(
2753
- (candidate2) => candidate2.profile.id === profile.id && isInside(target, candidate2.root)
2754
- );
2755
- const candidate = candidates.length > 0 ? candidates.sort(
2756
- (a, b) => pathDepth(b.root, projectRoot) - pathDepth(a.root, projectRoot) || a.root.localeCompare(b.root)
2757
- )[0] : profile.sourceFallback === false ? void 0 : getCandidate(state, profile, path4.dirname(target));
2758
- if (!candidate) continue;
2759
- candidate.evidence.push({
2760
- kind: "target",
2761
- path: target,
2762
- value: extension,
2763
- weight: TARGET_WEIGHT
2764
- });
2765
- }
2766
- }
2767
- async function finalizeCandidate(candidate, projectRoot) {
2768
- const evidence = dedupeEvidence(candidate.evidence).sort(compareEvidence);
2769
- const manifests = [...new Set(candidate.manifests)].sort();
2770
- const confidence = Math.min(1, evidence.reduce((sum, item) => sum + item.weight, 0) / 100);
2771
- const packageManager = await detectPackageManager2(candidate.profile, candidate.root, evidence);
2772
- const id = createHash("sha256").update(`${candidate.profile.id}\0${path4.relative(projectRoot, candidate.root)}`).digest("hex").slice(0, 16);
2773
- return Object.freeze({
2774
- id,
2775
- language: candidate.profile.id,
2776
- root: candidate.root,
2777
- confidence,
2778
- evidence: Object.freeze(evidence.map((item) => Object.freeze(item))),
2779
- ...packageManager ? { packageManager } : {},
2780
- manifests: Object.freeze(manifests),
2781
- capabilities: Object.freeze(
2782
- Object.keys(candidate.profile.operations).sort()
2783
- )
2784
- });
2785
- }
2786
- async function detectPackageManager2(profile, root, evidence) {
2787
- if (profile.packageManagers.length === 1) return profile.packageManagers[0];
2788
- if (profile.id !== "typescript" && profile.id !== "javascript") return void 0;
2789
- let declared;
2790
- try {
2791
- const pkg = JSON.parse(await fs2.readFile(path4.join(root, "package.json"), "utf8"));
2792
- if (typeof pkg.packageManager === "string") {
2793
- const manager = pkg.packageManager.split("@")[0];
2794
- if (manager && profile.packageManagers.includes(manager)) declared = manager;
2795
- }
2796
- } catch {
2797
- }
2798
- const lockManagers = /* @__PURE__ */ new Set();
2799
- for (const item of evidence) {
2800
- const name = path4.basename(item.path).toLowerCase();
2801
- if (name === "pnpm-lock.yaml") lockManagers.add("pnpm");
2802
- else if (name === "yarn.lock") lockManagers.add("yarn");
2803
- else if (name === "bun.lock" || name === "bun.lockb") lockManagers.add("bun");
2804
- else if (name === "package-lock.json") lockManagers.add("npm");
2805
- }
2806
- if (declared && (lockManagers.size === 0 || lockManagers.size === 1 && lockManagers.has(declared))) {
2807
- return declared;
2808
- }
2809
- if (lockManagers.size === 1) return [...lockManagers][0];
2810
- if (lockManagers.size > 1) return void 0;
2811
- return declared ?? "npm";
2812
- }
2813
- function getCandidate(state, profile, root) {
2814
- const key = `${profile.id}\0${root}`;
2815
- let candidate = state.candidates.get(key);
2816
- if (!candidate) {
2817
- candidate = { profile, root, evidence: [], manifests: [] };
2818
- state.candidates.set(key, candidate);
2819
- }
2820
- return candidate;
2821
- }
2822
- function shouldIgnoreDirectory(name, profiles, extraIgnores) {
2823
- if (GLOBAL_IGNORES.has(name) || extraIgnores.has(name) || name.startsWith(".")) return true;
2824
- return profiles.some((profile) => profile.ignoredDirectories.includes(name));
2825
- }
2826
- function normalizeLimits(input) {
2827
- const maxDepth = Math.max(
2828
- 0,
2829
- Math.min(12, Math.trunc(input?.maxDepth ?? DEFAULT_LIMITS.maxDepth))
2830
- );
2831
- const maxEntries = Math.max(
2832
- 1,
2833
- Math.min(5e4, Math.trunc(input?.maxEntries ?? DEFAULT_LIMITS.maxEntries))
2834
- );
2835
- return { maxDepth, maxEntries };
2836
- }
2837
- async function canonicalDirectory(input) {
2838
- const resolved = path4.resolve(input);
2839
- const real = await fs2.realpath(resolved);
2840
- const stat5 = await fs2.stat(real);
2841
- if (!stat5.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2842
- return real;
2843
- }
2844
- async function canonicalInside(input, root, label) {
2845
- const resolved = path4.resolve(input);
2846
- let real;
2847
- try {
2848
- real = await fs2.realpath(resolved);
2849
- } catch (error) {
2850
- if (error.code !== "ENOENT") throw error;
2851
- const parent = await fs2.realpath(path4.dirname(resolved));
2852
- real = path4.join(parent, path4.basename(resolved));
2853
- }
2854
- if (!isInside(real, root)) throw new Error(`${label} is outside project root: ${input}`);
2855
- return real;
2856
- }
2857
- function resolveFrom(cwd, input) {
2858
- return path4.isAbsolute(input) ? input : path4.resolve(cwd, input);
2859
- }
2860
- function isInside(candidate, root) {
2861
- const relative5 = path4.relative(root, candidate);
2862
- return relative5 === "" || !relative5.startsWith("..") && !path4.isAbsolute(relative5);
2863
- }
2864
- function pathDepth(candidate, root) {
2865
- const relative5 = path4.relative(root, candidate);
2866
- return relative5 === "" ? 0 : relative5.split(path4.sep).length;
2867
- }
2868
- function dedupeEvidence(items) {
2869
- const seen = /* @__PURE__ */ new Set();
2870
- return items.filter((item) => {
2871
- const key = `${item.kind}\0${item.path}\0${item.value}\0${item.weight}`;
2872
- if (seen.has(key)) return false;
2873
- seen.add(key);
2874
- return true;
2875
- });
2876
- }
2877
- function compareEvidence(a, b) {
2878
- return a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind) || b.weight - a.weight;
2879
- }
2880
- function compareWorkspaces(a, b) {
2881
- return b.confidence - a.confidence || a.language.localeCompare(b.language) || a.root.localeCompare(b.root) || a.id.localeCompare(b.id);
2882
- }
2883
-
2884
- // src/languages/diagnostics.ts
2885
- import * as path5 from "node:path";
2886
- var MAX_DIAGNOSTICS = 200;
2887
- function parseLanguageDiagnostics(parser, stdout, stderr, workspaceRoot) {
2888
- const text = `${stdout}${stdout && stderr ? "\n" : ""}${stderr}`;
2889
- let diagnostics;
2890
- switch (parser) {
2891
- case "typescript":
2892
- diagnostics = parseTypeScript(text, workspaceRoot);
2893
- break;
2894
- case "cargo-json":
2895
- diagnostics = parseCargoJson(text, workspaceRoot);
2896
- break;
2897
- case "php-lint":
2898
- diagnostics = parsePhpLint(text, workspaceRoot);
2899
- break;
2900
- case "dotnet-build":
2901
- case "dotnet-format":
2902
- case "dotnet-test":
2903
- diagnostics = parseDotnet(text, workspaceRoot);
2904
- break;
2905
- case "go-test":
2906
- case "go-compiler":
2907
- case "gofmt":
2908
- diagnostics = parseGo(text, workspaceRoot);
2909
- break;
2910
- case "biome":
2911
- diagnostics = parseBiome(text, workspaceRoot);
2912
- break;
2913
- default:
2914
- diagnostics = parseGeneric(text, parser, workspaceRoot);
2915
- break;
2916
- }
2917
- const sorted = dedupeDiagnostics(diagnostics).sort(compareDiagnostics);
2918
- const omitted = Math.max(0, sorted.length - MAX_DIAGNOSTICS);
2919
- const kept = Object.freeze(sorted.slice(0, MAX_DIAGNOSTICS).map((item) => Object.freeze(item)));
2920
- return {
2921
- diagnostics: kept,
2922
- omitted,
2923
- summary: summarize(kept)
2924
- };
2925
- }
2926
- function parseTypeScript(text, root) {
2927
- const diagnostics = [];
2928
- const regex = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s*(.+)$/gm;
2929
- for (const match of text.matchAll(regex)) {
2930
- diagnostics.push({
2931
- severity: match[4] === "warning" ? "warning" : "error",
2932
- code: match[5],
2933
- message: match[6].trim(),
2934
- file: normalizeDiagnosticPath(match[1], root),
2935
- range: { start: { line: toPositiveInt(match[2]), column: toPositiveInt(match[3]) } },
2936
- source: "typescript"
2937
- });
2938
- }
2939
- return diagnostics;
2940
- }
2941
- function parseCargoJson(text, root) {
2942
- const diagnostics = [];
2943
- for (const line of text.split(/\r?\n/)) {
2944
- if (!line.trim().startsWith("{")) continue;
2945
- try {
2946
- const value = JSON.parse(line);
2947
- if (value.reason !== "compiler-message" || !value.message?.message) continue;
2948
- const primary = value.message.spans?.find((span) => span.is_primary) ?? value.message.spans?.[0];
2949
- diagnostics.push({
2950
- severity: normalizeSeverity(value.message.level),
2951
- ...value.message.code?.code ? { code: value.message.code.code } : {},
2952
- message: value.message.message,
2953
- ...primary?.file_name ? { file: normalizeDiagnosticPath(primary.file_name, root) } : {},
2954
- ...primary?.line_start ? { range: { start: { line: primary.line_start, column: primary.column_start ?? 1 } } } : {},
2955
- source: "rustc"
2956
- });
2957
- } catch {
2958
- }
2959
- }
2960
- return diagnostics;
2961
- }
2962
- function parseGo(text, root) {
2963
- return parseLinePattern(
2964
- text,
2965
- /^(.*?\.go):(\d+):(\d+):\s*(.+)$/gm,
2966
- root,
2967
- "go",
2968
- (_match, message) => ({ message, severity: /warning/i.test(message) ? "warning" : "error" })
2969
- );
2970
- }
2971
- function parsePhpLint(text, root) {
2972
- const diagnostics = [];
2973
- const regex = /(?:PHP\s+)?(?:Parse|Fatal) error:\s*(.+?)\s+in\s+(.+?)\s+on line\s+(\d+)/gi;
2974
- for (const match of text.matchAll(regex)) {
2975
- diagnostics.push({
2976
- severity: "error",
2977
- message: match[1].trim(),
2978
- file: normalizeDiagnosticPath(match[2], root),
2979
- range: { start: { line: toPositiveInt(match[3]), column: 1 } },
2980
- source: "php"
2981
- });
2982
- }
2983
- return diagnostics;
2984
- }
2985
- function parseDotnet(text, root) {
2986
- const diagnostics = [];
2987
- const regex = /^(.+?)\((\d+),(\d+)\):\s*(error|warning)\s+([A-Z]+\d+):\s*(.+?)(?:\s+\[.+\])?$/gm;
2988
- for (const match of text.matchAll(regex)) {
2989
- diagnostics.push({
2990
- severity: match[4] === "warning" ? "warning" : "error",
2991
- code: match[5],
2992
- message: match[6].trim(),
2993
- file: normalizeDiagnosticPath(match[1], root),
2994
- range: { start: { line: toPositiveInt(match[2]), column: toPositiveInt(match[3]) } },
2995
- source: "dotnet"
2996
- });
2997
- }
2998
- return diagnostics;
2999
- }
3000
- function parseBiome(text, root) {
3001
- const diagnostics = [];
3002
- const regex = /^(.+?):(\d+):(\d+)\s+(lint\/[^\s]+|format)\s+(.+)$/gm;
3003
- for (const match of text.matchAll(regex)) {
3004
- diagnostics.push({
3005
- severity: "warning",
3006
- code: match[4],
3007
- message: match[5].trim(),
3008
- file: normalizeDiagnosticPath(match[1], root),
3009
- range: { start: { line: toPositiveInt(match[2]), column: toPositiveInt(match[3]) } },
3010
- source: "biome"
3011
- });
3012
- }
3013
- return diagnostics;
3014
- }
3015
- function parseGeneric(text, source, root) {
3016
- return parseLinePattern(
3017
- text,
3018
- /^(.+?):(\d+):(\d+):\s*(?:(error|warning|info):\s*)?(.+)$/gm,
3019
- root,
3020
- source,
3021
- (match, fallback) => ({
3022
- severity: normalizeSeverity(match[4]),
3023
- message: match[5]?.trim() || fallback
3024
- })
3025
- );
3026
- }
3027
- function parsePackageReports(parser, stdout, stderr) {
3028
- const text = `${stdout}${stdout && stderr ? "\n" : ""}${stderr}`;
3029
- switch (parser) {
3030
- case "npm-audit":
3031
- return parseNpmAudit(text);
3032
- case "npm-outdated":
3033
- return parseNpmOutdated(text);
3034
- case "cargo-audit":
3035
- return parseCargoAudit(text);
3036
- case "composer-audit":
3037
- return parseComposerAudit(text);
3038
- case "composer-outdated":
3039
- return parseComposerOutdated(text);
3040
- case "dotnet-package":
3041
- return parseDotnetPackage(text);
3042
- default:
3043
- return { diagnostics: [], vulnerabilities: [], outdated: [] };
3044
- }
3045
- }
3046
- function parseNpmAudit(text) {
3047
- const advisories = [];
3048
- const diagnostics = [];
3049
- let root;
3050
- try {
3051
- root = JSON.parse(text);
3052
- } catch {
3053
- return { diagnostics, vulnerabilities: advisories, outdated: [] };
3054
- }
3055
- const vulnerabilities = root?.vulnerabilities ?? {};
3056
- const advisoriesRecord = root?.advisories ?? vulnerabilities;
3057
- for (const [id, value] of Object.entries(advisoriesRecord)) {
3058
- const advisory = value;
3059
- const name = advisory.module_name ?? advisory.package_name ?? advisory.name ?? id;
3060
- advisories.push({
3061
- package: name,
3062
- ...advisory.title ? { advisory: advisory.title } : { advisory: id },
3063
- severity: mapSeverity(advisory.severity),
3064
- ...advisory.patched_versions ? { fixedIn: advisory.patched_versions } : {},
3065
- ...advisory.url ? { url: advisory.url } : {}
3066
- });
3067
- diagnostics.push({
3068
- severity: mapSeverity(advisory.severity) === "critical" || mapSeverity(advisory.severity) === "high" ? "error" : "warning",
3069
- code: id,
3070
- message: advisory.title ?? `Vulnerability reported for ${name}.`,
3071
- source: "npm-audit"
3072
- });
3073
- }
3074
- return { diagnostics, vulnerabilities: advisories, outdated: [] };
3075
- }
3076
- function parseNpmOutdated(text) {
3077
- const diagnostics = [];
3078
- const outdated = [];
3079
- let payload = {};
3080
- try {
3081
- payload = JSON.parse(text);
3082
- } catch {
3083
- return { diagnostics, vulnerabilities: [], outdated };
3084
- }
3085
- for (const [name, info] of Object.entries(payload)) {
3086
- const entry = info;
3087
- if (!entry.latest || entry.latest === entry.current) continue;
3088
- outdated.push({
3089
- name,
3090
- previous: entry.current,
3091
- resolved: entry.latest,
3092
- kind: mapOutdatedKind(entry.type)
3093
- });
3094
- diagnostics.push({
3095
- severity: "info",
3096
- code: "outdated",
3097
- message: `${name}: ${entry.current ?? "?"} \u2192 ${entry.latest}`,
3098
- source: "npm-outdated"
3099
- });
3100
- }
3101
- return { diagnostics, vulnerabilities: [], outdated };
3102
- }
3103
- function parseCargoAudit(text) {
3104
- const diagnostics = [];
3105
- const vulnerabilities = [];
3106
- let root;
3107
- try {
3108
- root = JSON.parse(text);
3109
- } catch {
3110
- return { diagnostics, vulnerabilities, outdated: [] };
3111
- }
3112
- const findings = root.vulnerabilities?.found;
3113
- if (!Array.isArray(findings)) return { diagnostics, vulnerabilities, outdated: [] };
3114
- for (const finding of findings) {
3115
- const item = finding;
3116
- const name = item.package ?? "unknown";
3117
- const advisory = item.id ?? item.advisory?.id ?? "cargo-audit";
3118
- vulnerabilities.push({
3119
- package: name,
3120
- ...item.title ? { advisory: item.title } : { advisory },
3121
- severity: mapSeverity(item.severity),
3122
- ...item.patched_versions && item.patched_versions.length > 0 ? { fixedIn: item.patched_versions[0] } : {},
3123
- ...item.url?.short ? { url: item.url.short } : {}
3124
- });
3125
- diagnostics.push({
3126
- severity: mapSeverity(item.severity) === "critical" || mapSeverity(item.severity) === "high" ? "error" : "warning",
3127
- code: advisory,
3128
- message: item.title ?? `${name} reported by cargo-audit.`,
3129
- source: "cargo-audit"
3130
- });
3131
- }
3132
- return { diagnostics, vulnerabilities, outdated: [] };
3133
- }
3134
- function parseComposerAudit(text) {
3135
- const diagnostics = [];
3136
- const vulnerabilities = [];
3137
- const lines = text.split(/\r?\n/);
3138
- for (const line of lines) {
3139
- if (!line.trim().startsWith("{")) continue;
3140
- try {
3141
- const entry = JSON.parse(line);
3142
- if (!entry.package) continue;
3143
- vulnerabilities.push({
3144
- package: entry.package,
3145
- ...entry.advisory ? { advisory: entry.advisory } : {},
3146
- ...entry.title ? { advisory: entry.title } : { advisory: entry.advisory ?? "composer-audit" },
3147
- severity: mapSeverity(entry.severity),
3148
- ...entry.affectedVersions ? { fixedIn: entry.affectedVersions } : {},
3149
- ...entry.url ? { url: entry.url } : {}
3150
- });
3151
- diagnostics.push({
3152
- severity: "warning",
3153
- code: entry.advisory ?? "composer-audit",
3154
- message: entry.title ?? `${entry.package} reported by composer audit.`,
3155
- source: "composer-audit"
3156
- });
3157
- } catch {
3158
- }
3159
- }
3160
- return { diagnostics, vulnerabilities, outdated: [] };
3161
- }
3162
- function parseComposerOutdated(text) {
3163
- const diagnostics = [];
3164
- const outdated = [];
3165
- const lines = text.split(/\r?\n/);
3166
- for (const line of lines) {
3167
- if (!line.trim().startsWith("{")) continue;
3168
- try {
3169
- const entry = JSON.parse(line);
3170
- if (!entry.name || entry.version === entry.latest) continue;
3171
- outdated.push({
3172
- name: entry.name,
3173
- previous: entry.version,
3174
- resolved: entry.latest ?? entry.version
3175
- });
3176
- diagnostics.push({
3177
- severity: "info",
3178
- code: "outdated",
3179
- message: `${entry.name}: ${entry.version ?? "?"} \u2192 ${entry.latest ?? "?"}`,
3180
- source: "composer-outdated"
3181
- });
3182
- } catch {
3183
- }
3184
- }
3185
- return { diagnostics, vulnerabilities: [], outdated };
3186
- }
3187
- function parseDotnetPackage(text) {
3188
- const diagnostics = [];
3189
- const vulnerabilities = [];
3190
- const outdated = [];
3191
- let root;
3192
- try {
3193
- root = JSON.parse(text);
3194
- } catch {
3195
- return { diagnostics, vulnerabilities, outdated };
3196
- }
3197
- const projects = root.projects;
3198
- if (!Array.isArray(projects)) return { diagnostics, vulnerabilities, outdated };
3199
- for (const project of projects) {
3200
- const packages = Array.isArray(project.packages) ? project.packages : [];
3201
- for (const pkg of packages) {
3202
- const name = typeof pkg.name === "string" ? pkg.name : "unknown";
3203
- const requested = typeof pkg.requestedVersion === "string" ? pkg.requestedVersion : void 0;
3204
- const resolved = typeof pkg.resolvedVersion === "string" ? pkg.resolvedVersion : void 0;
3205
- const vulnerabilitiesRaw = Array.isArray(pkg.vulnerabilities) ? pkg.vulnerabilities : [];
3206
- for (const vuln of vulnerabilitiesRaw) {
3207
- const advisory = typeof vuln.advisoryUrl === "string" ? vuln.advisoryUrl : "dotnet-vulnerable";
3208
- vulnerabilities.push({
3209
- package: name,
3210
- advisory,
3211
- severity: mapSeverity(typeof vuln.severity === "string" ? vuln.severity : void 0)
3212
- });
3213
- }
3214
- if (vulnerabilitiesRaw.length > 0) {
3215
- diagnostics.push({
3216
- severity: "warning",
3217
- code: "dotnet-vulnerable",
3218
- message: `${name} has ${vulnerabilitiesRaw.length} known vulnerability entry/entries.`,
3219
- source: "dotnet-package"
3220
- });
3221
- }
3222
- if (requested && resolved && requested.startsWith(">") && requested.split(">")[1].split(".").slice(0, 2).join(".") !== resolved.split(".").slice(0, 2).join(".")) {
3223
- outdated.push({ name, requested, resolved });
3224
- diagnostics.push({
3225
- severity: "info",
3226
- code: "outdated",
3227
- message: `${name}: ${requested} \u2192 ${resolved}`,
3228
- source: "dotnet-package"
3229
- });
3230
- }
3231
- }
3232
- }
3233
- return { diagnostics, vulnerabilities, outdated };
3234
- }
3235
- function mapSeverity(value) {
3236
- switch (value?.toLowerCase()) {
3237
- case "critical":
3238
- case "high":
3239
- return "high";
3240
- case "medium":
3241
- case "moderate":
3242
- return "moderate";
3243
- case "low":
3244
- return "low";
3245
- case "unknown":
3246
- return "unknown";
3247
- default:
3248
- return "unknown";
3249
- }
3250
- }
3251
- function mapOutdatedKind(value) {
3252
- switch (value?.toLowerCase()) {
3253
- case "devdependencies":
3254
- case "development":
3255
- return "development";
3256
- case "optionaldependencies":
3257
- case "optional":
3258
- return "optional";
3259
- default:
3260
- return "runtime";
3261
- }
3262
- }
3263
- function parseLinePattern(text, regex, root, source, details) {
3264
- const diagnostics = [];
3265
- for (const match of text.matchAll(regex)) {
3266
- const parsed = details(match, match.at(-1)?.trim() ?? "Diagnostic");
3267
- diagnostics.push({
3268
- severity: parsed.severity,
3269
- message: parsed.message,
3270
- file: normalizeDiagnosticPath(match[1], root),
3271
- range: { start: { line: toPositiveInt(match[2]), column: toPositiveInt(match[3]) } },
3272
- source
3273
- });
3274
- }
3275
- return diagnostics;
3276
- }
3277
- function normalizeDiagnosticPath(value, root) {
3278
- const clean = value.trim().replace(/^['"]|['"]$/g, "");
3279
- return path5.resolve(root, clean);
3280
- }
3281
- function normalizeSeverity(value) {
3282
- if (value === "warning" || value === "warn") return "warning";
3283
- if (value === "info" || value === "note" || value === "help") return "info";
3284
- if (value === "hint") return "hint";
3285
- return "error";
3286
- }
3287
- function toPositiveInt(value) {
3288
- return Math.max(1, Number.parseInt(value ?? "1", 10) || 1);
3289
- }
3290
- function summarize(diagnostics) {
3291
- return {
3292
- errors: diagnostics.filter((item) => item.severity === "error").length,
3293
- warnings: diagnostics.filter((item) => item.severity === "warning").length,
3294
- infos: diagnostics.filter((item) => item.severity === "info" || item.severity === "hint").length
3295
- };
3296
- }
3297
- function dedupeDiagnostics(items) {
3298
- const seen = /* @__PURE__ */ new Set();
3299
- return items.filter((item) => {
3300
- const key = [
3301
- item.source,
3302
- item.code ?? "",
3303
- item.file ?? "",
3304
- item.range?.start.line ?? 0,
3305
- item.range?.start.column ?? 0,
3306
- item.message
3307
- ].join("\0");
3308
- if (seen.has(key)) return false;
3309
- seen.add(key);
3310
- return true;
3311
- });
3312
- }
3313
- function compareDiagnostics(a, b) {
3314
- return (a.file ?? "").localeCompare(b.file ?? "") || (a.range?.start.line ?? 0) - (b.range?.start.line ?? 0) || (a.range?.start.column ?? 0) - (b.range?.start.column ?? 0) || severityRank(a.severity) - severityRank(b.severity) || (a.code ?? "").localeCompare(b.code ?? "") || a.message.localeCompare(b.message);
3315
- }
3316
- function severityRank(value) {
3317
- return value === "error" ? 0 : value === "warning" ? 1 : value === "info" ? 2 : 3;
3318
- }
3319
-
3320
- // src/languages/execute.ts
3321
- import * as fs4 from "node:fs/promises";
3322
- import * as path7 from "node:path";
3323
-
3324
- // src/languages/plan.ts
3325
- import * as fs3 from "node:fs/promises";
3326
- import * as path6 from "node:path";
3327
- var MAX_ARGUMENTS = 128;
3328
- var MAX_ARGUMENT_LENGTH = 4096;
3329
- var PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
3330
- var COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
3331
- var GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
3332
- var PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
3333
- async function planLanguageOperation(options) {
3334
- validateOperationOptions(options.operation, options.operationOptions?.packages);
3335
- validateFilterOption(options.operationOptions?.filter);
3336
- const detection = await detectLanguageWorkspaces(options);
3337
- const candidates = detection.workspaces.filter(
3338
- (workspace) => workspace.capabilities.includes(options.operation)
3339
- );
3340
- const selected = selectWorkspace(candidates, options);
3341
- if (selected.status !== "selected") return selected.result;
3342
- const profile = (options.profiles ?? languageProfileRegistry.list()).find(
3343
- (item) => item.id === selected.workspace.language
3344
- );
3345
- if (!profile) {
3346
- return {
3347
- status: "not_found",
3348
- reason: `Profile ${selected.workspace.language} is unavailable.`,
3349
- candidates
3350
- };
3351
- }
3352
- if ((profile.id === "typescript" || profile.id === "javascript") && options.operation !== "syntax" && selected.workspace.packageManager === void 0 && new Set(
3353
- selected.workspace.evidence.filter((evidence) => evidence.kind === "lockfile").map((evidence) => evidence.value.toLowerCase())
3354
- ).size > 1) {
3355
- return {
3356
- status: "unavailable",
3357
- workspace: selected.workspace,
3358
- unavailable: {
3359
- status: "unavailable",
3360
- profileId: profile.id,
3361
- workspaceId: selected.workspace.id,
3362
- operation: options.operation,
3363
- reason: "Conflicting Node lockfiles make the package manager ambiguous."
3364
- }
3365
- };
3366
- }
3367
- const resolver = profile.operations[options.operation];
3368
- if (!resolver) {
3369
- return {
3370
- status: "unavailable",
3371
- workspace: selected.workspace,
3372
- unavailable: {
3373
- status: "unavailable",
3374
- profileId: profile.id,
3375
- workspaceId: selected.workspace.id,
3376
- operation: options.operation,
3377
- reason: `${profile.displayName} does not define ${options.operation}.`
3378
- }
3379
- };
3380
- }
3381
- const canonicalCwd = options.cwd ? path6.isAbsolute(options.cwd) ? options.cwd : path6.resolve(detection.projectRoot, options.cwd) : detection.projectRoot;
3382
- const target = options.target ? await canonicalTarget(canonicalCwd, options.target, detection.projectRoot) : void 0;
3383
- const ctx = {
3384
- projectRoot: detection.projectRoot,
3385
- workspace: selected.workspace,
3386
- ...target ? { target } : {},
3387
- mode: options.mode ?? "standard",
3388
- options: options.operationOptions ?? {},
3389
- platform: process.platform,
3390
- pathExists: async (candidate) => {
3391
- try {
3392
- await fs3.access(path6.resolve(detection.projectRoot, candidate));
3393
- return true;
3394
- } catch {
3395
- return false;
3396
- }
3397
- }
3398
- };
3399
- const result = await resolver(ctx);
3400
- if ("status" in result) {
3401
- return { status: "unavailable", workspace: selected.workspace, unavailable: result };
3402
- }
3403
- const errors = validateCommandPlan(result, profile, detection.projectRoot);
3404
- if (errors.length > 0) {
3405
- return {
3406
- status: "unavailable",
3407
- workspace: selected.workspace,
3408
- unavailable: {
3409
- status: "unavailable",
3410
- profileId: profile.id,
3411
- workspaceId: selected.workspace.id,
3412
- operation: options.operation,
3413
- reason: `Generated plan failed validation: ${errors.join("; ")}`
3414
- }
3415
- };
3416
- }
3417
- return { status: "planned", workspace: selected.workspace, plan: Object.freeze(result) };
3418
- }
3419
- function validateCommandPlan(plan, profile, projectRoot) {
3420
- const errors = [];
3421
- if (plan.profileId !== profile.id) errors.push("profile id does not match");
3422
- if (!isInside2(plan.cwd, projectRoot)) errors.push("cwd is outside project root");
3423
- if (plan.args.length > MAX_ARGUMENTS) errors.push(`argument count exceeds ${MAX_ARGUMENTS}`);
3424
- if (plan.args.some((arg) => arg.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(arg))) {
3425
- errors.push("arguments contain an invalid or oversized value");
3426
- }
3427
- if (!Number.isFinite(plan.timeoutMs) || plan.timeoutMs < 1 || plan.timeoutMs > 6e5) {
3428
- errors.push("timeout is outside 1..600000ms");
3429
- }
3430
- if (!Number.isFinite(plan.outputLimitBytes) || plan.outputLimitBytes < 1 || plan.outputLimitBytes > 1e6) {
3431
- errors.push("output limit is outside 1..1000000 bytes");
3432
- }
3433
- if (plan.kind === "internal") {
3434
- if (plan.command !== null || plan.args.length !== 0)
3435
- errors.push("internal plans cannot declare a command");
3436
- } else {
3437
- if (!plan.command || !profile.executables.includes(plan.command)) {
3438
- errors.push(`executable "${plan.command ?? ""}" is not allowlisted by the profile`);
3439
- }
3440
- }
3441
- if (Object.keys(plan.env).length > 0)
3442
- errors.push("Phase 1 plans cannot override environment variables");
3443
- return errors;
3444
- }
3445
- function selectWorkspace(candidates, options) {
3446
- if (candidates.length === 0) {
3447
- return {
3448
- status: "result",
3449
- result: {
3450
- status: "not_found",
3451
- reason: `No workspace supports ${options.operation}.`,
3452
- candidates: []
3453
- }
3454
- };
3455
- }
3456
- if (options.workspace) {
3457
- const requested = path6.isAbsolute(options.workspace) ? path6.resolve(options.workspace) : path6.resolve(options.projectRoot, options.workspace);
3458
- const matches = candidates.filter(
3459
- (item) => item.id === options.workspace || path6.resolve(item.root) === requested
3460
- );
3461
- if (matches.length === 1) return { status: "selected", workspace: matches[0] };
3462
- return {
3463
- status: "result",
3464
- result: {
3465
- status: "not_found",
3466
- reason: `Requested workspace "${options.workspace}" was not detected.`,
3467
- candidates: [...candidates]
3468
- }
3469
- };
3470
- }
3471
- if (options.target) {
3472
- const base = options.cwd ? path6.isAbsolute(options.cwd) ? path6.resolve(options.cwd) : path6.resolve(options.projectRoot, options.cwd) : path6.resolve(options.projectRoot);
3473
- const target = path6.isAbsolute(options.target) ? path6.resolve(options.target) : path6.resolve(base, options.target);
3474
- const targeted = candidates.filter((item) => item.evidence.some((evidence) => evidence.kind === "target")).sort(
3475
- (a, b) => workspaceDepth(b.root, options.projectRoot) - workspaceDepth(a.root, options.projectRoot) || compareCandidates(a, b)
3476
- );
3477
- if (targeted[0]) return { status: "selected", workspace: targeted[0] };
3478
- const containing = candidates.filter((item) => isInside2(target, item.root)).sort(
3479
- (a, b) => workspaceDepth(b.root, options.projectRoot) - workspaceDepth(a.root, options.projectRoot) || compareCandidates(a, b)
3480
- );
3481
- if (containing[0]) return { status: "selected", workspace: containing[0] };
3482
- }
3483
- const sorted = [...candidates].sort(compareCandidates);
3484
- const first = sorted[0];
3485
- const firstDepth = workspaceDepth(first.root, options.projectRoot);
3486
- const tied = sorted.filter(
3487
- (item) => item.confidence === first.confidence && workspaceDepth(item.root, options.projectRoot) === firstDepth
3488
- );
3489
- if (tied.length > 1) {
3490
- return {
3491
- status: "result",
3492
- result: {
3493
- status: "ambiguous",
3494
- reason: "Multiple workspaces have equal confidence; provide target, language, or workspace.",
3495
- candidates: tied
3496
- }
3497
- };
3498
- }
3499
- return { status: "selected", workspace: first };
3500
- }
3501
- function compareCandidates(a, b) {
3502
- return b.confidence - a.confidence || a.language.localeCompare(b.language) || a.root.localeCompare(b.root);
3503
- }
3504
- function workspaceDepth(workspaceRoot, projectRoot) {
3505
- const relative5 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3506
- return relative5 === "" ? 0 : relative5.split(path6.sep).length;
3507
- }
3508
- function validateFilterOption(filter) {
3509
- if (filter === void 0) return;
3510
- if (filter.startsWith("-") || filter.includes(" --")) {
3511
- throw new Error(`Invalid test filter (parsed as a runner option): "${filter}"`);
3512
- }
3513
- if (filter.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(filter)) {
3514
- throw new Error("Invalid test filter (oversized or contains a control character)");
3515
- }
3516
- }
3517
- function validateOperationOptions(operation, packages) {
3518
- if (!operation.startsWith("package-") || !packages) return;
3519
- for (const value of packages) {
3520
- if (!value || value.length > 214 || value.startsWith("-") || /[\r\n\0;&|`$<>\\]/.test(value)) {
3521
- throw new Error(`Invalid package identifier "${value}"`);
3522
- }
3523
- if (value.startsWith(".") || value.startsWith("/") || /^[A-Za-z]:/.test(value) || value.includes("://")) {
3524
- throw new Error(`Package paths and URLs are not supported: "${value}"`);
3525
- }
3526
- if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value) && !PYTHON_PACKAGE_RE.test(value)) {
3527
- throw new Error(`Invalid package identifier "${value}"`);
3528
- }
3529
- }
3530
- }
3531
- async function canonicalTarget(cwd, target, projectRoot) {
3532
- const resolved = path6.isAbsolute(target) ? path6.resolve(target) : path6.resolve(cwd, target);
3533
- let real;
3534
- try {
3535
- real = await fs3.realpath(resolved);
3536
- } catch (error) {
3537
- if (error.code !== "ENOENT") throw error;
3538
- const parent = await fs3.realpath(path6.dirname(resolved));
3539
- real = path6.join(parent, path6.basename(resolved));
3540
- }
3541
- if (!isInside2(real, projectRoot)) throw new Error(`target is outside project root: ${target}`);
3542
- return real;
3543
- }
3544
- function isInside2(candidate, root) {
3545
- const relative5 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3546
- return relative5 === "" || !relative5.startsWith("..") && !path6.isAbsolute(relative5);
3547
- }
3548
-
3549
- // src/languages/execute.ts
3550
- async function validatePlanRealpaths(plan, projectRoot) {
3551
- const realRoot = await fs4.realpath(projectRoot);
3552
- let realCwd;
3553
- try {
3554
- realCwd = await fs4.realpath(plan.cwd);
3555
- } catch (error) {
3556
- return `Plan cwd is unavailable: ${error instanceof Error ? error.message : String(error)}`;
3557
- }
3558
- if (!isRealInside(realCwd, realRoot)) return "Plan cwd resolves outside project root.";
3559
- for (const argument of plan.args) {
3560
- if (!path7.isAbsolute(argument)) continue;
3561
- try {
3562
- const realArgument = await fs4.realpath(argument);
3563
- if (!isRealInside(realArgument, realRoot)) {
3564
- return `Plan argument resolves outside project root: ${argument}`;
3565
- }
3566
- } catch (error) {
3567
- if (error.code !== "ENOENT") {
3568
- return `Plan argument could not be resolved safely: ${argument}`;
3569
- }
3570
- }
3571
- }
3572
- return void 0;
3573
- }
3574
- function isRealInside(candidate, root) {
3575
- const relative5 = path7.relative(root, candidate);
3576
- return relative5 === "" || !relative5.startsWith("..") && !path7.isAbsolute(relative5);
3577
- }
3578
- async function* executePackagePlan(options) {
3579
- const { plan, workspace } = options;
3580
- const startedAt = Date.now();
3581
- if (options.signal.aborted) {
3582
- yield { type: "log", text: "Run cancelled before execution." };
3583
- return terminalPackageResult(options, "cancelled", startedAt);
3584
- }
3585
- const profile = languageProfileRegistry.get(plan.profileId);
3586
- if (!profile) {
3587
- yield { type: "warning", text: `Unknown language profile: ${plan.profileId}` };
3588
- return terminalPackageResult(options, "unavailable", startedAt, "Unknown language profile.");
3589
- }
3590
- if (workspace.id !== plan.workspaceId || workspace.language !== plan.profileId) {
3591
- return terminalPackageResult(
3592
- options,
3593
- "unavailable",
3594
- startedAt,
3595
- "Plan and workspace identity do not match."
3596
- );
3597
- }
3598
- const validation = validateCommandPlan(plan, profile, options.projectRoot);
3599
- if (validation.length > 0) {
3600
- return terminalPackageResult(
3601
- options,
3602
- "unavailable",
3603
- startedAt,
3604
- `Plan failed execution validation: ${validation.join("; ")}`
3605
- );
3606
- }
3607
- const containmentError = await validatePlanRealpaths(plan, options.projectRoot);
3608
- if (containmentError) {
3609
- return terminalPackageResult(options, "unavailable", startedAt, containmentError);
3610
- }
3611
- const manifestsBefore = await snapshotPaths(workspace.manifests);
3612
- const lockfilePaths = collectLockfilePaths(workspace);
3613
- const lockfilesBefore = await snapshotPaths(lockfilePaths);
3614
- const manifestSizesBefore = await snapshotSizes(workspace.manifests);
3615
- const lockfileSizesBefore = await snapshotSizes(lockfilePaths);
3616
- yield {
3617
- type: "log",
3618
- text: plan.kind === "process" ? `${plan.command} ${plan.args.join(" ")}` : `${plan.parser}: ${plan.operation}`,
3619
- data: {
3620
- language: plan.profileId,
3621
- operation: plan.operation,
3622
- workspace: workspace.root,
3623
- packages: [...options.packages]
3624
- }
3625
- };
3626
- const timeoutController = new AbortController();
3627
- const timer = setTimeout(
3628
- () => timeoutController.abort(new Error("language package plan timed out")),
3629
- plan.timeoutMs
3630
- );
3631
- timer.unref?.();
3632
- const signal = AbortSignal.any([options.signal, timeoutController.signal]);
3633
- let spawned;
3634
- let run;
3635
- let status = "unavailable";
3636
- let error;
3637
- try {
3638
- if (plan.kind !== "process") {
3639
- return terminalPackageResult(
3640
- options,
3641
- "unavailable",
3642
- startedAt,
3643
- "Package operations require an executable plan; internal plans cannot mutate the manifest."
3644
- );
3645
- }
3646
- const stream = spawnStream({
3647
- cmd: plan.command,
3648
- args: [...plan.args],
3649
- cwd: plan.cwd,
3650
- signal,
3651
- maxBytes: plan.outputLimitBytes
3652
- });
3653
- for (; ; ) {
3654
- const next = await stream.next();
3655
- if (next.done) {
3656
- spawned = next.value;
3657
- break;
3658
- }
3659
- yield next.value;
3660
- }
3661
- } catch (error_) {
3662
- error = error_ instanceof Error ? error_.message : String(error_);
3663
- } finally {
3664
- clearTimeout(timer);
3665
- }
3666
- if (spawned) {
3667
- const parsed = parseLanguageDiagnostics(
3668
- plan.parser,
3669
- spawned.stdout,
3670
- spawned.stderr,
3671
- workspace.root
3672
- );
3673
- const timedOut = timeoutController.signal.aborted && !options.signal.aborted;
3674
- const cancelled = options.signal.aborted;
3675
- const spawnUnavailable = Boolean(
3676
- spawned.error && /ENOENT|not found|cannot find/i.test(spawned.error)
3677
- );
3678
- status = timedOut ? "timed_out" : cancelled ? "cancelled" : spawnUnavailable ? "unavailable" : spawned.exitCode === 0 ? "passed" : "failed";
3679
- error ??= spawned.error;
3680
- const raw = [spawned.stdout, spawned.stderr, spawned.error].filter(Boolean).join("\n");
3681
- run = {
3682
- status,
3683
- language: plan.profileId,
3684
- workspace,
3685
- plan,
3686
- exitCode: spawned.exitCode,
3687
- durationMs: Date.now() - startedAt,
3688
- diagnostics: parsed.diagnostics,
3689
- omittedDiagnostics: parsed.omitted,
3690
- summary: parsed.summary,
3691
- output: normalizeCommandOutput(raw, { maxBytes: plan.outputLimitBytes }),
3692
- truncated: spawned.truncated,
3693
- ...spawned.spoolPath ? { spoolPath: spawned.spoolPath } : {},
3694
- ...spawned.error ? { error: spawned.error } : {}
3695
- };
3696
- } else {
3697
- status = options.signal.aborted ? "cancelled" : timeoutController.signal.aborted ? "timed_out" : "failed";
3698
- }
3699
- if (status !== "passed") {
3700
- return terminalPackageResult(
3701
- options,
3702
- status,
3703
- startedAt,
3704
- error,
3705
- run,
3706
- manifestsBefore,
3707
- lockfilesBefore
3708
- );
3709
- }
3710
- const manifestsAfter = await snapshotPaths(workspace.manifests);
3711
- const lockfilesAfter = await snapshotPaths(lockfilePaths);
3712
- const manifestSizesAfter = await snapshotSizes(workspace.manifests);
3713
- const lockfileSizesAfter = await snapshotSizes(lockfilePaths);
3714
- const manifestsChanged = await changedPaths(
3715
- manifestsBefore,
3716
- manifestsAfter,
3717
- manifestSizesBefore,
3718
- manifestSizesAfter
3719
- );
3720
- const lockfilesChanged = await changedPaths(
3721
- lockfilesBefore,
3722
- lockfilesAfter,
3723
- lockfileSizesBefore,
3724
- lockfileSizesAfter
3725
- );
3726
- const reports = parsePackageReports(plan.parser, spawned?.stdout ?? "", spawned?.stderr ?? "");
3727
- const mutations = packageMutationsFromInputs(options.packages, reports.outdated);
3728
- return {
3729
- workspace,
3730
- language: plan.profileId,
3731
- operation: plan.operation,
3732
- status,
3733
- ...run ? { run } : {},
3734
- durationMs: Date.now() - startedAt,
3735
- manifestsBefore,
3736
- lockfilesBefore,
3737
- manifestsAfter,
3738
- lockfilesAfter,
3739
- mutations,
3740
- vulnerabilities: reports.vulnerabilities,
3741
- outdated: reports.outdated,
3742
- manifestsChanged: Object.freeze(manifestsChanged),
3743
- lockfilesChanged: Object.freeze(lockfilesChanged)
3744
- };
3745
- }
3746
- function packageMutationsFromInputs(inputs, outdated) {
3747
- const outdatedNames = new Set(outdated.map((entry) => entry.name));
3748
- return inputs.filter((name) => !outdatedNames.has(name)).map((name) => ({ name, requested: name, kind: "runtime" }));
3749
- }
3750
- async function snapshotPaths(paths) {
3751
- const existing = [];
3752
- for (const candidate of paths) {
3753
- try {
3754
- const stat5 = await fs4.stat(candidate);
3755
- if (!stat5.isFile()) continue;
3756
- existing.push(candidate);
3757
- } catch {
3758
- }
3759
- }
3760
- return Object.freeze(existing);
3761
- }
3762
- async function changedPaths(before, after, beforeSizes, afterSizes) {
3763
- const beforeSet = new Set(before);
3764
- const afterSet = new Set(after);
3765
- const changed = /* @__PURE__ */ new Set();
3766
- for (const path9 of after) {
3767
- if (!beforeSet.has(path9)) changed.add(path9);
3768
- }
3769
- for (const path9 of before) {
3770
- if (!afterSet.has(path9)) changed.add(path9);
3771
- }
3772
- if (beforeSizes && afterSizes) {
3773
- for (const path9 of after) {
3774
- if (beforeSizes.get(path9) !== afterSizes.get(path9)) changed.add(path9);
3775
- }
3776
- }
3777
- return [...changed].sort();
3778
- }
3779
- async function snapshotSizes(paths) {
3780
- const sizes = /* @__PURE__ */ new Map();
3781
- for (const candidate of paths) {
3782
- try {
3783
- const stat5 = await fs4.stat(candidate);
3784
- if (stat5.isFile()) sizes.set(candidate, stat5.size);
3785
- } catch {
3786
- }
3787
- }
3788
- return sizes;
3789
- }
3790
- function collectLockfilePaths(workspace) {
3791
- const explicit = workspace.evidence.filter((evidence) => evidence.kind === "lockfile").map((evidence) => evidence.path);
3792
- const detected = LOCKFILE_NAMES_BY_PROFILE.get(workspace.language)?.map(
3793
- (name) => path7.join(workspace.root, name)
3794
- ) ?? [];
3795
- const merged = /* @__PURE__ */ new Set();
3796
- for (const candidate of [...explicit, ...detected]) merged.add(candidate);
3797
- return Object.freeze([...merged]);
3798
- }
3799
- var LOCKFILE_NAMES_BY_PROFILE = /* @__PURE__ */ new Map([
3800
- ["typescript", ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"]],
3801
- ["javascript", ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"]],
3802
- ["go", ["go.sum"]],
3803
- ["rust", ["Cargo.lock"]],
3804
- ["php", ["composer.lock"]],
3805
- ["csharp", ["packages.lock.json"]]
3806
- ]);
3807
- function terminalPackageResult(options, status, startedAt, error, run, manifestsBefore = [], lockfilesBefore = []) {
3808
- return {
3809
- workspace: options.workspace,
3810
- language: options.plan.profileId,
3811
- operation: options.plan.operation,
3812
- status,
3813
- ...run ? { run } : {},
3814
- durationMs: Date.now() - startedAt,
3815
- manifestsBefore,
3816
- lockfilesBefore,
3817
- manifestsAfter: manifestsBefore,
3818
- lockfilesAfter: lockfilesBefore,
3819
- manifestsChanged: Object.freeze([]),
3820
- lockfilesChanged: Object.freeze([]),
3821
- mutations: Object.freeze([]),
3822
- vulnerabilities: Object.freeze([]),
3823
- outdated: Object.freeze([]),
3824
- ...error ? { error } : {}
3825
- };
3826
- }
3827
-
3828
- // src/languages/legacy-bridge.ts
3829
- var NON_JS_MARKERS = [
3830
- { filename: "go.mod", language: "go" },
3831
- { filename: "go.work", language: "go" },
3832
- { filename: "Cargo.toml", language: "rust" },
3833
- { filename: "composer.json", language: "php" }
3834
- ];
3835
- var NON_JS_SUFFIXES = [
3836
- { suffix: ".csproj", language: "csharp" },
3837
- { suffix: ".fsproj", language: "csharp" }
3838
- ];
3839
- async function detectNonJsEcosystem(cwd, projectRoot) {
3840
- try {
3841
- let dir = path8.resolve(cwd);
3842
- const root = path8.resolve(projectRoot);
3843
- for (let depth = 0; depth <= 4 && dir.startsWith(root); depth++) {
3844
- for (const marker of NON_JS_MARKERS) {
3845
- try {
3846
- const s = await fs5.stat(path8.join(dir, marker.filename));
3847
- if (s.isFile()) return marker.language;
3848
- } catch {
3849
- }
3850
- }
3851
- try {
3852
- const entries = await fs5.readdir(dir);
3853
- for (const entry of entries) {
3854
- for (const suffix of NON_JS_SUFFIXES) {
3855
- if (entry.toLowerCase().endsWith(suffix.suffix)) return suffix.language;
3856
- }
3857
- }
3858
- } catch {
3859
- }
3860
- if (dir === root) break;
3861
- dir = path8.dirname(dir);
3862
- }
3863
- } catch {
3864
- }
3865
- return null;
3866
- }
3867
- async function tryLegacyPackageOperation(operation, ctx, packages = []) {
3868
- const language = await detectNonJsEcosystem(ctx.cwd, ctx.projectRoot);
3869
- if (!language) return null;
3870
- const planResult = await planLanguageOperation({
3871
- projectRoot: ctx.projectRoot,
3872
- cwd: ctx.cwd,
3873
- operation,
3874
- language,
3875
- signal: ctx.signal,
3876
- ...packages.length > 0 ? { operationOptions: { packages: [...packages] } } : {}
3877
- });
3878
- if (planResult.status !== "planned") return null;
3879
- const runner = executePackagePlan({
3880
- projectRoot: ctx.projectRoot,
3881
- workspace: planResult.workspace,
3882
- plan: planResult.plan,
3883
- packages,
3884
- signal: ctx.signal
3885
- });
3886
- for (; ; ) {
3887
- const next = await runner.next();
3888
- if (next.done) return { language, outcome: next.value };
3889
- }
3890
- }
3891
-
3892
- // src/install.ts
3893
- var installTool = {
3894
- name: "install",
3895
- category: "Package Management",
3896
- description: "Install, update or manage packages using the detected package manager (pnpm/npm/yarn). Strongly preferred over raw shell commands for dependency management because it is structured and safer.",
3897
- usageHint: "ALWAYS USE THIS INSTEAD OF BASH FOR PACKAGE WORK:\n\n- Empty `packages` \u2192 normal `install` (respects lockfile).\n- Provide names \u2192 adds/updates specific packages.\n- `dry_run: true` for safe preview.\n- Set `save` appropriately.\nThis tool has proper capability declaration and is heavily recommended in the security posture of the project.",
3898
- permission: "confirm",
3899
- // WS-046: gives permission decisions something to key on.
3900
- // What gets installed is the subject — installing a package runs its
3901
- // lifecycle scripts, so `express` and `evil-pkg` must not be interchangeable.
3902
- subjectKey: "packages",
3903
- mutating: true,
3904
- riskTier: "standard",
3905
- icon: "package",
3906
- timeoutMs: 12e4,
3907
- capabilities: ["package.install", "shell.restricted"],
3908
- inputSchema: {
3909
- type: "object",
3910
- properties: {
3911
- packages: {
3912
- type: "string",
3913
- description: "Package(s) to install: single name, comma-separated list, or empty for all deps"
3914
- },
3915
- save: {
3916
- type: "string",
3917
- enum: ["dependency", "dev", "optional"],
3918
- description: 'Where to save the package(s): "dependency", "devDependencies", or "optionalDependencies".'
3919
- },
3920
- cwd: {
3921
- type: "string",
3922
- description: "Working directory for the install command (must stay inside project)."
3923
- },
3924
- dry_run: {
3925
- type: "boolean",
3926
- description: "If true, show what would be installed without actually modifying package.json or node_modules."
3927
- },
3928
- global: {
3929
- type: "boolean",
3930
- description: "Whether to perform a global install (use with caution)."
3931
- },
3932
- lifecycleScripts: {
3933
- type: "boolean",
3934
- description: "Opt in to running package lifecycle scripts (preinstall / install / postinstall / prepare / \u2026). Default: false \u2014 installs pass --ignore-scripts so a malicious package cannot execute arbitrary code at install time. Set true to opt back in to the legacy npm/pnpm/yarn default."
3935
- }
3936
- }
3937
- },
3938
- async execute(input, ctx, opts) {
3939
- let final;
3940
- const executeStream = installTool.executeStream;
3941
- if (!executeStream) throw new Error("installTool: stream execution unavailable");
3942
- for await (const ev of executeStream(input, ctx, opts)) {
3943
- if (ev.type === "final") final = ev.output;
3944
- }
3945
- if (!final) throw new Error("install: stream ended without final event");
3946
- return final;
3947
- },
3948
- async *executeStream(input, ctx, opts) {
3949
- const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
3950
- if (!input.global) {
3951
- const pkgList2 = input.packages ? Array.isArray(input.packages) ? input.packages : input.packages.split(",").map((p) => p.trim()) : [];
3952
- const bridge = await tryLegacyPackageOperation(
3953
- "package-install",
3954
- {
3955
- cwd,
3956
- projectRoot: ctx.projectRoot,
3957
- signal: opts.signal
3958
- },
3959
- pkgList2
3960
- );
3961
- if (bridge?.outcome) {
3962
- const outcome = bridge.outcome;
3963
- const run = outcome.run;
3964
- yield {
3965
- type: "final",
3966
- output: {
3967
- exit_code: run?.exitCode ?? 0,
3968
- packages: [],
3969
- output: normalizeCommandOutput(
3970
- run?.output || outcome.manifestsChanged.join(", ") || "ok"
3971
- ),
3972
- truncated: run?.truncated ?? false,
3973
- dry_run: input.dry_run ?? false
3974
- }
3975
- };
3976
- return;
3977
- }
3978
- }
3979
- const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
3980
- yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
3981
- const globalFlag = input.global ? ["-g"] : [];
3982
- const ignoreScripts = input.lifecycleScripts !== true;
3983
- const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
3984
- (p) => p.trim()
3985
- ) : [];
3986
- const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9^~><=*.+-]+)?$/i;
3987
- for (const pkg of pkgList) {
3988
- if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
3989
- yield {
3990
- type: "final",
3991
- output: {
3992
- packages: pkgList,
3993
- exit_code: 1,
3994
- output: `Invalid package name "${pkg}". Names must match ${PKG_NAME_RE} and not start with "-".`,
3995
- dry_run: Boolean(input.dry_run),
3996
- truncated: false
3997
- }
3998
- };
3999
- return;
4000
- }
4001
- }
4002
- const hasPkgs = pkgList.length > 0;
4003
- const args = [];
4004
- if (input.dry_run) args.push("--dry-run");
4005
- if (ignoreScripts) args.push("--ignore-scripts");
4006
- if (pkgManager === "pnpm") {
4007
- if (hasPkgs) {
4008
- if (input.save === "dev") args.push("-D");
4009
- else if (input.save === "optional") args.push("-O");
4010
- args.push("add", ...globalFlag);
4011
- } else {
4012
- args.push("install", ...globalFlag);
4013
- }
4014
- } else if (pkgManager === "yarn") {
4015
- if (hasPkgs) {
4016
- args.push("add", ...globalFlag);
4017
- if (input.save === "dev") args.push("--dev");
4018
- else if (input.save === "optional") args.push("--optional");
4019
- } else {
4020
- args.push("install", ...globalFlag);
4021
- }
4022
- } else {
4023
- args.push("install", ...globalFlag);
4024
- if (hasPkgs) {
4025
- if (input.save === "dev") args.push("--save-dev");
4026
- else if (input.save === "optional") args.push("--save-optional");
4027
- }
4028
- }
4029
- if (hasPkgs) args.push(...pkgList);
4030
- yield {
4031
- type: "log",
4032
- text: `Fetching ${pkgList.length || "all"} packages\u2026`,
4033
- data: { phase: "fetch" }
4034
- };
4035
- const result = yield* spawnStream({
4036
- cmd: pkgManager,
4037
- args,
4038
- cwd,
4039
- signal: opts.signal,
4040
- maxBytes: 1e5
4041
- });
4042
- const output = {
4043
- packages: pkgList,
4044
- exit_code: result.exitCode,
4045
- output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
4046
- dry_run: args.includes("--dry-run"),
4047
- truncated: result.truncated
4048
- };
4049
- const isSuccess = result.exitCode === 0 && !output.dry_run && !input.global;
4050
- if (isSuccess && pkgList.length > 0) {
4051
- const trackerOpts = ctx.meta?.["packageTrackerOpts"];
4052
- if (trackerOpts) {
4053
- const manifestPath = resolveManifestPath(cwd, pkgManager);
4054
- for (const pkg of pkgList) {
4055
- try {
4056
- await recordPackageAction(trackerOpts, {
4057
- manifestPath,
4058
- packageName: pkg,
4059
- versionSpec: "latest",
4060
- // exact version resolved by package manager at install time
4061
- ecosystem: detectPackageEcosystem(manifestPath),
4062
- agentId: ctx.agentId,
4063
- agentName: ctx.agentName,
4064
- sessionId: ctx.session.id
4065
- });
4066
- } catch {
4067
- }
4068
- }
4069
- }
4070
- }
4071
- ctx.recordSideEffect?.({
4072
- toolUseId: `install-${Date.now()}`,
4073
- toolName: "install",
4074
- ts: (/* @__PURE__ */ new Date()).toISOString(),
4075
- input: { packages: pkgList, cwd, dry_run: Boolean(input.dry_run) },
4076
- outcome: output.dry_run ? "dry run" : result.exitCode === 0 ? `installed ${pkgList.length || "all"} packages` : `failed (exit ${result.exitCode})`,
4077
- risk: "package"
4078
- });
4079
- yield { type: "final", output };
4080
- }
4081
- };
4082
- function resolveManifestPath(cwd, pkgManager) {
4083
- switch (pkgManager) {
4084
- case "pnpm":
4085
- case "yarn":
4086
- case "npm":
4087
- return join8(cwd, "package.json");
4088
- /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
4089
- default:
4090
- return join8(cwd, "package.json");
4091
- }
4092
- }
2
+ installTool
3
+ } from "./chunk-H5OHH6F4.js";
4
+ import "./chunk-NHA6BEIH.js";
5
+ import "./chunk-DPL47QGS.js";
6
+ import "./chunk-MAN26PZ5.js";
7
+ import "./chunk-2TFQDEPU.js";
8
+ import "./chunk-3PVYQFRQ.js";
9
+ import "./chunk-HFXSNKCV.js";
10
+ import "./chunk-V43EQ6VR.js";
11
+ import "./chunk-D33S4RQG.js";
4093
12
  export {
4094
13
  installTool
4095
14
  };