@peterxiaoyang/superspec 0.1.7 → 0.1.8

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.
@@ -0,0 +1,2624 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
4
+ import { GuardError, isObject, reason, renderList, safe_within, sha256_text, toPosix } from "../util.js";
5
+ export function readHookEventRef(eventRef) {
6
+ if (!eventRef)
7
+ throw new GuardError("hook_event_ref_missing");
8
+ const text = readFileSync(eventRef, "utf8");
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(text);
12
+ }
13
+ catch (err) {
14
+ throw new GuardError(`hook_event_unparsable: ${err.message}`);
15
+ }
16
+ if (!isObject(parsed))
17
+ throw new GuardError("hook_event_unparsable: event must be a JSON object");
18
+ return parsed;
19
+ }
20
+ function stableEventDigest(event) {
21
+ const explicit = String(event.hook_event_id ?? event.tool_use_id ?? event.agent_id ?? "");
22
+ const payload = explicit ? `${explicit}\n${JSON.stringify(event)}` : JSON.stringify(event);
23
+ return sha256_text(payload);
24
+ }
25
+ export function normalizeHookEvent(event) {
26
+ const hookEventName = typeof event.hook_event_name === "string" && event.hook_event_name
27
+ ? event.hook_event_name
28
+ : "Unknown";
29
+ const toolInput = isObject(event.tool_input) ? event.tool_input : {};
30
+ return {
31
+ event,
32
+ hook_event_id: stableEventDigest(event),
33
+ hook_event_name: hookEventName,
34
+ session_id: typeof event.session_id === "string" && event.session_id ? event.session_id : "unknown-session",
35
+ cwd: typeof event.cwd === "string" && event.cwd ? event.cwd : process.cwd(),
36
+ tool_name: typeof event.tool_name === "string" && event.tool_name ? event.tool_name : "",
37
+ command: typeof toolInput.command === "string" ? toolInput.command : "",
38
+ };
39
+ }
40
+ export function cleanRelPath(pathValue) {
41
+ return toPosix(pathValue).replace(/^\.\/+/u, "").replace(/\/+$/u, "").toLowerCase();
42
+ }
43
+ export function pathsIntersect(a, b) {
44
+ const left = cleanRelPath(a);
45
+ const right = cleanRelPath(b);
46
+ if (!left || left === "." || !right || right === ".")
47
+ return true;
48
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
49
+ }
50
+ function pathAtOrUnder(pathValue, root) {
51
+ const path = cleanRelPath(pathValue);
52
+ const normalizedRoot = cleanRelPath(root);
53
+ return path === normalizedRoot || path.startsWith(`${normalizedRoot}/`);
54
+ }
55
+ export function isSuperSpecTrustRootPath(pathValue, opts = {}) {
56
+ const rel = cleanRelPath(pathValue);
57
+ const commonRoots = [
58
+ ".codex/superspec",
59
+ ".superspec",
60
+ ];
61
+ if (commonRoots.some((root) => pathAtOrUnder(rel, root)))
62
+ return true;
63
+ if (opts.changeRootRel && pathAtOrUnder(rel, `${cleanRelPath(opts.changeRootRel)}/.superspec`))
64
+ return true;
65
+ return /^openspec\/changes\/[^/]+\/\.superspec(?:\/|$)/u.test(rel);
66
+ }
67
+ export function anyPathInScope(paths, scope) {
68
+ return paths.some((path) => pathsIntersect(path, scope));
69
+ }
70
+ function addOutsideRepoReason(reasons, rawPath) {
71
+ if (!reasons)
72
+ return;
73
+ const ref = toPosix(rawPath.trim().replace(/^['"]|['"]$/gu, ""));
74
+ if (!ref || reasons.some((item) => item.code === "target_path_outside_repo" && item.refs.includes(ref)))
75
+ return;
76
+ reasons.push(reason("target_path_outside_repo", `write target escapes the hook repo root: ${ref}`, [ref]));
77
+ }
78
+ function realpathMaybe(filePath) {
79
+ try {
80
+ return realpathSync(filePath);
81
+ }
82
+ catch {
83
+ return resolve(filePath);
84
+ }
85
+ }
86
+ function resolvePhysicalPath(base, pathValue) {
87
+ const absolute = isAbsolute(pathValue);
88
+ const rawParts = pathValue.split(/[\\/]+/u);
89
+ let current = absolute ? resolve("/") : realpathMaybe(base);
90
+ for (const part of rawParts) {
91
+ if (!part || part === ".")
92
+ continue;
93
+ if (part === "..") {
94
+ current = dirname(current);
95
+ continue;
96
+ }
97
+ const next = resolve(current, part);
98
+ current = existsSync(next) ? realpathMaybe(next) : next;
99
+ }
100
+ return current;
101
+ }
102
+ function normalizeTargetPath(rawPath, cwd, repoRoot, opts = {}, reasons) {
103
+ const stripped = rawPath.trim().replace(/^['"]|['"]$/gu, "");
104
+ if (!stripped || stripped === "/dev/null")
105
+ return null;
106
+ const withoutPrefix = opts.stripDiffPrefix ? stripped.replace(/^a\//u, "").replace(/^b\//u, "") : stripped;
107
+ const abs = resolvePhysicalPath(cwd, withoutPrefix);
108
+ const rel = relative(realpathMaybe(repoRoot), abs);
109
+ if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
110
+ addOutsideRepoReason(reasons, stripped);
111
+ return null;
112
+ }
113
+ return toPosix(rel);
114
+ }
115
+ function pushPath(paths, rawPath, cwd, repoRoot, opts = {}, reasons) {
116
+ const rel = normalizeTargetPath(rawPath, cwd, repoRoot, opts, reasons);
117
+ if (rel)
118
+ paths.add(rel);
119
+ }
120
+ function addShellPathTrustRootReason(reasons, rawPath) {
121
+ if (!reasons)
122
+ return;
123
+ const ref = toPosix(rawPath.trim().replace(/^['"]|['"]$/gu, ""));
124
+ if (!ref || reasons.some((item) => item.code === "shell_path_may_touch_trust_root" && item.refs.includes(ref)))
125
+ return;
126
+ reasons.push(reason("shell_path_may_touch_trust_root", `shell-expanded path may touch a SuperSpec trust root: ${ref}`, [ref]));
127
+ }
128
+ function shellPathHasMeta(rawPath) {
129
+ return /[*?\[{\$`~]/u.test(rawPath.trim().replace(/^['"]|['"]$/gu, ""));
130
+ }
131
+ function shellStaticPrefix(rawPath) {
132
+ const stripped = rawPath.trim().replace(/^['"]|['"]$/gu, "");
133
+ const meta = stripped.search(/[*?\[{\$`~]/u);
134
+ if (meta < 0)
135
+ return stripped;
136
+ const prefix = stripped.slice(0, meta).replace(/\/+$/u, "");
137
+ return prefix || ".";
138
+ }
139
+ function trustRootUnderShellPrefix(root, prefix, opts) {
140
+ const normalizedRoot = cleanRelPath(root);
141
+ const normalizedPrefix = cleanRelPath(prefix);
142
+ if (!normalizedPrefix || normalizedPrefix === ".")
143
+ return true;
144
+ if (opts.allowPartialPrefix)
145
+ return normalizedRoot.startsWith(normalizedPrefix);
146
+ return normalizedRoot === normalizedPrefix || normalizedRoot.startsWith(`${normalizedPrefix}/`);
147
+ }
148
+ function shellPathMayTouchTrustRoot(rawPath, cwd, repoRoot) {
149
+ const rawPrefix = shellStaticPrefix(rawPath);
150
+ const normalizedPrefix = normalizeTargetPath(rawPrefix, cwd, repoRoot);
151
+ const prefix = cleanRelPath(normalizedPrefix ?? rawPrefix);
152
+ if (isSuperSpecTrustRootPath(prefix))
153
+ return true;
154
+ const hasMeta = shellPathHasMeta(rawPath);
155
+ const commonRoots = [
156
+ ".codex/superspec",
157
+ ".superspec",
158
+ "openspec/changes",
159
+ ];
160
+ const changePrefix = prefix.match(/^(openspec\/changes\/[^/]+)(?:\/.*)?$/u);
161
+ const dynamicChangeTrustRoot = changePrefix ? `${changePrefix[1]}/.superspec` : "";
162
+ return commonRoots.some((root) => trustRootUnderShellPrefix(root, prefix, { allowPartialPrefix: hasMeta }))
163
+ || Boolean(dynamicChangeTrustRoot && trustRootUnderShellPrefix(dynamicChangeTrustRoot, prefix, { allowPartialPrefix: hasMeta }));
164
+ }
165
+ function addDestructiveTrustRootReason(reasons, rawPath, cwd, repoRoot) {
166
+ if (!reasons)
167
+ return;
168
+ if (shellPathMayTouchTrustRoot(rawPath, cwd, repoRoot))
169
+ addShellPathTrustRootReason(reasons, rawPath);
170
+ }
171
+ function shellPathPrefix(rawPath) {
172
+ return shellStaticPrefix(rawPath);
173
+ }
174
+ function pushShellPath(paths, rawPath, cwd, repoRoot, reasons) {
175
+ const stripped = rawPath.trim().replace(/^['"]|['"]$/gu, "");
176
+ if (stripped.startsWith("~")) {
177
+ addOutsideRepoReason(reasons, stripped);
178
+ return;
179
+ }
180
+ if (shellPathHasMeta(stripped) && shellPathMayTouchTrustRoot(stripped, cwd, repoRoot)) {
181
+ addShellPathTrustRootReason(reasons, stripped);
182
+ return;
183
+ }
184
+ pushPath(paths, shellPathPrefix(rawPath), cwd, repoRoot, {}, reasons);
185
+ }
186
+ function addTrustRootLinkSourceReason(reasons, rawPath, cwd, repoRoot) {
187
+ if (!reasons)
188
+ return;
189
+ const stripped = rawPath.trim().replace(/^['"]|['"]$/gu, "");
190
+ if (shellPathHasMeta(stripped) && shellPathMayTouchTrustRoot(stripped, cwd, repoRoot)) {
191
+ addShellPathTrustRootReason(reasons, stripped);
192
+ return;
193
+ }
194
+ const rel = normalizeTargetPath(rawPath, cwd, repoRoot);
195
+ if (!rel || !isSuperSpecTrustRootPath(rel))
196
+ return;
197
+ if (reasons.some((item) => item.code === "protected_trust_root_link_source" && item.refs.includes(rel)))
198
+ return;
199
+ reasons.push(reason("protected_trust_root_link_source", `link source points at a SuperSpec trust root: ${rel}`, [rel]));
200
+ }
201
+ function readHexEscape(command, index, maxLength) {
202
+ let hex = "";
203
+ let next = index;
204
+ while (next < command.length && hex.length < maxLength && /[0-9A-Fa-f]/u.test(command[next])) {
205
+ hex += command[next];
206
+ next += 1;
207
+ }
208
+ if (!hex)
209
+ return null;
210
+ return { value: String.fromCodePoint(Number.parseInt(hex, 16)), next };
211
+ }
212
+ function readOctalEscape(command, index) {
213
+ let octal = "";
214
+ let next = index;
215
+ while (next < command.length && octal.length < 3 && /[0-7]/u.test(command[next])) {
216
+ octal += command[next];
217
+ next += 1;
218
+ }
219
+ return { value: String.fromCodePoint(Number.parseInt(octal, 8)), next };
220
+ }
221
+ function readAnsiCQuoted(command, index) {
222
+ let current = "";
223
+ let cursor = index;
224
+ const simpleEscapes = {
225
+ a: "\u0007",
226
+ b: "\b",
227
+ e: "\u001b",
228
+ E: "\u001b",
229
+ f: "\f",
230
+ n: "\n",
231
+ r: "\r",
232
+ t: "\t",
233
+ v: "\v",
234
+ "\\": "\\",
235
+ "'": "'",
236
+ "\"": "\"",
237
+ };
238
+ while (cursor < command.length) {
239
+ const ch = command[cursor];
240
+ if (ch === "'")
241
+ return { value: current, next: cursor + 1 };
242
+ if (ch !== "\\") {
243
+ current += ch;
244
+ cursor += 1;
245
+ continue;
246
+ }
247
+ const escaped = command[cursor + 1];
248
+ if (!escaped) {
249
+ current += "\\";
250
+ cursor += 1;
251
+ continue;
252
+ }
253
+ if (escaped === "x") {
254
+ const hex = readHexEscape(command, cursor + 2, 2);
255
+ if (hex) {
256
+ current += hex.value;
257
+ cursor = hex.next;
258
+ continue;
259
+ }
260
+ }
261
+ else if (escaped === "u") {
262
+ const hex = readHexEscape(command, cursor + 2, 4);
263
+ if (hex) {
264
+ current += hex.value;
265
+ cursor = hex.next;
266
+ continue;
267
+ }
268
+ }
269
+ else if (escaped === "U") {
270
+ const hex = readHexEscape(command, cursor + 2, 8);
271
+ if (hex) {
272
+ current += hex.value;
273
+ cursor = hex.next;
274
+ continue;
275
+ }
276
+ }
277
+ else if (/[0-7]/u.test(escaped)) {
278
+ const octal = readOctalEscape(command, cursor + 1);
279
+ current += octal.value;
280
+ cursor = octal.next;
281
+ continue;
282
+ }
283
+ else if (simpleEscapes[escaped] !== undefined) {
284
+ current += simpleEscapes[escaped];
285
+ cursor += 2;
286
+ continue;
287
+ }
288
+ current += escaped;
289
+ cursor += 2;
290
+ }
291
+ return { value: current, next: cursor };
292
+ }
293
+ function shellTokens(command) {
294
+ const tokens = [];
295
+ let current = "";
296
+ let quote = null;
297
+ let escaped = false;
298
+ let index = 0;
299
+ const push = () => {
300
+ if (current)
301
+ tokens.push(current);
302
+ current = "";
303
+ };
304
+ while (index < command.length) {
305
+ const ch = command[index];
306
+ if (escaped) {
307
+ current += ch === "(" || ch === ")" ? `\\${ch}` : ch;
308
+ escaped = false;
309
+ index += 1;
310
+ continue;
311
+ }
312
+ if (ch === "\\" && quote !== "'") {
313
+ if (command[index + 1] === "\n") {
314
+ index += 2;
315
+ continue;
316
+ }
317
+ if (command[index + 1] === "\r" && command[index + 2] === "\n") {
318
+ index += 3;
319
+ continue;
320
+ }
321
+ escaped = true;
322
+ index += 1;
323
+ continue;
324
+ }
325
+ if (quote) {
326
+ if (quote === "\"" && ch === "\\" && command[index + 1]) {
327
+ if (command[index + 1] === "\n") {
328
+ index += 2;
329
+ continue;
330
+ }
331
+ if (command[index + 1] === "\r" && command[index + 2] === "\n") {
332
+ index += 3;
333
+ continue;
334
+ }
335
+ current += command[index + 1];
336
+ index += 2;
337
+ continue;
338
+ }
339
+ if (ch === quote) {
340
+ quote = null;
341
+ }
342
+ else {
343
+ current += ch;
344
+ }
345
+ index += 1;
346
+ continue;
347
+ }
348
+ if (ch === "$" && command[index + 1] === "'") {
349
+ const ansi = readAnsiCQuoted(command, index + 2);
350
+ current += ansi.value;
351
+ index = ansi.next;
352
+ continue;
353
+ }
354
+ if (ch === "$" && command[index + 1] === "\"") {
355
+ quote = "\"";
356
+ index += 2;
357
+ continue;
358
+ }
359
+ if (ch === "'" || ch === "\"") {
360
+ quote = ch;
361
+ index += 1;
362
+ continue;
363
+ }
364
+ if (ch === "\n") {
365
+ push();
366
+ tokens.push(";");
367
+ index += 1;
368
+ continue;
369
+ }
370
+ if (ch === "\r" && command[index + 1] === "\n") {
371
+ push();
372
+ tokens.push(";");
373
+ index += 2;
374
+ continue;
375
+ }
376
+ if (/\s/u.test(ch)) {
377
+ push();
378
+ index += 1;
379
+ continue;
380
+ }
381
+ if (ch === ">") {
382
+ push();
383
+ if (command[index + 1] === "&") {
384
+ tokens.push(">&");
385
+ index += 2;
386
+ }
387
+ else if (command[index + 1] === ">") {
388
+ tokens.push(">>");
389
+ index += 2;
390
+ }
391
+ else if (command[index + 1] === "|") {
392
+ tokens.push(">|");
393
+ index += 2;
394
+ }
395
+ else {
396
+ tokens.push(">");
397
+ index += 1;
398
+ }
399
+ continue;
400
+ }
401
+ if (ch === "&" && command[index + 1] === ">") {
402
+ push();
403
+ if (command[index + 2] === ">") {
404
+ tokens.push("&>>");
405
+ index += 3;
406
+ }
407
+ else {
408
+ tokens.push("&>");
409
+ index += 2;
410
+ }
411
+ continue;
412
+ }
413
+ if (ch === ";" || ch === "|" || ch === "&" || ch === "(" || ch === ")") {
414
+ push();
415
+ tokens.push(ch);
416
+ index += 1;
417
+ continue;
418
+ }
419
+ current += ch;
420
+ index += 1;
421
+ }
422
+ push();
423
+ return tokens;
424
+ }
425
+ function shellCommandName(token) {
426
+ return token.split("/").pop() ?? token;
427
+ }
428
+ function isShellSeparator(token) {
429
+ return token === ";" || token === "|" || token === "&" || token === "(" || token === ")";
430
+ }
431
+ function shellCommandEnd(tokens, start) {
432
+ let end = start;
433
+ while (end < tokens.length && !isShellSeparator(tokens[end]))
434
+ end += 1;
435
+ return end;
436
+ }
437
+ function redirectionTarget(tokens, idx) {
438
+ const token = tokens[idx];
439
+ if (/^\d+$/u.test(token) && redirectionTarget(tokens, idx + 1)) {
440
+ const nested = redirectionTarget(tokens, idx + 1);
441
+ return nested ? { target: nested.target, skip: nested.skip + 1 } : null;
442
+ }
443
+ if (![">", ">>", ">|", "&>", "&>>", ">&"].includes(token))
444
+ return null;
445
+ const candidate = tokens[idx + 1];
446
+ if (!candidate || isShellSeparator(candidate) || redirectionTarget(tokens, idx + 1)) {
447
+ return { target: null, skip: 1 };
448
+ }
449
+ if (token === ">&" && (/^\d+$/u.test(candidate) || candidate === "-")) {
450
+ return { target: null, skip: 2 };
451
+ }
452
+ return { target: candidate, skip: 2 };
453
+ }
454
+ function isAssignmentToken(token) {
455
+ return /^[A-Za-z_][A-Za-z0-9_]*=/u.test(token);
456
+ }
457
+ function skipWrapperOptions(tokens, start, end, optionsWithValues) {
458
+ let idx = start;
459
+ while (idx < end) {
460
+ const token = tokens[idx];
461
+ const redirect = redirectionTarget(tokens, idx);
462
+ if (redirect) {
463
+ idx += redirect.skip;
464
+ continue;
465
+ }
466
+ if (token === "--")
467
+ return idx + 1;
468
+ if (!token.startsWith("-") || token === "-")
469
+ return idx;
470
+ if (optionsWithValues.has(token))
471
+ idx += 2;
472
+ else
473
+ idx += 1;
474
+ }
475
+ return idx;
476
+ }
477
+ function shellCommandIndexInSegment(tokens, start, end) {
478
+ let idx = start;
479
+ while (idx < end) {
480
+ const redirect = redirectionTarget(tokens, idx);
481
+ if (redirect) {
482
+ idx += redirect.skip;
483
+ continue;
484
+ }
485
+ if (isAssignmentToken(tokens[idx])) {
486
+ idx += 1;
487
+ continue;
488
+ }
489
+ const name = shellCommandName(tokens[idx]);
490
+ if (name === "{") {
491
+ idx += 1;
492
+ continue;
493
+ }
494
+ if (name === "env") {
495
+ if (envSplitCommandAt(tokens, idx, end))
496
+ return idx;
497
+ idx = skipWrapperOptions(tokens, idx + 1, end, new Set(["-u", "--unset", "-C", "--chdir", "-S"]));
498
+ while (idx < end && isAssignmentToken(tokens[idx]))
499
+ idx += 1;
500
+ continue;
501
+ }
502
+ if (name === "sudo") {
503
+ idx = skipWrapperOptions(tokens, idx + 1, end, new Set([
504
+ "-A", "-a", "-b", "-C", "-c", "-D", "-g", "-h", "-p", "-R", "-r", "-T", "-t", "-U", "-u",
505
+ ]));
506
+ continue;
507
+ }
508
+ if (["builtin", "command", "exec", "noglob"].includes(name)) {
509
+ idx = skipWrapperOptions(tokens, idx + 1, end, new Set([]));
510
+ continue;
511
+ }
512
+ return idx;
513
+ }
514
+ return null;
515
+ }
516
+ function nestedShellCommands(tokens, start = 0) {
517
+ const commands = [];
518
+ for (let segmentStart = start; segmentStart < tokens.length;) {
519
+ if (isShellSeparator(tokens[segmentStart])) {
520
+ segmentStart += 1;
521
+ continue;
522
+ }
523
+ const segmentEnd = shellCommandEnd(tokens, segmentStart);
524
+ const commandIdx = shellCommandIndexInSegment(tokens, segmentStart, segmentEnd);
525
+ if (commandIdx !== null) {
526
+ commands.push(...nestedExecutableCommandsAt(tokens, commandIdx, segmentEnd).map((nested) => nested.command));
527
+ }
528
+ segmentStart = segmentEnd;
529
+ }
530
+ return commands;
531
+ }
532
+ function lastShellCommandSegment(tokens) {
533
+ let last = null;
534
+ for (let segmentStart = 0; segmentStart < tokens.length;) {
535
+ if (isShellSeparator(tokens[segmentStart])) {
536
+ segmentStart += 1;
537
+ continue;
538
+ }
539
+ const segmentEnd = shellCommandEnd(tokens, segmentStart);
540
+ const commandIdx = shellCommandIndexInSegment(tokens, segmentStart, segmentEnd);
541
+ if (commandIdx !== null)
542
+ last = { commandIdx, segmentEnd };
543
+ segmentStart = segmentEnd;
544
+ }
545
+ return last;
546
+ }
547
+ function nestedExecutableCommandsAt(tokens, start, end, cwd, repoRoot) {
548
+ const direct = nestedShellCommandAt(tokens, start, end)
549
+ ?? envSplitCommandAt(tokens, start, end)
550
+ ?? evalCommandAt(tokens, start, end)
551
+ ?? packageRunnerCommandAt(tokens, start, end);
552
+ const commands = direct ? [{ command: direct }] : [];
553
+ commands.push(...findExecCommandsAt(tokens, start, end, cwd, repoRoot));
554
+ return commands;
555
+ }
556
+ function nestedShellCommandAt(tokens, start, end) {
557
+ const name = shellCommandName(tokens[start]);
558
+ if (!["bash", "sh", "zsh", "dash", "fish"].includes(name))
559
+ return null;
560
+ for (let idx = start + 1; idx < end; idx += 1) {
561
+ const token = tokens[idx];
562
+ if (token === "-c" || /^-[A-Za-z]*c[A-Za-z]*$/u.test(token)) {
563
+ const candidate = tokens[idx + 1];
564
+ return candidate && idx + 1 < end ? candidate : null;
565
+ }
566
+ }
567
+ return null;
568
+ }
569
+ function envSplitCommandAt(tokens, start, end) {
570
+ if (shellCommandName(tokens[start]) !== "env")
571
+ return null;
572
+ for (let idx = start + 1; idx < end; idx += 1) {
573
+ const token = tokens[idx];
574
+ const redirect = redirectionTarget(tokens, idx);
575
+ if (redirect) {
576
+ idx += redirect.skip - 1;
577
+ continue;
578
+ }
579
+ if (token === "-S" || token === "--split-string") {
580
+ const candidate = tokens[idx + 1];
581
+ return candidate && idx + 1 < end ? candidate : null;
582
+ }
583
+ if (token.startsWith("-S") && token.length > 2)
584
+ return token.slice(2);
585
+ if (token.startsWith("--split-string="))
586
+ return token.slice("--split-string=".length);
587
+ }
588
+ return null;
589
+ }
590
+ function evalCommandAt(tokens, start, end) {
591
+ if (shellCommandName(tokens[start]) !== "eval")
592
+ return null;
593
+ const parts = [];
594
+ for (let idx = start + 1; idx < end; idx += 1) {
595
+ const token = tokens[idx];
596
+ if (token === "}")
597
+ continue;
598
+ const redirect = redirectionTarget(tokens, idx);
599
+ if (redirect) {
600
+ idx += redirect.skip - 1;
601
+ continue;
602
+ }
603
+ parts.push(token);
604
+ }
605
+ return parts.length > 0 ? parts.join(" ") : null;
606
+ }
607
+ function shellQuoteToken(token) {
608
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(token))
609
+ return token;
610
+ return `'${token.replace(/'/gu, "'\\''")}'`;
611
+ }
612
+ function shellCommandFromTokens(parts) {
613
+ return parts.length > 0 ? parts.map((part) => shellQuoteToken(part)).join(" ") : null;
614
+ }
615
+ function packageOptionHasValue(token, optionsWithValues) {
616
+ if (token.startsWith("--") && token.includes("="))
617
+ return false;
618
+ return optionsWithValues.has(token);
619
+ }
620
+ function packageSubcommandIndex(args, subcommands) {
621
+ const optionsWithValues = new Set([
622
+ "-C",
623
+ "-c",
624
+ "-F",
625
+ "-w",
626
+ "--cache",
627
+ "--config",
628
+ "--cwd",
629
+ "--dir",
630
+ "--filter",
631
+ "--globalconfig",
632
+ "--prefix",
633
+ "--registry",
634
+ "--userconfig",
635
+ "--workspace",
636
+ ]);
637
+ for (let idx = 0; idx < args.length; idx += 1) {
638
+ const token = args[idx];
639
+ if (token === "--")
640
+ continue;
641
+ if (token.startsWith("--")) {
642
+ if (packageOptionHasValue(token, optionsWithValues) && args[idx + 1])
643
+ idx += 1;
644
+ continue;
645
+ }
646
+ if (/^-[^-]/u.test(token)) {
647
+ if (token.length === 2 && optionsWithValues.has(token) && args[idx + 1])
648
+ idx += 1;
649
+ continue;
650
+ }
651
+ return subcommands.has(token) ? idx : null;
652
+ }
653
+ return null;
654
+ }
655
+ function packageCallCommand(args, start) {
656
+ const optionsWithValues = new Set([
657
+ "-p",
658
+ "-F",
659
+ "-w",
660
+ "--cache",
661
+ "--cwd",
662
+ "--dir",
663
+ "--filter",
664
+ "--package",
665
+ "--registry",
666
+ "--shell",
667
+ "--userconfig",
668
+ "--workspace",
669
+ ]);
670
+ for (let idx = start; idx < args.length; idx += 1) {
671
+ const token = args[idx];
672
+ if (token === "--")
673
+ return null;
674
+ if (token === "-c" || token === "--call")
675
+ return args[idx + 1] ?? null;
676
+ if (token.startsWith("-c") && token.length > 2)
677
+ return token.slice(2);
678
+ if (token.startsWith("--call="))
679
+ return token.slice("--call=".length);
680
+ if (token.startsWith("--")) {
681
+ if (packageOptionHasValue(token, optionsWithValues) && args[idx + 1])
682
+ idx += 1;
683
+ continue;
684
+ }
685
+ if (/^-[^-]/u.test(token)) {
686
+ if (token.length === 2 && optionsWithValues.has(token) && args[idx + 1])
687
+ idx += 1;
688
+ continue;
689
+ }
690
+ return null;
691
+ }
692
+ return null;
693
+ }
694
+ function packageExecutableArgs(args, start) {
695
+ const optionsWithValues = new Set([
696
+ "-p",
697
+ "-F",
698
+ "-w",
699
+ "--cache",
700
+ "--cwd",
701
+ "--dir",
702
+ "--filter",
703
+ "--package",
704
+ "--registry",
705
+ "--shell",
706
+ "--userconfig",
707
+ "--workspace",
708
+ ]);
709
+ let idx = start;
710
+ while (idx < args.length) {
711
+ const token = args[idx];
712
+ if (token === "--") {
713
+ idx += 1;
714
+ break;
715
+ }
716
+ if (token.startsWith("--")) {
717
+ if (token.includes("=")) {
718
+ idx += 1;
719
+ }
720
+ else if (packageOptionHasValue(token, optionsWithValues) && args[idx + 1]) {
721
+ idx += 2;
722
+ }
723
+ else {
724
+ idx += 1;
725
+ }
726
+ continue;
727
+ }
728
+ if (/^-[^-]/u.test(token)) {
729
+ if (token.length === 2 && optionsWithValues.has(token) && args[idx + 1])
730
+ idx += 2;
731
+ else
732
+ idx += 1;
733
+ continue;
734
+ }
735
+ break;
736
+ }
737
+ const command = args.slice(idx);
738
+ const separator = command.indexOf("--");
739
+ if (separator >= 0)
740
+ command.splice(separator, 1);
741
+ return command;
742
+ }
743
+ function skipPackageGlobalOptions(args) {
744
+ const optionsWithValues = new Set([
745
+ "-C",
746
+ "--cwd",
747
+ "--cache",
748
+ "--config",
749
+ "--global-folder",
750
+ "--modules-folder",
751
+ "--mutex",
752
+ "--prefix",
753
+ "--registry",
754
+ "--userconfig",
755
+ ]);
756
+ let idx = 0;
757
+ while (idx < args.length) {
758
+ const token = args[idx];
759
+ if (token === "--")
760
+ return idx + 1;
761
+ if (token.startsWith("--")) {
762
+ if (packageOptionHasValue(token, optionsWithValues) && args[idx + 1])
763
+ idx += 2;
764
+ else
765
+ idx += 1;
766
+ continue;
767
+ }
768
+ if (/^-[^-]/u.test(token)) {
769
+ if (token.length === 2 && optionsWithValues.has(token) && args[idx + 1])
770
+ idx += 2;
771
+ else
772
+ idx += 1;
773
+ continue;
774
+ }
775
+ break;
776
+ }
777
+ return idx;
778
+ }
779
+ function packageRunnerCommandAt(tokens, start, end) {
780
+ const name = shellCommandName(tokens[start]);
781
+ const args = sameShellCommandArguments(tokens, start + 1, end);
782
+ if (name === "npx" || name === "bunx")
783
+ return packageCallCommand(args, 0) ?? shellCommandFromTokens(packageExecutableArgs(args, 0));
784
+ if (name === "npm") {
785
+ const subcommand = packageSubcommandIndex(args, new Set(["exec", "x"]));
786
+ return subcommand === null ? null : packageCallCommand(args, subcommand + 1) ?? shellCommandFromTokens(packageExecutableArgs(args, subcommand + 1));
787
+ }
788
+ if (name === "yarn") {
789
+ const first = skipPackageGlobalOptions(args);
790
+ if (args[first] === "workspace" && args[first + 1]) {
791
+ const subcommand = packageSubcommandIndex(args.slice(first + 2), new Set(["exec", "dlx"]));
792
+ return subcommand === null ? null : packageCallCommand(args, first + subcommand + 3) ?? shellCommandFromTokens(packageExecutableArgs(args, first + subcommand + 3));
793
+ }
794
+ }
795
+ if (name === "pnpm" || name === "yarn") {
796
+ const subcommand = packageSubcommandIndex(args, new Set(["exec", "dlx"]));
797
+ return subcommand === null ? null : packageCallCommand(args, subcommand + 1) ?? shellCommandFromTokens(packageExecutableArgs(args, subcommand + 1));
798
+ }
799
+ if (name === "bun") {
800
+ const subcommand = packageSubcommandIndex(args, new Set(["x"]));
801
+ return subcommand === null ? null : packageCallCommand(args, subcommand + 1) ?? shellCommandFromTokens(packageExecutableArgs(args, subcommand + 1));
802
+ }
803
+ return null;
804
+ }
805
+ function backtickCommandSubstitutions(command) {
806
+ const commands = [];
807
+ let quote = null;
808
+ let escaped = false;
809
+ let inBacktick = false;
810
+ let backtickEscaped = false;
811
+ let current = "";
812
+ for (let idx = 0; idx < command.length; idx += 1) {
813
+ const ch = command[idx];
814
+ if (inBacktick) {
815
+ if (backtickEscaped) {
816
+ current += ch;
817
+ backtickEscaped = false;
818
+ continue;
819
+ }
820
+ if (ch === "\\") {
821
+ backtickEscaped = true;
822
+ continue;
823
+ }
824
+ if (ch === "`") {
825
+ if (current.trim())
826
+ commands.push(current);
827
+ current = "";
828
+ inBacktick = false;
829
+ continue;
830
+ }
831
+ current += ch;
832
+ continue;
833
+ }
834
+ if (escaped) {
835
+ escaped = false;
836
+ continue;
837
+ }
838
+ if (ch === "\\" && quote !== "'") {
839
+ escaped = true;
840
+ continue;
841
+ }
842
+ if (quote === "'") {
843
+ if (ch === "'")
844
+ quote = null;
845
+ continue;
846
+ }
847
+ if (quote === "\"") {
848
+ if (ch === "\"") {
849
+ quote = null;
850
+ continue;
851
+ }
852
+ if (ch === "`") {
853
+ inBacktick = true;
854
+ current = "";
855
+ }
856
+ continue;
857
+ }
858
+ if (ch === "'" || ch === "\"") {
859
+ quote = ch;
860
+ continue;
861
+ }
862
+ if (ch === "`") {
863
+ inBacktick = true;
864
+ current = "";
865
+ }
866
+ }
867
+ return commands;
868
+ }
869
+ function dollarParenCommandSubstitutions(command) {
870
+ const commands = [];
871
+ let quote = null;
872
+ let escaped = false;
873
+ for (let idx = 0; idx < command.length; idx += 1) {
874
+ const ch = command[idx];
875
+ if (escaped) {
876
+ escaped = false;
877
+ continue;
878
+ }
879
+ if (ch === "\\" && quote !== "'") {
880
+ escaped = true;
881
+ continue;
882
+ }
883
+ if (quote === "'") {
884
+ if (ch === "'")
885
+ quote = null;
886
+ continue;
887
+ }
888
+ if (ch === "\"" && quote === "\"") {
889
+ quote = null;
890
+ continue;
891
+ }
892
+ if (ch === "\"" && quote === null) {
893
+ quote = "\"";
894
+ continue;
895
+ }
896
+ if (ch === "'" && quote === null) {
897
+ quote = "'";
898
+ continue;
899
+ }
900
+ if (ch === "$" && command[idx + 1] === "(") {
901
+ const parsed = readDollarParen(command, idx + 2);
902
+ if (parsed) {
903
+ if (parsed.value.trim())
904
+ commands.push(parsed.value);
905
+ idx = parsed.next - 1;
906
+ }
907
+ }
908
+ }
909
+ return commands;
910
+ }
911
+ function readDollarParen(command, start) {
912
+ let depth = 1;
913
+ let quote = null;
914
+ let escaped = false;
915
+ let current = "";
916
+ for (let idx = start; idx < command.length; idx += 1) {
917
+ const ch = command[idx];
918
+ if (escaped) {
919
+ current += ch;
920
+ escaped = false;
921
+ continue;
922
+ }
923
+ if (ch === "\\" && quote !== "'") {
924
+ current += ch;
925
+ escaped = true;
926
+ continue;
927
+ }
928
+ if (quote) {
929
+ if (ch === quote)
930
+ quote = null;
931
+ current += ch;
932
+ continue;
933
+ }
934
+ if (ch === "'" || ch === "\"") {
935
+ quote = ch;
936
+ current += ch;
937
+ continue;
938
+ }
939
+ if (ch === "$" && command[idx + 1] === "(") {
940
+ depth += 1;
941
+ current += "$(";
942
+ idx += 1;
943
+ continue;
944
+ }
945
+ if (ch === ")") {
946
+ depth -= 1;
947
+ if (depth === 0)
948
+ return { value: current, next: idx + 1 };
949
+ }
950
+ current += ch;
951
+ }
952
+ return null;
953
+ }
954
+ function commandSubstitutions(command) {
955
+ return [...backtickCommandSubstitutions(command), ...dollarParenCommandSubstitutions(command)];
956
+ }
957
+ function findCommandRoots(tokens, start, end) {
958
+ const roots = [];
959
+ for (let idx = start + 1; idx < end; idx += 1) {
960
+ const token = tokens[idx];
961
+ const redirect = redirectionTarget(tokens, idx);
962
+ if (redirect) {
963
+ idx += redirect.skip - 1;
964
+ continue;
965
+ }
966
+ if (["-H", "-L", "-P"].includes(token))
967
+ continue;
968
+ if (token === "-D") {
969
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
970
+ idx += 1;
971
+ continue;
972
+ }
973
+ if (/^-O\d*$/u.test(token))
974
+ continue;
975
+ if (token === "!" || token === "(" || token === "\\(" || token.startsWith("-"))
976
+ break;
977
+ roots.push(token);
978
+ }
979
+ return roots.length > 0 ? roots : ["."];
980
+ }
981
+ function findExecdirCwds(tokens, start, end, cwd, repoRoot) {
982
+ if (!cwd || !repoRoot)
983
+ return [];
984
+ const cwds = new Set();
985
+ for (const root of findCommandRoots(tokens, start, end)) {
986
+ if (root.startsWith("~") || shellPathHasMeta(root))
987
+ continue;
988
+ cwds.add(resolvePhysicalPath(cwd, root));
989
+ }
990
+ return [...cwds];
991
+ }
992
+ function findHasDelete(tokens, start, end) {
993
+ if (shellCommandName(tokens[start]) !== "find")
994
+ return false;
995
+ for (let idx = start + 1; idx < end; idx += 1) {
996
+ const redirect = redirectionTarget(tokens, idx);
997
+ if (redirect) {
998
+ idx += redirect.skip - 1;
999
+ continue;
1000
+ }
1001
+ if (tokens[idx] === "-delete")
1002
+ return true;
1003
+ }
1004
+ return false;
1005
+ }
1006
+ function findDeleteRoots(tokens, start, end) {
1007
+ return findHasDelete(tokens, start, end) ? findCommandRoots(tokens, start, end) : [];
1008
+ }
1009
+ function findRootParentPaths(tokens, start, end, cwd, repoRoot) {
1010
+ if (!cwd || !repoRoot)
1011
+ return [];
1012
+ const paths = new Set();
1013
+ for (const root of findCommandRoots(tokens, start, end)) {
1014
+ pushShellPath(paths, root, cwd, repoRoot);
1015
+ }
1016
+ return [...paths];
1017
+ }
1018
+ function findRootParentRawPaths(tokens, start, end) {
1019
+ return findCommandRoots(tokens, start, end);
1020
+ }
1021
+ function findExecCommandsAt(tokens, start, end, cwd, repoRoot) {
1022
+ if (shellCommandName(tokens[start]) !== "find")
1023
+ return [];
1024
+ const commands = [];
1025
+ for (let idx = start + 1; idx < end; idx += 1) {
1026
+ const token = tokens[idx];
1027
+ const redirect = redirectionTarget(tokens, idx);
1028
+ if (redirect) {
1029
+ idx += redirect.skip - 1;
1030
+ continue;
1031
+ }
1032
+ if (!["-exec", "-execdir", "-ok", "-okdir"].includes(token))
1033
+ continue;
1034
+ const parts = [];
1035
+ for (idx += 1; idx < end; idx += 1) {
1036
+ const part = tokens[idx];
1037
+ if (part === ";" || part === "+")
1038
+ break;
1039
+ parts.push(part);
1040
+ }
1041
+ const command = shellCommandFromTokens(parts);
1042
+ if (!command)
1043
+ continue;
1044
+ if (token === "-execdir" || token === "-okdir") {
1045
+ const execdirCwds = findExecdirCwds(tokens, start, end, cwd, repoRoot);
1046
+ const possibleParentPaths = findRootParentPaths(tokens, start, end, cwd, repoRoot);
1047
+ const possibleParentRawPaths = findRootParentRawPaths(tokens, start, end);
1048
+ if (execdirCwds.length > 0) {
1049
+ commands.push(...execdirCwds.map((execCwd) => ({ command, cwd: execCwd, possibleParentPaths, possibleParentRawPaths })));
1050
+ }
1051
+ else {
1052
+ commands.push({ command, possibleParentPaths, possibleParentRawPaths });
1053
+ }
1054
+ }
1055
+ else {
1056
+ commands.push({ command });
1057
+ }
1058
+ }
1059
+ return commands;
1060
+ }
1061
+ function shortOptionAttachedValue(token, opt) {
1062
+ if (!token.startsWith("-") || token.startsWith("--"))
1063
+ return null;
1064
+ const index = token.indexOf(opt, 1);
1065
+ if (index < 0 || index === token.length - 1)
1066
+ return null;
1067
+ return token.slice(index + 1);
1068
+ }
1069
+ function shellCommandPathOperands(tokens, start) {
1070
+ const operands = [];
1071
+ const targetDirs = [];
1072
+ let optionsEnded = false;
1073
+ for (let idx = start; idx < tokens.length; idx += 1) {
1074
+ const token = tokens[idx];
1075
+ if (isShellSeparator(token))
1076
+ break;
1077
+ if (token === "}")
1078
+ continue;
1079
+ const redirect = redirectionTarget(tokens, idx);
1080
+ if (redirect) {
1081
+ idx += redirect.skip - 1;
1082
+ continue;
1083
+ }
1084
+ if (!optionsEnded && token === "--") {
1085
+ optionsEnded = true;
1086
+ continue;
1087
+ }
1088
+ if (!optionsEnded && token.startsWith("--")) {
1089
+ if (token.startsWith("--target-directory="))
1090
+ targetDirs.push(token.slice("--target-directory=".length));
1091
+ else if (token === "--target-directory" && tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1092
+ targetDirs.push(tokens[idx + 1]);
1093
+ idx += 1;
1094
+ }
1095
+ continue;
1096
+ }
1097
+ if (!optionsEnded && /^-[^-]/u.test(token)) {
1098
+ const attachedTarget = shortOptionAttachedValue(token, "t");
1099
+ if (attachedTarget) {
1100
+ targetDirs.push(attachedTarget);
1101
+ }
1102
+ else if (token.includes("t") && tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1103
+ targetDirs.push(tokens[idx + 1]);
1104
+ idx += 1;
1105
+ }
1106
+ else if (/[mog]/u.test(token) && tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1107
+ idx += 1;
1108
+ }
1109
+ continue;
1110
+ }
1111
+ operands.push(token);
1112
+ }
1113
+ return { operands, targetDirs };
1114
+ }
1115
+ function pushShellRedirectionPaths(tokens, paths, cwd, repoRoot, reasons, start = 0, end = tokens.length) {
1116
+ for (let idx = start; idx < end; idx += 1) {
1117
+ const redirect = redirectionTarget(tokens, idx);
1118
+ if (!redirect)
1119
+ continue;
1120
+ if (redirect.target)
1121
+ pushShellPath(paths, redirect.target, cwd, repoRoot, reasons);
1122
+ idx += redirect.skip - 1;
1123
+ }
1124
+ }
1125
+ function sedInPlaceTargets(tokens, start) {
1126
+ const operands = [];
1127
+ let hasInPlace = false;
1128
+ let hasScriptFlag = false;
1129
+ let optionsEnded = false;
1130
+ for (let idx = start; idx < tokens.length; idx += 1) {
1131
+ const token = tokens[idx];
1132
+ if (isShellSeparator(token))
1133
+ break;
1134
+ if (token === "}")
1135
+ continue;
1136
+ const redirect = redirectionTarget(tokens, idx);
1137
+ if (redirect) {
1138
+ idx += redirect.skip - 1;
1139
+ continue;
1140
+ }
1141
+ if (!optionsEnded && token === "--") {
1142
+ optionsEnded = true;
1143
+ continue;
1144
+ }
1145
+ if (!optionsEnded && token.startsWith("--")) {
1146
+ if (token === "--in-place" || token.startsWith("--in-place="))
1147
+ hasInPlace = true;
1148
+ if (token === "--expression" || token === "--file") {
1149
+ hasScriptFlag = true;
1150
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
1151
+ idx += 1;
1152
+ }
1153
+ else if (token.startsWith("--expression=") || token.startsWith("--file=")) {
1154
+ hasScriptFlag = true;
1155
+ }
1156
+ continue;
1157
+ }
1158
+ if (!optionsEnded && /^-[^-]/u.test(token)) {
1159
+ if (token === "-i" || token.startsWith("-i") || /^-[A-Za-z]*i/u.test(token))
1160
+ hasInPlace = true;
1161
+ if (token === "-e" || token === "-f") {
1162
+ hasScriptFlag = true;
1163
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
1164
+ idx += 1;
1165
+ }
1166
+ continue;
1167
+ }
1168
+ operands.push(token);
1169
+ }
1170
+ if (!hasInPlace)
1171
+ return { hasInPlace: false, targets: [] };
1172
+ return { hasInPlace: true, targets: hasScriptFlag ? operands : operands.slice(1) };
1173
+ }
1174
+ function teeTargets(tokens, start) {
1175
+ const targets = [];
1176
+ let optionsEnded = false;
1177
+ for (let idx = start; idx < tokens.length; idx += 1) {
1178
+ const token = tokens[idx];
1179
+ if (isShellSeparator(token))
1180
+ break;
1181
+ if (token === "}")
1182
+ continue;
1183
+ const redirect = redirectionTarget(tokens, idx);
1184
+ if (redirect) {
1185
+ idx += redirect.skip - 1;
1186
+ continue;
1187
+ }
1188
+ if (!optionsEnded && token === "--") {
1189
+ optionsEnded = true;
1190
+ continue;
1191
+ }
1192
+ if (!optionsEnded && token.startsWith("-"))
1193
+ continue;
1194
+ targets.push(token);
1195
+ }
1196
+ return targets;
1197
+ }
1198
+ function shellOperands(tokens, start, optionsWithValues = new Set()) {
1199
+ const operands = [];
1200
+ let optionsEnded = false;
1201
+ for (let idx = start; idx < tokens.length; idx += 1) {
1202
+ const token = tokens[idx];
1203
+ if (isShellSeparator(token))
1204
+ break;
1205
+ if (token === "}")
1206
+ continue;
1207
+ const redirect = redirectionTarget(tokens, idx);
1208
+ if (redirect) {
1209
+ idx += redirect.skip - 1;
1210
+ continue;
1211
+ }
1212
+ if (!optionsEnded && token === "--") {
1213
+ optionsEnded = true;
1214
+ continue;
1215
+ }
1216
+ if (!optionsEnded && token.startsWith("--")) {
1217
+ if (packageOptionHasValue(token, optionsWithValues) && tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
1218
+ idx += 1;
1219
+ continue;
1220
+ }
1221
+ if (!optionsEnded && /^-[^-]/u.test(token)) {
1222
+ if (token.length === 2 && optionsWithValues.has(token) && tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
1223
+ idx += 1;
1224
+ continue;
1225
+ }
1226
+ operands.push(token);
1227
+ }
1228
+ return operands;
1229
+ }
1230
+ function isRsyncDeleteOption(token) {
1231
+ return token === "--del" || token === "--delete" || token.startsWith("--delete-");
1232
+ }
1233
+ function rsyncValueOptions() {
1234
+ return new Set([
1235
+ "-B",
1236
+ "-e",
1237
+ "-f",
1238
+ "-M",
1239
+ "-T",
1240
+ "--address",
1241
+ "--backup-dir",
1242
+ "--block-size",
1243
+ "--bwlimit",
1244
+ "--checksum-choice",
1245
+ "--chmod",
1246
+ "--chown",
1247
+ "--compare-dest",
1248
+ "--compress-choice",
1249
+ "--compress-level",
1250
+ "--contimeout",
1251
+ "--copy-as",
1252
+ "--copy-dest",
1253
+ "--debug",
1254
+ "--dir-merge",
1255
+ "--exclude",
1256
+ "--exclude-from",
1257
+ "--files-from",
1258
+ "--filter",
1259
+ "--filter-from",
1260
+ "--groupmap",
1261
+ "--iconv",
1262
+ "--include",
1263
+ "--include-from",
1264
+ "--info",
1265
+ "--link-dest",
1266
+ "--log-file",
1267
+ "--log-file-format",
1268
+ "--max-alloc",
1269
+ "--max-delete",
1270
+ "--max-size",
1271
+ "--min-size",
1272
+ "--modify-window",
1273
+ "--only-write-batch",
1274
+ "--out-format",
1275
+ "--outbuf",
1276
+ "--partial-dir",
1277
+ "--password-file",
1278
+ "--port",
1279
+ "--protocol",
1280
+ "--remote-option",
1281
+ "--remote-shell",
1282
+ "--rsh",
1283
+ "--rsync-path",
1284
+ "--skip-compress",
1285
+ "--sockopts",
1286
+ "--suffix",
1287
+ "--temp-dir",
1288
+ "--timeout",
1289
+ "--usermap",
1290
+ "--write-batch",
1291
+ "--zc",
1292
+ "--zl",
1293
+ ]);
1294
+ }
1295
+ function rsyncTargets(tokens, start) {
1296
+ const optionTargets = [];
1297
+ const optionTargetDirs = [];
1298
+ let removeSourceFiles = false;
1299
+ let deleteExtraneous = false;
1300
+ for (let idx = start; idx < tokens.length; idx += 1) {
1301
+ const token = tokens[idx];
1302
+ if (isShellSeparator(token))
1303
+ break;
1304
+ const redirect = redirectionTarget(tokens, idx);
1305
+ if (redirect) {
1306
+ idx += redirect.skip - 1;
1307
+ continue;
1308
+ }
1309
+ if (isRsyncDeleteOption(token)) {
1310
+ deleteExtraneous = true;
1311
+ continue;
1312
+ }
1313
+ if (token === "--remove-source-files") {
1314
+ removeSourceFiles = true;
1315
+ continue;
1316
+ }
1317
+ if (token === "--log-file" || token === "--write-batch" || token === "--only-write-batch") {
1318
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1319
+ optionTargets.push(tokens[idx + 1]);
1320
+ idx += 1;
1321
+ }
1322
+ continue;
1323
+ }
1324
+ if (token.startsWith("--log-file="))
1325
+ optionTargets.push(token.slice("--log-file=".length));
1326
+ if (token.startsWith("--write-batch="))
1327
+ optionTargets.push(token.slice("--write-batch=".length));
1328
+ if (token.startsWith("--only-write-batch="))
1329
+ optionTargets.push(token.slice("--only-write-batch=".length));
1330
+ if (token === "--backup-dir" || token === "--partial-dir" || token === "--temp-dir") {
1331
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1332
+ optionTargetDirs.push(tokens[idx + 1]);
1333
+ idx += 1;
1334
+ }
1335
+ continue;
1336
+ }
1337
+ if (token === "-T") {
1338
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1339
+ optionTargetDirs.push(tokens[idx + 1]);
1340
+ idx += 1;
1341
+ }
1342
+ continue;
1343
+ }
1344
+ const attachedTempDir = shortOptionAttachedValue(token, "T");
1345
+ if (attachedTempDir) {
1346
+ optionTargetDirs.push(attachedTempDir);
1347
+ continue;
1348
+ }
1349
+ if (token.startsWith("--backup-dir="))
1350
+ optionTargetDirs.push(token.slice("--backup-dir=".length));
1351
+ if (token.startsWith("--partial-dir="))
1352
+ optionTargetDirs.push(token.slice("--partial-dir=".length));
1353
+ if (token.startsWith("--temp-dir="))
1354
+ optionTargetDirs.push(token.slice("--temp-dir=".length));
1355
+ }
1356
+ const operands = shellOperands(tokens, start, rsyncValueOptions());
1357
+ return { operands, optionTargets, optionTargetDirs, removeSourceFiles, deleteExtraneous };
1358
+ }
1359
+ function urlPathBasename(value) {
1360
+ let raw = value;
1361
+ try {
1362
+ raw = new URL(value).pathname;
1363
+ }
1364
+ catch {
1365
+ raw = value.split(/[?#]/u)[0] ?? value;
1366
+ }
1367
+ const name = basename(raw.replace(/[\\/]+$/u, ""));
1368
+ return name && name !== "." && name !== ".." ? name : null;
1369
+ }
1370
+ function joinShellPath(dir, name) {
1371
+ if (!dir || dir === "." || isAbsolute(name))
1372
+ return name;
1373
+ return `${dir.replace(/[\\/]+$/u, "")}/${name}`;
1374
+ }
1375
+ function curlPathWriteOptions() {
1376
+ return new Set([
1377
+ "--cookie-jar",
1378
+ "--dump-header",
1379
+ "--etag-save",
1380
+ "--libcurl",
1381
+ "--stderr",
1382
+ "--trace",
1383
+ "--trace-ascii",
1384
+ ]);
1385
+ }
1386
+ function pushCurlPathTarget(targets, value) {
1387
+ if (value === undefined || value === "")
1388
+ return false;
1389
+ if (value !== "-")
1390
+ targets.push(value);
1391
+ return true;
1392
+ }
1393
+ function curlOutputTargets(tokens, start) {
1394
+ const targets = [];
1395
+ const outputNames = [];
1396
+ const ambiguousTargetDirs = [];
1397
+ const remoteUrls = [];
1398
+ let outputDir = ".";
1399
+ let remoteName = false;
1400
+ let headerDerivedName = false;
1401
+ let configDrivenWrite = false;
1402
+ let unknownWriteTarget = false;
1403
+ let writes = false;
1404
+ for (let idx = start; idx < tokens.length; idx += 1) {
1405
+ const token = tokens[idx];
1406
+ if (isShellSeparator(token))
1407
+ break;
1408
+ const redirect = redirectionTarget(tokens, idx);
1409
+ if (redirect) {
1410
+ idx += redirect.skip - 1;
1411
+ continue;
1412
+ }
1413
+ if (token === "-K" || token === "--config") {
1414
+ writes = true;
1415
+ configDrivenWrite = true;
1416
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
1417
+ idx += 1;
1418
+ continue;
1419
+ }
1420
+ if (token.startsWith("--config=") || (token.startsWith("-K") && token.length > 2)) {
1421
+ writes = true;
1422
+ configDrivenWrite = true;
1423
+ continue;
1424
+ }
1425
+ if (token === "-o" || token === "--output") {
1426
+ writes = true;
1427
+ if (pushCurlPathTarget(outputNames, tokens[idx + 1]))
1428
+ idx += 1;
1429
+ else
1430
+ unknownWriteTarget = true;
1431
+ continue;
1432
+ }
1433
+ if (token.startsWith("--output=")) {
1434
+ writes = true;
1435
+ if (!pushCurlPathTarget(outputNames, token.slice("--output=".length)))
1436
+ unknownWriteTarget = true;
1437
+ continue;
1438
+ }
1439
+ if (token.startsWith("-o") && token.length > 2) {
1440
+ writes = true;
1441
+ if (!pushCurlPathTarget(outputNames, token.slice(2)))
1442
+ unknownWriteTarget = true;
1443
+ continue;
1444
+ }
1445
+ if (/^-[A-Za-z]*o$/u.test(token)) {
1446
+ writes = true;
1447
+ if (pushCurlPathTarget(outputNames, tokens[idx + 1]))
1448
+ idx += 1;
1449
+ else
1450
+ unknownWriteTarget = true;
1451
+ continue;
1452
+ }
1453
+ if (token === "-D" || token === "-c" || /^-[A-Za-z]*[Dc]$/u.test(token)) {
1454
+ writes = true;
1455
+ if (pushCurlPathTarget(targets, tokens[idx + 1]))
1456
+ idx += 1;
1457
+ else
1458
+ unknownWriteTarget = true;
1459
+ continue;
1460
+ }
1461
+ const attachedDumpHeader = shortOptionAttachedValue(token, "D");
1462
+ if (attachedDumpHeader) {
1463
+ writes = true;
1464
+ pushCurlPathTarget(targets, attachedDumpHeader);
1465
+ continue;
1466
+ }
1467
+ const attachedCookieJar = shortOptionAttachedValue(token, "c");
1468
+ if (attachedCookieJar) {
1469
+ writes = true;
1470
+ pushCurlPathTarget(targets, attachedCookieJar);
1471
+ continue;
1472
+ }
1473
+ if (curlPathWriteOptions().has(token)) {
1474
+ writes = true;
1475
+ if (pushCurlPathTarget(targets, tokens[idx + 1]))
1476
+ idx += 1;
1477
+ else
1478
+ unknownWriteTarget = true;
1479
+ continue;
1480
+ }
1481
+ const pathWriteOption = [...curlPathWriteOptions()].find((option) => token.startsWith(`${option}=`));
1482
+ if (pathWriteOption) {
1483
+ writes = true;
1484
+ if (!pushCurlPathTarget(targets, token.slice(pathWriteOption.length + 1)))
1485
+ unknownWriteTarget = true;
1486
+ continue;
1487
+ }
1488
+ if (token === "--url") {
1489
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1490
+ remoteUrls.push(tokens[idx + 1]);
1491
+ idx += 1;
1492
+ }
1493
+ continue;
1494
+ }
1495
+ if (token.startsWith("--url=")) {
1496
+ remoteUrls.push(token.slice("--url=".length));
1497
+ continue;
1498
+ }
1499
+ if (token === "--output-dir") {
1500
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1501
+ outputDir = tokens[idx + 1];
1502
+ idx += 1;
1503
+ }
1504
+ continue;
1505
+ }
1506
+ if (token.startsWith("--output-dir=")) {
1507
+ outputDir = token.slice("--output-dir=".length);
1508
+ continue;
1509
+ }
1510
+ if (token === "-J" || token === "--remote-header-name" || /^-[A-Za-z]*J[A-Za-z]*$/u.test(token)) {
1511
+ headerDerivedName = true;
1512
+ if (token === "--remote-header-name")
1513
+ continue;
1514
+ }
1515
+ if (token === "-O" || token === "--remote-name" || token === "--remote-name-all" || /^-[A-Za-z]*O[A-Za-z]*$/u.test(token)) {
1516
+ writes = true;
1517
+ remoteName = true;
1518
+ continue;
1519
+ }
1520
+ if (!token.startsWith("-"))
1521
+ remoteUrls.push(token);
1522
+ }
1523
+ for (const outputName of outputNames)
1524
+ targets.push(joinShellPath(outputDir, outputName));
1525
+ if (remoteName && headerDerivedName) {
1526
+ ambiguousTargetDirs.push(outputDir);
1527
+ }
1528
+ else if (remoteName) {
1529
+ for (const remoteUrl of remoteUrls) {
1530
+ const name = urlPathBasename(remoteUrl);
1531
+ if (name)
1532
+ targets.push(joinShellPath(outputDir, name));
1533
+ }
1534
+ }
1535
+ return { targets, ambiguousTargetDirs, configDrivenWrite, unknownWriteTarget, writes };
1536
+ }
1537
+ function wgetOutputTargets(tokens, start) {
1538
+ const targets = [];
1539
+ const ambiguousTargetDirs = [];
1540
+ const remoteUrls = [];
1541
+ let directoryPrefix = ".";
1542
+ let writes = false;
1543
+ let headerDerivedName = false;
1544
+ for (let idx = start; idx < tokens.length; idx += 1) {
1545
+ const token = tokens[idx];
1546
+ if (isShellSeparator(token))
1547
+ break;
1548
+ const redirect = redirectionTarget(tokens, idx);
1549
+ if (redirect) {
1550
+ idx += redirect.skip - 1;
1551
+ continue;
1552
+ }
1553
+ if (token === "-O" || token === "--output-document") {
1554
+ writes = true;
1555
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1556
+ targets.push(tokens[idx + 1]);
1557
+ idx += 1;
1558
+ }
1559
+ continue;
1560
+ }
1561
+ if (token.startsWith("--output-document=")) {
1562
+ writes = true;
1563
+ targets.push(token.slice("--output-document=".length));
1564
+ continue;
1565
+ }
1566
+ if (token === "-P" || token === "--directory-prefix") {
1567
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1568
+ directoryPrefix = tokens[idx + 1];
1569
+ idx += 1;
1570
+ }
1571
+ continue;
1572
+ }
1573
+ const attachedPrefix = shortOptionAttachedValue(token, "P");
1574
+ if (attachedPrefix) {
1575
+ directoryPrefix = attachedPrefix;
1576
+ continue;
1577
+ }
1578
+ if (token.startsWith("--directory-prefix=")) {
1579
+ directoryPrefix = token.slice("--directory-prefix=".length);
1580
+ continue;
1581
+ }
1582
+ if (token === "--content-disposition") {
1583
+ headerDerivedName = true;
1584
+ continue;
1585
+ }
1586
+ if (token.startsWith("-O") && token.length > 2) {
1587
+ writes = true;
1588
+ targets.push(token.slice(2));
1589
+ continue;
1590
+ }
1591
+ if (!token.startsWith("-"))
1592
+ remoteUrls.push(token);
1593
+ }
1594
+ if (targets.length === 0 && remoteUrls.length > 0) {
1595
+ writes = true;
1596
+ if (headerDerivedName) {
1597
+ ambiguousTargetDirs.push(directoryPrefix);
1598
+ }
1599
+ else {
1600
+ for (const remoteUrl of remoteUrls) {
1601
+ const name = urlPathBasename(remoteUrl);
1602
+ if (name)
1603
+ targets.push(joinShellPath(directoryPrefix, name));
1604
+ }
1605
+ }
1606
+ }
1607
+ return { targets, ambiguousTargetDirs, writes };
1608
+ }
1609
+ function stripTarMemberPath(member, stripComponents) {
1610
+ if (stripComponents <= 0)
1611
+ return member;
1612
+ const parts = member.split(/[\\/]+/u).filter((part) => part && part !== ".");
1613
+ if (parts.length <= stripComponents)
1614
+ return null;
1615
+ return parts.slice(stripComponents).join("/");
1616
+ }
1617
+ function tarTargets(tokens, start) {
1618
+ const archiveTargets = [];
1619
+ const extractionTargets = [];
1620
+ const ambiguousTargetDirs = [];
1621
+ const archiveFiles = [];
1622
+ const memberTargets = [];
1623
+ let directory = ".";
1624
+ let stripComponents = 0;
1625
+ let extractMode = false;
1626
+ let archiveWriteMode = false;
1627
+ let pendingFile = false;
1628
+ let pendingDirectory = false;
1629
+ let pendingStripComponents = false;
1630
+ let sawOptionCluster = false;
1631
+ let transformUnknown = false;
1632
+ const rememberMember = (member) => {
1633
+ const stripped = stripTarMemberPath(member, stripComponents);
1634
+ if (stripped)
1635
+ memberTargets.push(joinShellPath(directory, stripped));
1636
+ else
1637
+ ambiguousTargetDirs.push(directory);
1638
+ };
1639
+ const handleOptionCluster = (token) => {
1640
+ sawOptionCluster = true;
1641
+ if (/[cruAd]/u.test(token))
1642
+ archiveWriteMode = true;
1643
+ if (token.includes("x"))
1644
+ extractMode = true;
1645
+ const attachedDirectory = shortOptionAttachedValue(token, "C");
1646
+ if (attachedDirectory) {
1647
+ directory = attachedDirectory;
1648
+ return;
1649
+ }
1650
+ const attachedFile = shortOptionAttachedValue(token, "f");
1651
+ if (attachedFile) {
1652
+ if (attachedFile !== "-")
1653
+ archiveFiles.push(attachedFile);
1654
+ }
1655
+ else if (token.includes("f")) {
1656
+ pendingFile = true;
1657
+ }
1658
+ };
1659
+ const handleOldStyleCluster = (cluster, idx) => {
1660
+ sawOptionCluster = true;
1661
+ let cursor = idx;
1662
+ for (const option of cluster) {
1663
+ if (/[cruAd]/u.test(option))
1664
+ archiveWriteMode = true;
1665
+ if (option === "x")
1666
+ extractMode = true;
1667
+ if (option === "C") {
1668
+ if (tokens[cursor + 1] && !isShellSeparator(tokens[cursor + 1])) {
1669
+ cursor += 1;
1670
+ directory = tokens[cursor];
1671
+ }
1672
+ else {
1673
+ pendingDirectory = true;
1674
+ }
1675
+ continue;
1676
+ }
1677
+ if (option === "f") {
1678
+ if (tokens[cursor + 1] && !isShellSeparator(tokens[cursor + 1])) {
1679
+ cursor += 1;
1680
+ if (tokens[cursor] !== "-")
1681
+ archiveFiles.push(tokens[cursor]);
1682
+ }
1683
+ else {
1684
+ pendingFile = true;
1685
+ }
1686
+ }
1687
+ }
1688
+ return cursor;
1689
+ };
1690
+ for (let idx = start; idx < tokens.length; idx += 1) {
1691
+ const token = tokens[idx];
1692
+ if (isShellSeparator(token))
1693
+ break;
1694
+ if (token === "}")
1695
+ continue;
1696
+ const redirect = redirectionTarget(tokens, idx);
1697
+ if (redirect) {
1698
+ idx += redirect.skip - 1;
1699
+ continue;
1700
+ }
1701
+ if (pendingFile) {
1702
+ if (token !== "-")
1703
+ archiveFiles.push(token);
1704
+ pendingFile = false;
1705
+ continue;
1706
+ }
1707
+ if (pendingDirectory) {
1708
+ directory = token;
1709
+ pendingDirectory = false;
1710
+ continue;
1711
+ }
1712
+ if (pendingStripComponents) {
1713
+ const parsed = Number.parseInt(token, 10);
1714
+ if (Number.isFinite(parsed) && parsed >= 0)
1715
+ stripComponents = parsed;
1716
+ else
1717
+ transformUnknown = true;
1718
+ pendingStripComponents = false;
1719
+ continue;
1720
+ }
1721
+ if (token === "--extract" || token === "--get") {
1722
+ extractMode = true;
1723
+ continue;
1724
+ }
1725
+ if (["--create", "--append", "--update", "--concatenate", "--catenate", "--delete"].includes(token)) {
1726
+ archiveWriteMode = true;
1727
+ continue;
1728
+ }
1729
+ if (token === "-f" || token === "--file") {
1730
+ pendingFile = true;
1731
+ continue;
1732
+ }
1733
+ if (token.startsWith("--file=")) {
1734
+ const file = token.slice("--file=".length);
1735
+ if (file !== "-")
1736
+ archiveFiles.push(file);
1737
+ continue;
1738
+ }
1739
+ if (token === "-C" || token === "--directory") {
1740
+ pendingDirectory = true;
1741
+ continue;
1742
+ }
1743
+ if (token.startsWith("--directory=")) {
1744
+ directory = token.slice("--directory=".length);
1745
+ continue;
1746
+ }
1747
+ if (token === "--strip-components") {
1748
+ pendingStripComponents = true;
1749
+ continue;
1750
+ }
1751
+ if (token.startsWith("--strip-components=")) {
1752
+ const parsed = Number.parseInt(token.slice("--strip-components=".length), 10);
1753
+ if (Number.isFinite(parsed) && parsed >= 0)
1754
+ stripComponents = parsed;
1755
+ else
1756
+ transformUnknown = true;
1757
+ continue;
1758
+ }
1759
+ if (token === "--transform" || token === "--xform") {
1760
+ transformUnknown = true;
1761
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
1762
+ idx += 1;
1763
+ continue;
1764
+ }
1765
+ if (token.startsWith("--transform=") || token.startsWith("--xform=")) {
1766
+ transformUnknown = true;
1767
+ continue;
1768
+ }
1769
+ if (/^-[^-]/u.test(token)) {
1770
+ handleOptionCluster(token);
1771
+ continue;
1772
+ }
1773
+ if (!sawOptionCluster && /^[A-Za-z]+$/u.test(token) && /[ctxruAdfC]/u.test(token)) {
1774
+ idx = handleOldStyleCluster(token, idx);
1775
+ continue;
1776
+ }
1777
+ rememberMember(token);
1778
+ }
1779
+ if (archiveWriteMode)
1780
+ archiveTargets.push(...archiveFiles);
1781
+ if (extractMode) {
1782
+ if (transformUnknown)
1783
+ ambiguousTargetDirs.push(directory);
1784
+ if (memberTargets.length > 0) {
1785
+ extractionTargets.push(...memberTargets);
1786
+ }
1787
+ else if (!transformUnknown) {
1788
+ ambiguousTargetDirs.push(directory);
1789
+ }
1790
+ }
1791
+ return { archiveTargets, extractionTargets, ambiguousTargetDirs };
1792
+ }
1793
+ function unzipTargets(tokens, start) {
1794
+ const targets = [];
1795
+ const ambiguousTargetDirs = [];
1796
+ const members = [];
1797
+ let directory = ".";
1798
+ let archiveSeen = false;
1799
+ for (let idx = start; idx < tokens.length; idx += 1) {
1800
+ const token = tokens[idx];
1801
+ if (isShellSeparator(token))
1802
+ break;
1803
+ if (token === "}")
1804
+ continue;
1805
+ const redirect = redirectionTarget(tokens, idx);
1806
+ if (redirect) {
1807
+ idx += redirect.skip - 1;
1808
+ continue;
1809
+ }
1810
+ if (token === "-d") {
1811
+ if (tokens[idx + 1] && !isShellSeparator(tokens[idx + 1])) {
1812
+ directory = tokens[idx + 1];
1813
+ idx += 1;
1814
+ }
1815
+ continue;
1816
+ }
1817
+ const attachedDirectory = shortOptionAttachedValue(token, "d");
1818
+ if (attachedDirectory) {
1819
+ directory = attachedDirectory;
1820
+ continue;
1821
+ }
1822
+ if (token.startsWith("-") && token !== "-")
1823
+ continue;
1824
+ if (!archiveSeen) {
1825
+ archiveSeen = true;
1826
+ continue;
1827
+ }
1828
+ members.push(token);
1829
+ }
1830
+ if (!archiveSeen)
1831
+ return { targets, ambiguousTargetDirs };
1832
+ if (members.length > 0) {
1833
+ for (const member of members)
1834
+ targets.push(joinShellPath(directory, member));
1835
+ }
1836
+ else {
1837
+ ambiguousTargetDirs.push(directory);
1838
+ }
1839
+ return { targets, ambiguousTargetDirs };
1840
+ }
1841
+ function shellTokensIncludeWrite(tokens) {
1842
+ for (let segmentStart = 0; segmentStart < tokens.length;) {
1843
+ if (isShellSeparator(tokens[segmentStart])) {
1844
+ segmentStart += 1;
1845
+ continue;
1846
+ }
1847
+ const segmentEnd = shellCommandEnd(tokens, segmentStart);
1848
+ for (let idx = segmentStart; idx < segmentEnd; idx += 1) {
1849
+ const redirect = redirectionTarget(tokens, idx);
1850
+ if (redirect?.target)
1851
+ return true;
1852
+ if (redirect) {
1853
+ idx += redirect.skip - 1;
1854
+ continue;
1855
+ }
1856
+ }
1857
+ const commandIdx = shellCommandIndexInSegment(tokens, segmentStart, segmentEnd);
1858
+ if (commandIdx === null) {
1859
+ segmentStart = segmentEnd;
1860
+ continue;
1861
+ }
1862
+ const name = shellCommandName(tokens[commandIdx]);
1863
+ if (name === "sed") {
1864
+ if (sedInPlaceTargets(tokens, commandIdx + 1).hasInPlace)
1865
+ return true;
1866
+ }
1867
+ else if (name === "find") {
1868
+ if (findHasDelete(tokens, commandIdx, segmentEnd))
1869
+ return true;
1870
+ }
1871
+ else if (name === "curl") {
1872
+ if (curlOutputTargets(tokens, commandIdx + 1).writes)
1873
+ return true;
1874
+ }
1875
+ else if (name === "wget") {
1876
+ if (wgetOutputTargets(tokens, commandIdx + 1).writes)
1877
+ return true;
1878
+ }
1879
+ else if (["tee", "mv", "cp", "rm", "install", "ln", "mkdir", "touch", "truncate", "dd", "tar", "unzip", "rsync"].includes(name)) {
1880
+ return true;
1881
+ }
1882
+ segmentStart = segmentEnd;
1883
+ }
1884
+ return false;
1885
+ }
1886
+ function addCurlConfigWriteReason(reasons) {
1887
+ if (!reasons)
1888
+ return;
1889
+ if (reasons.some((item) => item.code === "curl_config_write_target_unknown"))
1890
+ return;
1891
+ reasons.push(reason("curl_config_write_target_unknown", "curl --config/-K can hide output paths from the hook classifier"));
1892
+ }
1893
+ function addCurlWriteTargetUnknownReason(reasons) {
1894
+ if (!reasons)
1895
+ return;
1896
+ if (reasons.some((item) => item.code === "curl_write_target_unknown"))
1897
+ return;
1898
+ reasons.push(reason("curl_write_target_unknown", "curl write option is missing a concrete output path"));
1899
+ }
1900
+ function shellCdTarget(tokens, start, end) {
1901
+ let optionsEnded = false;
1902
+ for (let idx = start; idx < end; idx += 1) {
1903
+ const token = tokens[idx];
1904
+ const redirect = redirectionTarget(tokens, idx);
1905
+ if (redirect) {
1906
+ idx += redirect.skip - 1;
1907
+ continue;
1908
+ }
1909
+ if (!optionsEnded && token === "--") {
1910
+ optionsEnded = true;
1911
+ continue;
1912
+ }
1913
+ if (!optionsEnded && token !== "-" && token.startsWith("-"))
1914
+ continue;
1915
+ return token;
1916
+ }
1917
+ return null;
1918
+ }
1919
+ function firstShellArgument(tokens, start, end, optionsWithValues = new Set()) {
1920
+ let optionsEnded = false;
1921
+ for (let idx = start; idx < end; idx += 1) {
1922
+ const token = tokens[idx];
1923
+ if (token === "}")
1924
+ continue;
1925
+ const redirect = redirectionTarget(tokens, idx);
1926
+ if (redirect) {
1927
+ idx += redirect.skip - 1;
1928
+ continue;
1929
+ }
1930
+ if (!optionsEnded && token === "--") {
1931
+ optionsEnded = true;
1932
+ continue;
1933
+ }
1934
+ if (!optionsEnded && token.startsWith("--")) {
1935
+ if (optionsWithValues.has(token) && tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
1936
+ idx += 1;
1937
+ continue;
1938
+ }
1939
+ if (!optionsEnded && /^-[^-]/u.test(token)) {
1940
+ if ([...optionsWithValues].some((option) => token === option) && tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
1941
+ idx += 1;
1942
+ continue;
1943
+ }
1944
+ return token;
1945
+ }
1946
+ return null;
1947
+ }
1948
+ function shellCwdAfterCd(target, cwd, repoRoot) {
1949
+ if (!target || target === "-" || target.startsWith("~") || /[*?\[{\$`]/u.test(target)) {
1950
+ return resolve(repoRoot, "..");
1951
+ }
1952
+ return isAbsolute(target) ? target : resolve(cwd, target);
1953
+ }
1954
+ function pushShellCommandOperandPaths(command, paths, cwd, repoRoot, reasons, depth = 0) {
1955
+ const tokens = shellTokens(command);
1956
+ let currentCwd = cwd;
1957
+ let nestedShellWrite = false;
1958
+ let localShellWrite = false;
1959
+ for (let segmentStart = 0; segmentStart < tokens.length;) {
1960
+ if (isShellSeparator(tokens[segmentStart])) {
1961
+ segmentStart += 1;
1962
+ continue;
1963
+ }
1964
+ const segmentEnd = shellCommandEnd(tokens, segmentStart);
1965
+ pushShellRedirectionPaths(tokens, paths, currentCwd, repoRoot, reasons, segmentStart, segmentEnd);
1966
+ const commandIdx = shellCommandIndexInSegment(tokens, segmentStart, segmentEnd);
1967
+ if (commandIdx !== null) {
1968
+ const name = shellCommandName(tokens[commandIdx]);
1969
+ if (depth < 3) {
1970
+ for (const nestedCommand of nestedExecutableCommandsAt(tokens, commandIdx, segmentEnd, currentCwd, repoRoot)) {
1971
+ const nested = extractShellPaths(nestedCommand.command, nestedCommand.cwd ?? currentCwd, repoRoot, reasons ?? [], depth + 1);
1972
+ for (const path of nested.paths)
1973
+ paths.add(path);
1974
+ nestedShellWrite = nestedShellWrite || nested.shell_write_command;
1975
+ if (nested.shell_write_command) {
1976
+ for (const rawParentPath of nestedCommand.possibleParentRawPaths ?? []) {
1977
+ addDestructiveTrustRootReason(reasons, rawParentPath, currentCwd, repoRoot);
1978
+ }
1979
+ for (const parentPath of nestedCommand.possibleParentPaths ?? []) {
1980
+ addDestructiveTrustRootReason(reasons, parentPath, currentCwd, repoRoot);
1981
+ paths.add(parentPath);
1982
+ }
1983
+ }
1984
+ }
1985
+ }
1986
+ if (name === "find") {
1987
+ const roots = findDeleteRoots(tokens, commandIdx, segmentEnd);
1988
+ if (roots.length > 0) {
1989
+ localShellWrite = true;
1990
+ for (const root of roots) {
1991
+ addDestructiveTrustRootReason(reasons, root, currentCwd, repoRoot);
1992
+ pushShellPath(paths, root, currentCwd, repoRoot, reasons);
1993
+ }
1994
+ }
1995
+ }
1996
+ else if (name === "tee") {
1997
+ for (const target of teeTargets(tokens, commandIdx + 1))
1998
+ pushShellPath(paths, target, currentCwd, repoRoot, reasons);
1999
+ }
2000
+ else if (name === "sed") {
2001
+ for (const target of sedInPlaceTargets(tokens, commandIdx + 1).targets)
2002
+ pushShellPath(paths, target, currentCwd, repoRoot, reasons);
2003
+ }
2004
+ else if (name === "rsync") {
2005
+ const { operands, optionTargets, optionTargetDirs, removeSourceFiles, deleteExtraneous } = rsyncTargets(tokens, commandIdx + 1);
2006
+ for (const optionTarget of optionTargets)
2007
+ pushShellPath(paths, optionTarget, currentCwd, repoRoot, reasons);
2008
+ for (const optionTargetDir of optionTargetDirs) {
2009
+ pushShellPath(paths, optionTargetDir, currentCwd, repoRoot, reasons);
2010
+ pushTargetDirectoryPaths(paths, optionTargetDir, operands, currentCwd, repoRoot, reasons);
2011
+ }
2012
+ if (removeSourceFiles) {
2013
+ for (const source of operands.slice(0, -1)) {
2014
+ addDestructiveTrustRootReason(reasons, source, currentCwd, repoRoot);
2015
+ pushShellPath(paths, source, currentCwd, repoRoot, reasons);
2016
+ }
2017
+ }
2018
+ const target = operands.at(-1);
2019
+ if (target) {
2020
+ if (deleteExtraneous)
2021
+ addDestructiveTrustRootReason(reasons, target, currentCwd, repoRoot);
2022
+ pushShellPath(paths, target, currentCwd, repoRoot, reasons);
2023
+ if (operands.length > 1)
2024
+ pushTargetDirectoryPaths(paths, target, operands.slice(0, -1), currentCwd, repoRoot, reasons);
2025
+ }
2026
+ }
2027
+ else if (name === "curl") {
2028
+ const output = curlOutputTargets(tokens, commandIdx + 1);
2029
+ localShellWrite = localShellWrite || output.writes;
2030
+ if (output.configDrivenWrite)
2031
+ addCurlConfigWriteReason(reasons);
2032
+ if (output.unknownWriteTarget)
2033
+ addCurlWriteTargetUnknownReason(reasons);
2034
+ for (const target of output.targets)
2035
+ pushShellPath(paths, target, currentCwd, repoRoot, reasons);
2036
+ for (const targetDir of output.ambiguousTargetDirs) {
2037
+ addDestructiveTrustRootReason(reasons, targetDir, currentCwd, repoRoot);
2038
+ pushShellPath(paths, targetDir, currentCwd, repoRoot, reasons);
2039
+ }
2040
+ }
2041
+ else if (name === "wget") {
2042
+ const output = wgetOutputTargets(tokens, commandIdx + 1);
2043
+ localShellWrite = localShellWrite || output.writes;
2044
+ for (const target of output.targets)
2045
+ pushShellPath(paths, target, currentCwd, repoRoot, reasons);
2046
+ for (const targetDir of output.ambiguousTargetDirs) {
2047
+ addDestructiveTrustRootReason(reasons, targetDir, currentCwd, repoRoot);
2048
+ pushShellPath(paths, targetDir, currentCwd, repoRoot, reasons);
2049
+ }
2050
+ }
2051
+ else if (name === "dd") {
2052
+ for (let argIdx = commandIdx + 1; argIdx < segmentEnd; argIdx += 1) {
2053
+ const token = tokens[argIdx];
2054
+ const redirect = redirectionTarget(tokens, argIdx);
2055
+ if (redirect) {
2056
+ argIdx += redirect.skip - 1;
2057
+ continue;
2058
+ }
2059
+ if (token.startsWith("of="))
2060
+ pushShellPath(paths, token.slice("of=".length), currentCwd, repoRoot, reasons);
2061
+ }
2062
+ }
2063
+ else if (name === "tar") {
2064
+ const { archiveTargets, extractionTargets, ambiguousTargetDirs } = tarTargets(tokens, commandIdx + 1);
2065
+ for (const target of archiveTargets)
2066
+ pushShellPath(paths, target, currentCwd, repoRoot, reasons);
2067
+ for (const target of extractionTargets)
2068
+ pushShellPath(paths, target, currentCwd, repoRoot, reasons);
2069
+ for (const targetDir of ambiguousTargetDirs) {
2070
+ addDestructiveTrustRootReason(reasons, targetDir, currentCwd, repoRoot);
2071
+ pushShellPath(paths, targetDir, currentCwd, repoRoot, reasons);
2072
+ }
2073
+ }
2074
+ else if (name === "unzip") {
2075
+ const { targets, ambiguousTargetDirs } = unzipTargets(tokens, commandIdx + 1);
2076
+ for (const target of targets)
2077
+ pushShellPath(paths, target, currentCwd, repoRoot, reasons);
2078
+ for (const targetDir of ambiguousTargetDirs) {
2079
+ addDestructiveTrustRootReason(reasons, targetDir, currentCwd, repoRoot);
2080
+ pushShellPath(paths, targetDir, currentCwd, repoRoot, reasons);
2081
+ }
2082
+ }
2083
+ else if (["cp", "mv", "rm", "install", "ln", "mkdir", "touch", "truncate"].includes(name)) {
2084
+ const { operands, targetDirs } = shellCommandPathOperands(tokens, commandIdx + 1);
2085
+ if (name === "ln") {
2086
+ for (const source of (targetDirs.length > 0 ? operands : operands.slice(0, -1))) {
2087
+ addTrustRootLinkSourceReason(reasons, source, currentCwd, repoRoot);
2088
+ }
2089
+ for (const targetDir of targetDirs)
2090
+ pushTargetDirectoryPaths(paths, targetDir, operands, currentCwd, repoRoot, reasons);
2091
+ if (targetDirs.length === 0)
2092
+ pushCopyLikeTargetPaths(paths, operands, currentCwd, repoRoot, reasons);
2093
+ }
2094
+ else if (["cp", "install"].includes(name)) {
2095
+ for (const targetDir of targetDirs)
2096
+ pushTargetDirectoryPaths(paths, targetDir, operands, currentCwd, repoRoot, reasons);
2097
+ if (targetDirs.length === 0)
2098
+ pushCopyLikeTargetPaths(paths, operands, currentCwd, repoRoot, reasons);
2099
+ }
2100
+ else if (name === "mv") {
2101
+ for (const source of (targetDirs.length > 0 ? operands : operands.slice(0, -1))) {
2102
+ addDestructiveTrustRootReason(reasons, source, currentCwd, repoRoot);
2103
+ pushShellPath(paths, source, currentCwd, repoRoot);
2104
+ }
2105
+ for (const targetDir of targetDirs)
2106
+ pushTargetDirectoryPaths(paths, targetDir, operands, currentCwd, repoRoot, reasons);
2107
+ if (targetDirs.length === 0)
2108
+ pushCopyLikeTargetPaths(paths, operands, currentCwd, repoRoot, reasons);
2109
+ }
2110
+ else if (name === "rm") {
2111
+ for (const operand of operands) {
2112
+ addDestructiveTrustRootReason(reasons, operand, currentCwd, repoRoot);
2113
+ pushShellPath(paths, operand, currentCwd, repoRoot, reasons);
2114
+ }
2115
+ }
2116
+ else {
2117
+ for (const operand of operands)
2118
+ pushShellPath(paths, operand, currentCwd, repoRoot, reasons);
2119
+ }
2120
+ }
2121
+ }
2122
+ if (commandIdx !== null && shellCommandName(tokens[commandIdx]) === "cd") {
2123
+ currentCwd = shellCwdAfterCd(shellCdTarget(tokens, commandIdx + 1, segmentEnd), currentCwd, repoRoot);
2124
+ }
2125
+ segmentStart = segmentEnd;
2126
+ }
2127
+ return nestedShellWrite || localShellWrite;
2128
+ }
2129
+ function pushTargetDirectoryPaths(paths, targetDir, operands, cwd, repoRoot, reasons) {
2130
+ pushShellPath(paths, targetDir, cwd, repoRoot, reasons);
2131
+ for (const operand of operands) {
2132
+ const name = basename(operand.replace(/[\\/]+$/u, ""));
2133
+ if (name && name !== "." && name !== "..")
2134
+ pushShellPath(paths, `${targetDir.replace(/[\\/]+$/u, "")}/${name}`, cwd, repoRoot, reasons);
2135
+ }
2136
+ }
2137
+ function pushCopyLikeTargetPaths(paths, operands, cwd, repoRoot, reasons) {
2138
+ const target = operands.at(-1);
2139
+ if (!target)
2140
+ return;
2141
+ pushShellPath(paths, target, cwd, repoRoot, reasons);
2142
+ if (operands.length <= 1)
2143
+ return;
2144
+ pushTargetDirectoryPaths(paths, target, operands.slice(0, -1), cwd, repoRoot, reasons);
2145
+ }
2146
+ function extractPatchPaths(command, cwd, repoRoot, reasons) {
2147
+ const paths = new Set();
2148
+ for (const line of command.split(/\r?\n/u)) {
2149
+ const trimmed = line.trim();
2150
+ const direct = trimmed.match(/^\*\*\*\s+(?:Add|Update|Delete) File:\s+(.+?)\s*$/u);
2151
+ if (direct) {
2152
+ pushPath(paths, direct[1], cwd, repoRoot, {}, reasons);
2153
+ continue;
2154
+ }
2155
+ const move = trimmed.match(/^\*\*\*\s+Move to:\s+(.+?)\s*$/u);
2156
+ if (move) {
2157
+ pushPath(paths, move[1], cwd, repoRoot, {}, reasons);
2158
+ continue;
2159
+ }
2160
+ const diff = trimmed.match(/^(?:---|\+\+\+)\s+([ab]\/.+?|\/dev\/null)\s*$/u);
2161
+ if (diff)
2162
+ pushPath(paths, diff[1], cwd, repoRoot, { stripDiffPrefix: true }, reasons);
2163
+ }
2164
+ return [...paths].sort();
2165
+ }
2166
+ function extractToolInputPaths(event, cwd, repoRoot, reasons) {
2167
+ const toolInput = isObject(event.tool_input) ? event.tool_input : {};
2168
+ const paths = new Set();
2169
+ for (const key of ["path", "file_path", "filePath", "target", "target_path", "targetPath"]) {
2170
+ const value = toolInput[key];
2171
+ if (typeof value === "string")
2172
+ pushPath(paths, value, cwd, repoRoot, {}, reasons);
2173
+ }
2174
+ for (const key of ["paths", "files", "target_paths", "targetPaths"]) {
2175
+ const values = toolInput[key];
2176
+ if (!Array.isArray(values))
2177
+ continue;
2178
+ for (const value of values)
2179
+ if (typeof value === "string")
2180
+ pushPath(paths, value, cwd, repoRoot, {}, reasons);
2181
+ }
2182
+ return [...paths].sort();
2183
+ }
2184
+ function interpreterInlineWriteCommand(command) {
2185
+ const tokens = shellTokens(command);
2186
+ for (let segmentStart = 0; segmentStart < tokens.length;) {
2187
+ if (isShellSeparator(tokens[segmentStart])) {
2188
+ segmentStart += 1;
2189
+ continue;
2190
+ }
2191
+ const segmentEnd = shellCommandEnd(tokens, segmentStart);
2192
+ const commandIdx = shellCommandIndexInSegment(tokens, segmentStart, segmentEnd);
2193
+ if (commandIdx === null) {
2194
+ segmentStart = segmentEnd;
2195
+ continue;
2196
+ }
2197
+ const name = shellCommandName(tokens[commandIdx]);
2198
+ if (!["node", "python", "python3", "perl", "ruby"].includes(name)) {
2199
+ segmentStart = segmentEnd;
2200
+ continue;
2201
+ }
2202
+ const snippets = [];
2203
+ for (let argIdx = commandIdx + 1; argIdx < segmentEnd; argIdx += 1) {
2204
+ const token = tokens[argIdx];
2205
+ const redirect = redirectionTarget(tokens, argIdx);
2206
+ if (redirect) {
2207
+ argIdx += redirect.skip - 1;
2208
+ continue;
2209
+ }
2210
+ if (token === "-e" || token === "-c" || token === "--eval") {
2211
+ if (tokens[argIdx + 1])
2212
+ snippets.push(tokens[argIdx + 1]);
2213
+ argIdx += 1;
2214
+ continue;
2215
+ }
2216
+ if (token.startsWith("--eval="))
2217
+ snippets.push(token.slice("--eval=".length));
2218
+ }
2219
+ if (snippets.some((snippet) => /write(?:file)?|append(?:file)?|rmSync|unlink|mkdir|rmdir|rename|copyFile|openSync|createWriteStream|write_text|File\.write|IO\.write/u.test(snippet))) {
2220
+ return true;
2221
+ }
2222
+ segmentStart = segmentEnd;
2223
+ }
2224
+ return false;
2225
+ }
2226
+ function executableTextWriteCommand(text) {
2227
+ return /write(?:file)?|append(?:file)?|write_text|rmSync|unlink|mkdir|rmdir|rename|copyFile|openSync|createWriteStream|File\.write|IO\.write/u.test(text)
2228
+ || /(?:^|[;&|]\s*)(?:find\b[^;&|]*\s-delete\b|rsync\b|curl\b[^;&|]*(?:\s-o\s|\s--output(?:=|\s)|\s-O\b|\s--remote-name\b|\s-D\s|\s--dump-header(?:=|\s)|\s-c\s|\s--cookie-jar(?:=|\s)|\s--trace(?:=|\s)|\s--trace-ascii(?:=|\s)|\s--stderr(?:=|\s)|\s--libcurl(?:=|\s)|\s--etag-save(?:=|\s))|wget\b[^;&|]*(?:\s-O\s|\s--output-document(?:=|\s)|\s-P\s|\s--directory-prefix(?:=|\s))|sed\s+[^;&|]*\s-i\b|tee\b|mv\b|cp\b|rm\b|install\b|ln\b|mkdir\b|touch\b|truncate\b|dd\b|tar\b|unzip\b)|(?:>|>>)\s*[^&\s]/u.test(text);
2229
+ }
2230
+ function executableTextValues(value, keyHint = "") {
2231
+ const textKeys = new Set(["command", "cmd", "code", "script", "source", "input", "expression", "args", "arguments"]);
2232
+ if (typeof value === "string")
2233
+ return textKeys.has(keyHint) ? [value] : [];
2234
+ if (Array.isArray(value))
2235
+ return value.flatMap((item) => (typeof item === "string" && textKeys.has(keyHint)
2236
+ ? [item]
2237
+ : executableTextValues(item, keyHint)));
2238
+ if (!isObject(value))
2239
+ return [];
2240
+ return Object.entries(value).flatMap(([key, child]) => executableTextValues(child, key));
2241
+ }
2242
+ function trustRootTextMatches(text) {
2243
+ const lower = text.toLowerCase();
2244
+ const matches = new Set();
2245
+ if (/(?:^|[^a-z0-9_.-])\.codex\/superspec(?:\/|(?=$|[^a-z0-9_.-]))/u.test(lower)) {
2246
+ matches.add(".codex/superspec");
2247
+ }
2248
+ if (/(?:^|[^a-z0-9_.-])\.superspec(?:\/|(?=$|[^a-z0-9_.-]))/u.test(lower)) {
2249
+ matches.add(".superspec");
2250
+ }
2251
+ if (/openspec\/changes\/[^'"\s;&|]+\/\.superspec(?:\/|(?=$|[^a-z0-9_.-]))/u.test(lower)) {
2252
+ matches.add("openspec/changes/*/.superspec");
2253
+ }
2254
+ if (/\b(?:rmsync|rmdirsync|unlinksync|rm|rmdir|unlink)\s*\(\s*['"]\.codex['"]/u.test(lower)) {
2255
+ matches.add(".codex");
2256
+ }
2257
+ if (/\b(?:rmsync|rmdirsync|unlinksync|rm|rmdir|unlink)\s*\(\s*['"]openspec\/changes(?:\/[^/'"]+)?['"]/u.test(lower)) {
2258
+ matches.add("openspec/changes");
2259
+ }
2260
+ return [...matches].sort();
2261
+ }
2262
+ function extractShellPaths(command, cwd, repoRoot, reasons, depth = 0) {
2263
+ const paths = new Set();
2264
+ const tokens = shellTokens(command);
2265
+ const nestedShellWrite = pushShellCommandOperandPaths(command, paths, cwd, repoRoot, reasons, depth);
2266
+ let substitutionShellWrite = false;
2267
+ if (depth < 3) {
2268
+ for (const nestedCommand of commandSubstitutions(command)) {
2269
+ const nested = extractShellPaths(nestedCommand, cwd, repoRoot, reasons, depth + 1);
2270
+ for (const path of nested.paths)
2271
+ paths.add(path);
2272
+ substitutionShellWrite = substitutionShellWrite || nested.shell_write_command;
2273
+ }
2274
+ }
2275
+ const shellWrite = shellTokensIncludeWrite(tokens) || interpreterInlineWriteCommand(command) || nestedShellWrite || substitutionShellWrite;
2276
+ return { paths: [...paths].sort(), shell_write_command: shellWrite };
2277
+ }
2278
+ function taskCheckboxTransitions(command) {
2279
+ const removedUnchecked = new Set();
2280
+ const removedChecked = new Set();
2281
+ const addedUnchecked = new Set();
2282
+ const addedChecked = new Set();
2283
+ for (const line of command.split(/\r?\n/u)) {
2284
+ const sign = line.startsWith("-") ? "-" : line.startsWith("+") ? "+" : "";
2285
+ if (!sign)
2286
+ continue;
2287
+ const body = line.slice(1).trimStart();
2288
+ const match = body.match(/^(?:-\s+)?\[([ xX])\]\s+(\S+)/u);
2289
+ if (!match)
2290
+ continue;
2291
+ const checked = match[1].toLowerCase() === "x";
2292
+ const taskId = match[2];
2293
+ if (sign === "-" && checked)
2294
+ removedChecked.add(taskId);
2295
+ if (sign === "-" && !checked)
2296
+ removedUnchecked.add(taskId);
2297
+ if (sign === "+" && checked)
2298
+ addedChecked.add(taskId);
2299
+ if (sign === "+" && !checked)
2300
+ addedUnchecked.add(taskId);
2301
+ }
2302
+ return {
2303
+ completions: [...addedChecked].filter((taskId) => removedUnchecked.has(taskId)).sort(),
2304
+ reopens: [...addedUnchecked].filter((taskId) => removedChecked.has(taskId)).sort(),
2305
+ };
2306
+ }
2307
+ function commandLooksLikeArchive(command, depth = 0) {
2308
+ const tokens = shellTokens(command);
2309
+ for (let segmentStart = 0; segmentStart < tokens.length;) {
2310
+ if (isShellSeparator(tokens[segmentStart])) {
2311
+ segmentStart += 1;
2312
+ continue;
2313
+ }
2314
+ const segmentEnd = shellCommandEnd(tokens, segmentStart);
2315
+ const commandIdx = shellCommandIndexInSegment(tokens, segmentStart, segmentEnd);
2316
+ if (commandIdx === null) {
2317
+ segmentStart = segmentEnd;
2318
+ continue;
2319
+ }
2320
+ const name = shellCommandName(tokens[commandIdx]);
2321
+ if (name === "openspec") {
2322
+ const subcommand = firstShellArgument(tokens, commandIdx + 1, segmentEnd);
2323
+ if (subcommand === "archive")
2324
+ return true;
2325
+ }
2326
+ if (name === "mv") {
2327
+ const { operands } = shellCommandPathOperands(tokens, commandIdx + 1);
2328
+ if (operands.some((operand) => /^openspec\/changes\/[^/]+$/u.test(cleanRelPath(operand))))
2329
+ return true;
2330
+ }
2331
+ segmentStart = segmentEnd;
2332
+ }
2333
+ if (depth >= 3)
2334
+ return false;
2335
+ return [...nestedShellCommands(tokens), ...commandSubstitutions(command)].some((nestedCommand) => commandLooksLikeArchive(nestedCommand, depth + 1));
2336
+ }
2337
+ function tokenAtSameCommand(tokens, idx) {
2338
+ if (idx < 0 || idx >= tokens.length || isShellSeparator(tokens[idx]))
2339
+ return null;
2340
+ return tokens[idx];
2341
+ }
2342
+ function isHookEntrypointName(name) {
2343
+ return name === "superspec-guard"
2344
+ || name === "superspec-guard.js"
2345
+ || name === "superspec-hook"
2346
+ || name === "superspec-hook.js";
2347
+ }
2348
+ function isHookAdapterEntrypointName(name) {
2349
+ return name === "superspec-hook" || name === "superspec-hook.js";
2350
+ }
2351
+ function nodeScriptHookEntrypoint(tokens, commandIdx) {
2352
+ const name = shellCommandName(tokens[commandIdx]);
2353
+ if (!["node", "nodejs"].includes(name))
2354
+ return null;
2355
+ for (let idx = commandIdx + 1; idx < tokens.length && !isShellSeparator(tokens[idx]); idx += 1) {
2356
+ const token = tokens[idx];
2357
+ const redirect = redirectionTarget(tokens, idx);
2358
+ if (redirect) {
2359
+ idx += redirect.skip - 1;
2360
+ continue;
2361
+ }
2362
+ if (token === "--")
2363
+ continue;
2364
+ if (token.startsWith("-")) {
2365
+ if (["-r", "--require", "--loader", "--import"].includes(token) && tokens[idx + 1] && !isShellSeparator(tokens[idx + 1]))
2366
+ idx += 1;
2367
+ continue;
2368
+ }
2369
+ const scriptName = shellCommandName(token);
2370
+ return isHookEntrypointName(scriptName) ? { name: scriptName, argStart: idx + 1 } : null;
2371
+ }
2372
+ return null;
2373
+ }
2374
+ function commandLooksLikeInternalHookWriter(command, depth = 0) {
2375
+ const tokens = shellTokens(command);
2376
+ for (let segmentStart = 0; segmentStart < tokens.length;) {
2377
+ if (isShellSeparator(tokens[segmentStart])) {
2378
+ segmentStart += 1;
2379
+ continue;
2380
+ }
2381
+ const segmentEnd = shellCommandEnd(tokens, segmentStart);
2382
+ const commandIdx = shellCommandIndexInSegment(tokens, segmentStart, segmentEnd);
2383
+ if (commandIdx === null) {
2384
+ segmentStart = segmentEnd;
2385
+ continue;
2386
+ }
2387
+ const name = shellCommandName(tokens[commandIdx]);
2388
+ if (name === "superspec") {
2389
+ const next = tokenAtSameCommand(tokens, commandIdx + 1);
2390
+ const afterNext = tokenAtSameCommand(tokens, commandIdx + 2);
2391
+ if (next?.startsWith("hook-record-"))
2392
+ return true;
2393
+ if (next === "guard" && afterNext?.startsWith("hook-record-"))
2394
+ return true;
2395
+ }
2396
+ if (name === "superspec-guard" && tokenAtSameCommand(tokens, commandIdx + 1)?.startsWith("hook-record-"))
2397
+ return true;
2398
+ if (isHookAdapterEntrypointName(name))
2399
+ return true;
2400
+ if (isHookEntrypointName(name) && tokenAtSameCommand(tokens, commandIdx + 1)?.startsWith("hook-record-"))
2401
+ return true;
2402
+ const nodeEntrypoint = nodeScriptHookEntrypoint(tokens, commandIdx);
2403
+ if (nodeEntrypoint && isHookAdapterEntrypointName(nodeEntrypoint.name))
2404
+ return true;
2405
+ if (nodeEntrypoint && tokenAtSameCommand(tokens, nodeEntrypoint.argStart)?.startsWith("hook-record-"))
2406
+ return true;
2407
+ segmentStart = segmentEnd;
2408
+ }
2409
+ if (depth >= 3)
2410
+ return false;
2411
+ return [...nestedShellCommands(tokens), ...commandSubstitutions(command)].some((nestedCommand) => commandLooksLikeInternalHookWriter(nestedCommand, depth + 1));
2412
+ }
2413
+ function commandLooksLikeUnsafeLifecycleTermination(command, depth = 0) {
2414
+ const tokens = shellTokens(command);
2415
+ const dangerousReason = (value) => {
2416
+ const normalized = (value ?? "").replace(/^['"]|['"]$/gu, "");
2417
+ return normalized === "cancelled" || normalized === "abandoned";
2418
+ };
2419
+ const segmentHasDangerousReason = (start, end) => {
2420
+ for (let i = start; i < end; i += 1) {
2421
+ const token = tokens[i];
2422
+ if (token === "--reason" && dangerousReason(tokens[i + 1]))
2423
+ return true;
2424
+ if (token.startsWith("--reason=") && dangerousReason(token.slice("--reason=".length)))
2425
+ return true;
2426
+ }
2427
+ return false;
2428
+ };
2429
+ for (let segmentStart = 0; segmentStart < tokens.length;) {
2430
+ if (isShellSeparator(tokens[segmentStart])) {
2431
+ segmentStart += 1;
2432
+ continue;
2433
+ }
2434
+ const segmentEnd = shellCommandEnd(tokens, segmentStart);
2435
+ const commandIdx = shellCommandIndexInSegment(tokens, segmentStart, segmentEnd);
2436
+ if (commandIdx === null) {
2437
+ segmentStart = segmentEnd;
2438
+ continue;
2439
+ }
2440
+ const token = shellCommandName(tokens[commandIdx]);
2441
+ if (token === "superspec" && tokens[commandIdx + 1] === "guard" && tokens[commandIdx + 2] === "hook-session-end") {
2442
+ if (segmentHasDangerousReason(commandIdx + 3, segmentEnd))
2443
+ return true;
2444
+ }
2445
+ if (token === "superspec" && tokens[commandIdx + 1] === "hook-session-end") {
2446
+ if (segmentHasDangerousReason(commandIdx + 2, segmentEnd))
2447
+ return true;
2448
+ }
2449
+ if (token === "superspec-guard" && tokens[commandIdx + 1] === "hook-session-end") {
2450
+ if (segmentHasDangerousReason(commandIdx + 2, segmentEnd))
2451
+ return true;
2452
+ }
2453
+ if (isHookEntrypointName(token) && tokens[commandIdx + 1] === "hook-session-end") {
2454
+ if (segmentHasDangerousReason(commandIdx + 2, segmentEnd))
2455
+ return true;
2456
+ }
2457
+ const nodeEntrypoint = nodeScriptHookEntrypoint(tokens, commandIdx);
2458
+ if (nodeEntrypoint && tokens[nodeEntrypoint.argStart] === "hook-session-end") {
2459
+ if (segmentHasDangerousReason(nodeEntrypoint.argStart + 1, segmentEnd))
2460
+ return true;
2461
+ }
2462
+ if (depth < 3) {
2463
+ for (const nested of nestedExecutableCommandsAt(tokens, commandIdx, segmentEnd)) {
2464
+ if (commandLooksLikeUnsafeLifecycleTermination(nested.command, depth + 1))
2465
+ return true;
2466
+ }
2467
+ }
2468
+ segmentStart = segmentEnd;
2469
+ }
2470
+ if (depth < 3 && commandSubstitutions(command).some((nestedCommand) => commandLooksLikeUnsafeLifecycleTermination(nestedCommand, depth + 1))) {
2471
+ return true;
2472
+ }
2473
+ return false;
2474
+ }
2475
+ function sameShellCommandArguments(tokens, start, end) {
2476
+ const args = [];
2477
+ for (let idx = start; idx < end; idx += 1) {
2478
+ const token = tokens[idx];
2479
+ if (token === "}")
2480
+ continue;
2481
+ const redirect = redirectionTarget(tokens, idx);
2482
+ if (redirect) {
2483
+ idx += redirect.skip - 1;
2484
+ continue;
2485
+ }
2486
+ args.push(token);
2487
+ }
2488
+ return args;
2489
+ }
2490
+ function npmLikeTestCommand(name, args) {
2491
+ if (name === "npm") {
2492
+ if (args[0] === "test" || args[0] === "t")
2493
+ return true;
2494
+ return args[0] === "run" && /^(?:test(?::[A-Za-z0-9_.-]+)?|typecheck|build)$/u.test(args[1] ?? "");
2495
+ }
2496
+ if (["pnpm", "yarn", "bun"].includes(name)) {
2497
+ if (args[0] === "test")
2498
+ return true;
2499
+ return args[0] === "run" && /^(?:test(?::[A-Za-z0-9_.-]+)?|typecheck|build)$/u.test(args[1] ?? "");
2500
+ }
2501
+ return false;
2502
+ }
2503
+ export function commandLooksLikeTestValidation(command, depth = 0) {
2504
+ const tokens = shellTokens(command);
2505
+ const last = lastShellCommandSegment(tokens);
2506
+ if (!last)
2507
+ return false;
2508
+ const name = shellCommandName(tokens[last.commandIdx]);
2509
+ const args = sameShellCommandArguments(tokens, last.commandIdx + 1, last.segmentEnd);
2510
+ if (name === "openspec" && args[0] === "validate")
2511
+ return true;
2512
+ if (name === "node" && args.includes("--test"))
2513
+ return true;
2514
+ if (npmLikeTestCommand(name, args))
2515
+ return true;
2516
+ if (["pytest", "vitest", "jest", "mocha"].includes(name))
2517
+ return true;
2518
+ if (name === "go" && args[0] === "test")
2519
+ return true;
2520
+ if (name === "cargo" && args[0] === "test")
2521
+ return true;
2522
+ if (name === "tsc" && args.includes("--noEmit"))
2523
+ return true;
2524
+ if (depth >= 3)
2525
+ return false;
2526
+ const nested = nestedShellCommandAt(tokens, last.commandIdx, last.segmentEnd)
2527
+ ?? envSplitCommandAt(tokens, last.commandIdx, last.segmentEnd)
2528
+ ?? evalCommandAt(tokens, last.commandIdx, last.segmentEnd);
2529
+ return nested ? commandLooksLikeTestValidation(nested, depth + 1) : false;
2530
+ }
2531
+ function addExecutableTextShellPaths(texts, paths, cwd, repoRoot, reasons) {
2532
+ let writes = false;
2533
+ for (const text of texts) {
2534
+ const shell = extractShellPaths(text, cwd, repoRoot, reasons);
2535
+ for (const path of shell.paths)
2536
+ paths.add(path);
2537
+ writes = writes || shell.shell_write_command;
2538
+ }
2539
+ return writes;
2540
+ }
2541
+ export function extractWriteIntent(event, repoRoot) {
2542
+ const normalized = normalizeHookEvent(event);
2543
+ const toolInput = isObject(event.tool_input) ? event.tool_input : {};
2544
+ const executableTexts = [normalized.command, ...executableTextValues(toolInput)].filter(Boolean);
2545
+ const textWriteCommand = executableTexts.some((text) => executableTextWriteCommand(text));
2546
+ const trustTextMatches = [...new Set(executableTexts.flatMap((text) => trustRootTextMatches(text)))].sort();
2547
+ const paths = new Set();
2548
+ const tool = normalized.tool_name;
2549
+ const command = normalized.command;
2550
+ const reasons = [];
2551
+ let shellWriteCommand = false;
2552
+ if (tool === "apply_patch" || tool === "Edit" || tool === "Write") {
2553
+ for (const path of extractPatchPaths(command, normalized.cwd, repoRoot, reasons))
2554
+ paths.add(path);
2555
+ for (const path of extractToolInputPaths(event, normalized.cwd, repoRoot, reasons))
2556
+ paths.add(path);
2557
+ }
2558
+ else if (tool === "Bash") {
2559
+ const shell = extractShellPaths(command, normalized.cwd, repoRoot, reasons);
2560
+ for (const path of shell.paths)
2561
+ paths.add(path);
2562
+ shellWriteCommand = shell.shell_write_command || textWriteCommand;
2563
+ }
2564
+ else if (tool.startsWith("mcp__")) {
2565
+ for (const path of extractToolInputPaths(event, normalized.cwd, repoRoot, reasons))
2566
+ paths.add(path);
2567
+ const executableShellWrite = addExecutableTextShellPaths(executableTexts, paths, normalized.cwd, repoRoot, reasons);
2568
+ shellWriteCommand = paths.size > 0 || textWriteCommand || executableShellWrite;
2569
+ reasons.push(reason("unsupported_write_surface", `hook surface ${tool} has no strict write classifier`));
2570
+ }
2571
+ else if (tool) {
2572
+ const inputPaths = extractToolInputPaths(event, normalized.cwd, repoRoot, reasons);
2573
+ for (const path of inputPaths)
2574
+ paths.add(path);
2575
+ const executableShellWrite = addExecutableTextShellPaths(executableTexts, paths, normalized.cwd, repoRoot, reasons);
2576
+ shellWriteCommand = paths.size > 0 || textWriteCommand || executableShellWrite;
2577
+ reasons.push(reason("unsupported_write_surface", `hook surface ${tool} has no strict write classifier`));
2578
+ }
2579
+ if (paths.size === 0 && ["apply_patch", "Edit", "Write"].includes(tool)) {
2580
+ reasons.push(reason("unknown_patch_shape", `${tool} event did not expose concrete target paths`));
2581
+ }
2582
+ const transitions = taskCheckboxTransitions(command);
2583
+ return {
2584
+ target_paths: [...paths].sort(),
2585
+ trust_root_text_matches: trustTextMatches,
2586
+ archive_command: commandLooksLikeArchive(command),
2587
+ internal_hook_writer_command: commandLooksLikeInternalHookWriter(command),
2588
+ unsafe_lifecycle_termination_command: commandLooksLikeUnsafeLifecycleTermination(command),
2589
+ unsupported_write_surface: reasons.some((item) => item.code === "unsupported_write_surface"),
2590
+ task_checkbox_completions: transitions.completions,
2591
+ task_checkbox_reopens: transitions.reopens,
2592
+ shell_write_command: shellWriteCommand,
2593
+ reasons,
2594
+ };
2595
+ }
2596
+ export function eventContentHash(event) {
2597
+ return `sha256:${createHash("sha256").update(JSON.stringify(event), "utf8").digest("hex")}`;
2598
+ }
2599
+ export function nonce() {
2600
+ return randomUUID();
2601
+ }
2602
+ export function relFromChangeOrRepo(repoRoot, changeRoot, pathValue) {
2603
+ const abs = isAbsolute(pathValue) ? pathValue : resolve(repoRoot, pathValue);
2604
+ const changeRel = relative(changeRoot, abs);
2605
+ if (changeRel && !changeRel.startsWith("..") && !isAbsolute(changeRel))
2606
+ return { root: "change", path: toPosix(changeRel) };
2607
+ const repoRel = relative(repoRoot, abs);
2608
+ if (repoRel && !repoRel.startsWith("..") && !isAbsolute(repoRel))
2609
+ return { root: "repo", path: toPosix(repoRel) };
2610
+ return null;
2611
+ }
2612
+ export function pinnedFileRef(baseRoot, relPath) {
2613
+ const target = safe_within(baseRoot, relPath);
2614
+ if (target === null || !existsSync(target) || !statSync(target).isFile())
2615
+ return null;
2616
+ const data = readFileSync(target);
2617
+ return {
2618
+ path: toPosix(relPath),
2619
+ blob_sha: `sha256:${createHash("sha256").update(data).digest("hex")}`,
2620
+ };
2621
+ }
2622
+ export function renderReasons(reasons) {
2623
+ return renderList(reasons.map((item) => item.code).sort());
2624
+ }