@delt/claude-jev-advisor 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/rm-hook.js CHANGED
@@ -4,19 +4,123 @@
4
4
  import os from "os";
5
5
 
6
6
  // src/rm/guard.ts
7
+ import path2 from "path";
8
+
9
+ // src/rm/targets.ts
7
10
  import fs from "fs";
8
11
  import path from "path";
9
12
  import { spawnSync } from "child_process";
10
13
  var win = path.win32;
14
+ var BUILD_DIRS = /* @__PURE__ */ new Set(["target", "build", "dist", "out", "node_modules", "coverage", "__pycache__", ".pytest_cache", ".gradle", ".next", ".nuxt", ".turbo", ".cache", "bin", "obj"]);
15
+ var MAX_LISTED = 3;
16
+ var emptyRead = () => ({ deny: null, targets: [], failed: [], shells: [] });
17
+ var DELETE_WORDS = /(?<![\w.-])(?:rm|rmdir|del|erase|rd|ri|remove-item)(?![\w-])|(?:\.|::)\s*delete\w*/i;
18
+ function isInside(child, parent) {
19
+ const c = child.toLowerCase();
20
+ const p = parent.toLowerCase().replace(/\\+$/, "");
21
+ return c.startsWith(`${p}\\`);
22
+ }
23
+ function segments(p) {
24
+ return p.toLowerCase().split("\\");
25
+ }
26
+ function judgePath(p, pattern, ctx) {
27
+ const shown = pattern === null ? p : `${p.replace(/\\$/, "")}\\${pattern}`;
28
+ const narrowPattern = pattern !== null && !/^[*?.]+$/.test(pattern);
29
+ if (ctx.tmpdirs.some((t) => isInside(p, t) || narrowPattern && p.toLowerCase() === t.toLowerCase().replace(/\\+$/, ""))) return null;
30
+ if (segments(p).includes(".superpowers")) return null;
31
+ if (pattern === null && !ctx.probe.exists(p)) return null;
32
+ if (segments(p).some((s) => BUILD_DIRS.has(s)) && ctx.probe.isIgnored(p)) return null;
33
+ return { shown, path: pattern === null ? p : null };
34
+ }
35
+ function splitPattern(value, wildcard) {
36
+ const cut = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"));
37
+ if (wildcard.test(value.slice(0, Math.max(cut, 0)))) return null;
38
+ return { dir: value.slice(0, Math.max(cut, 0)) || ".", pattern: value.slice(cut + 1) };
39
+ }
40
+ function splitWindowsPattern(value, wildcard) {
41
+ const drive = /^([A-Za-z]:)([^\\/]*)$/.exec(value);
42
+ if (drive) return { dir: drive[1], pattern: drive[2] };
43
+ const split = splitPattern(value, wildcard);
44
+ if (split === null) return null;
45
+ const root = /^((?:[A-Za-z]:)?[\\/])[^\\/]*$/.exec(value);
46
+ return root ? { dir: root[1], pattern: split.pattern } : split;
47
+ }
48
+ function resolveWindowsPath(value, cwd, home) {
49
+ if (/^(?:\\\\|\/\/)/.test(value)) return { unc: value };
50
+ if (home !== null && /^~(?:[\\/]|$)/.test(value)) return { path: win.resolve(home, value.slice(2) || ".") };
51
+ if (/^[A-Za-z]:[\\/]/.test(value)) return { path: win.normalize(value) };
52
+ if (/^[A-Za-z]:/.test(value)) return { unresolvable: `a path relative to another drive's folder (${value})` };
53
+ if (cwd === null) return { unresolvable: `a path relative to a folder that cannot be worked out (${value})` };
54
+ return { path: win.resolve(cwd, value) };
55
+ }
56
+ var EXAMPLES = {
57
+ bash: {
58
+ what: "this rm",
59
+ rules: "no shell variables, command substitutions, xargs, brace expansion or wildcards in folder names, and no cd to such a path before it",
60
+ example: 'rm -f "C:/full/path/file.txt"',
61
+ then: "rm the printed paths"
62
+ },
63
+ powershell: {
64
+ what: "this PowerShell command",
65
+ rules: "no variables other than $env:TEMP, $env:TMP, $env:USERPROFILE, $HOME and $PWD, no expressions, script blocks or pipeline input, no wildcards in folder names, and no Set-Location to such a path before it",
66
+ example: 'Remove-Item -LiteralPath "C:\\full\\path\\file.txt"',
67
+ then: "delete the printed paths"
68
+ },
69
+ cmd: {
70
+ what: "this del or rd",
71
+ rules: "no variables other than %TEMP%, %TMP%, %USERPROFILE% and %CD%, no wildcards in folder names, and no cd to such a path before it",
72
+ example: 'del /q "C:\\full\\path\\file.txt"',
73
+ then: "delete the printed paths"
74
+ }
75
+ };
76
+ function denyReason(shell, why) {
77
+ const { what, rules, example, then } = EXAMPLES[shell];
78
+ return `rm-guard could not work out what ${what} would delete (${why}). Rewrite it with literal paths: ${rules}. Example: ${example}. If you need a variable or a listing to find the paths, run that on its own first, then ${then}.`;
79
+ }
80
+ function listed(targets) {
81
+ const names = targets.slice(0, MAX_LISTED).map((t) => t.shown).join(", ");
82
+ return targets.length > MAX_LISTED ? `${names} \uC678 ${targets.length - MAX_LISTED}\uAC1C` : names;
83
+ }
84
+ var UNREAD_TARGET = "(\uBD84\uC11D\uD558\uC9C0 \uBABB\uD55C \uBA85\uB839)";
85
+ function decisionOf({ deny, targets, failed }) {
86
+ if (deny !== null) return { decision: "deny", reason: deny };
87
+ if (failed.length) {
88
+ const note = `\uC0AD\uC81C \uBA85\uB839\uC744 \uBD84\uC11D\uD558\uC9C0 \uBABB\uD568: ${failed[0]}`;
89
+ return { decision: "ask", reason: targets.length ? `\uC2E4\uC81C \uD30C\uC77C \uC0AD\uC81C: ${listed(targets)} \xB7 ${note}` : note, targets: [...targets, { shown: UNREAD_TARGET, path: null }] };
90
+ }
91
+ if (!targets.length) return null;
92
+ return { decision: "ask", reason: `\uC2E4\uC81C \uD30C\uC77C \uC0AD\uC81C: ${listed(targets)}`, targets };
93
+ }
94
+ function hookOutput({ decision, reason }) {
95
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: decision, permissionDecisionReason: reason } });
96
+ }
97
+ var realProbe = {
98
+ exists(p) {
99
+ try {
100
+ fs.lstatSync(p);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ },
106
+ isIgnored(p) {
107
+ const r = spawnSync("git", ["-C", win.dirname(p), "check-ignore", "-q", "--", p], { timeout: 3e3, windowsHide: true });
108
+ return r.status === 0;
109
+ }
110
+ };
111
+
112
+ // src/rm/guard.ts
113
+ var win2 = path2.win32;
11
114
  var SEPARATORS = /* @__PURE__ */ new Set([";", "&&", "||", "|", "|&", "&", "(", ")"]);
12
115
  var SKIP_WORDS = /* @__PURE__ */ new Set(["then", "else", "elif", "do", "while", "until", "if", "!", "{", "time"]);
13
116
  var HEADER_WORDS = /* @__PURE__ */ new Set(["for", "select", "case"]);
14
117
  var CLOSING_WORDS = /* @__PURE__ */ new Set(["done", "fi", "esac", "}"]);
15
- var BUILD_DIRS = /* @__PURE__ */ new Set(["target", "build", "dist", "out", "node_modules", "coverage", "__pycache__", ".pytest_cache", ".gradle", ".next", ".nuxt", ".turbo", ".cache", "bin", "obj"]);
16
- var MAX_LISTED = 3;
17
118
  var ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=/;
18
119
  var DECLARERS = /* @__PURE__ */ new Set(["export", "readonly", "declare", "typeset", "local"]);
19
120
  var KNOWN_ENV = ["TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE"];
121
+ var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "cmd", "powershell", "pwsh"]);
122
+ var NATIVE_SHELLS = /* @__PURE__ */ new Set(["cmd", "powershell", "pwsh"]);
123
+ var SHELL_WORDS = /\b(?:sh|bash|cmd|powershell|pwsh)(?:\.exe)?\b/i;
20
124
  function stripHeredocs(text) {
21
125
  const out = [];
22
126
  const pending = [];
@@ -149,6 +253,7 @@ function tokenize(text) {
149
253
  if (word !== null && /^\d+$/.test(word.value) && !word.dynamic) word = null;
150
254
  else finish();
151
255
  const m = /^(&>>?|\d*[<>]+[&|]?)/.exec(text.slice(i));
256
+ if (/^\d*<<<?$/.test(m[0])) tokens.push({ type: "here" });
152
257
  i += m[0].length;
153
258
  if (/&$/.test(m[0]) && /^\d|^-/.test(text[i] ?? "")) {
154
259
  while (i < text.length && /[\d-]/.test(text[i])) i++;
@@ -177,13 +282,16 @@ function tokenize(text) {
177
282
  function splitCommands(tokens) {
178
283
  const commands = [];
179
284
  let current = [];
285
+ let here = false;
180
286
  for (const t of tokens) {
181
287
  if (t.type === "op" && SEPARATORS.has(t.value)) {
182
- commands.push({ words: current, after: t.value });
288
+ commands.push({ words: current, after: t.value, here });
183
289
  current = [];
290
+ here = false;
184
291
  } else if (t.type === "word") current.push(t);
292
+ else if (t.type === "here") here = true;
185
293
  }
186
- commands.push({ words: current, after: null });
294
+ commands.push({ words: current, after: null, here });
187
295
  return commands;
188
296
  }
189
297
  function expand(word, vars) {
@@ -207,7 +315,7 @@ function assign(word, vars, home) {
207
315
  }
208
316
  function commandName(word) {
209
317
  if (word.dynamic) return null;
210
- return win.basename(word.value.replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
318
+ return win2.basename(word.value.replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
211
319
  }
212
320
  function stripPrefixes(words) {
213
321
  let rest = words;
@@ -235,23 +343,24 @@ function stripPrefixes(words) {
235
343
  }
236
344
  function toWindowsPath(word, cwd, home, tmpdir) {
237
345
  const v = word.value;
238
- if (word.tilde && (v === "~" || v.startsWith("~/") || v.startsWith("~\\"))) return { path: win.resolve(home, v.slice(2) || ".") };
239
- if (/^[A-Za-z]:[\\/]/.test(v)) return { path: win.normalize(v) };
346
+ if (word.tilde && (v === "~" || v.startsWith("~/") || v.startsWith("~\\"))) return { path: win2.resolve(home, v.slice(2) || ".") };
347
+ if (/^[A-Za-z]:[\\/]/.test(v)) return { path: win2.normalize(v) };
240
348
  const drive = /^\/([A-Za-z])(?:\/(.*))?$/.exec(v);
241
- if (drive) return { path: win.normalize(`${drive[1].toUpperCase()}:\\${drive[2] ?? ""}`) };
349
+ if (drive) return { path: win2.normalize(`${drive[1].toUpperCase()}:\\${drive[2] ?? ""}`) };
242
350
  const tmp = /^\/tmp(?:\/(.*))?$/.exec(v);
243
- if (tmp) return { path: win.resolve(tmpdir, tmp[1] ?? ".") };
351
+ if (tmp) return { path: win2.resolve(tmpdir, tmp[1] ?? ".") };
244
352
  if (v.startsWith("/")) return { unknown: v };
245
353
  if (cwd === null) return { unresolvable: "a relative path after cd to a path that cannot be worked out" };
246
- return { path: win.resolve(cwd, v) };
247
- }
248
- function isInside(child, parent) {
249
- const c = child.toLowerCase();
250
- const p = parent.toLowerCase().replace(/\\+$/, "");
251
- return c.startsWith(`${p}\\`);
354
+ return { path: win2.resolve(cwd, v) };
252
355
  }
253
- function segments(p) {
254
- return p.toLowerCase().split("\\");
356
+ function nativeArg(value, tmpdir) {
357
+ if (value === null || !value.startsWith("/") || value === "/?") return value;
358
+ if (value.startsWith("//")) return /^\/\/[^/]*$/.test(value) ? value.slice(1) : value;
359
+ const drive = /^\/([A-Za-z])(\/.*)?$/.exec(value);
360
+ if (drive) return `${drive[1].toUpperCase()}:${drive[2] ?? "/"}`;
361
+ const tmp = /^\/tmp(\/.*)?$/.exec(value);
362
+ if (tmp) return `${tmpdir}${(tmp[1] ?? "").replace(/\//g, "\\")}`;
363
+ return null;
255
364
  }
256
365
  function classify(target, ctx) {
257
366
  if (target.brace) return { unresolvable: `brace expansion (${target.value})` };
@@ -260,31 +369,19 @@ function classify(target, ctx) {
260
369
  let w = { ...target, value, dynamic: false };
261
370
  let pattern = null;
262
371
  if (target.glob) {
263
- const parts = value.split(/[\\/]/);
264
- if (parts.slice(0, -1).some((s) => /[*?[]/.test(s))) return { unresolvable: `a wildcard in a folder name (${target.value})` };
265
- pattern = parts[parts.length - 1];
266
- w = { ...w, value: parts.slice(0, -1).join("/") || "." };
372
+ const split = splitWindowsPattern(value, /[*?[]/);
373
+ if (split === null) return { unresolvable: `a wildcard in a folder name (${target.value})` };
374
+ pattern = split.pattern;
375
+ w = { ...w, value: split.dir };
267
376
  }
268
377
  const resolved = toWindowsPath(w, ctx.cwd, ctx.home, ctx.tmpdirs[0]);
269
378
  if (resolved.unresolvable) return { unresolvable: resolved.unresolvable };
270
379
  if (resolved.unknown) return { real: { shown: target.value, path: null } };
271
- const p = resolved.path;
272
- const shown = pattern === null ? p : `${p}\\${pattern}`;
273
- const narrowPattern = pattern !== null && !/^[*?.]+$/.test(pattern);
274
- if (ctx.tmpdirs.some((t) => isInside(p, t) || narrowPattern && p.toLowerCase() === t.toLowerCase().replace(/\\+$/, ""))) return {};
275
- if (segments(p).includes(".superpowers")) return {};
276
- if (pattern === null && !ctx.probe.exists(p)) return {};
277
- if (segments(p).some((s) => BUILD_DIRS.has(s)) && ctx.probe.isIgnored(p)) return {};
278
- return { real: { shown, path: pattern === null ? p : null } };
279
- }
280
- function deny(why) {
281
- return {
282
- decision: "deny",
283
- reason: `rm-guard could not work out what this rm would delete (${why}). Rewrite it with literal paths: no shell variables, command substitutions, xargs, brace expansion or wildcards in folder names, and no cd to such a path before it. Example: rm -f "C:/full/path/file.txt". If you need a variable or a listing to find the paths, run that on its own first, then rm the printed paths.`
284
- };
380
+ return { real: judgePath(resolved.path, pattern, ctx) ?? void 0 };
285
381
  }
286
- function decide({ command, cwd, home, tmpdirs, env = {}, probe }) {
287
- if (!/\b(?:rm|rmdir|xargs)\b/.test(command)) return null;
382
+ function readBash(command, { cwd, home, tmpdirs, env, probe }) {
383
+ const out = emptyRead();
384
+ if (!/\b(?:rm|rmdir|xargs)\b/.test(command) && !SHELL_WORDS.test(command)) return out;
288
385
  const commands = splitCommands(tokenize(stripHeredocs(command)));
289
386
  const vars = new Map(KNOWN_ENV.filter((n) => env[n]).map((n) => [n, env[n]]));
290
387
  const ctx = { cwd, home, tmpdirs, probe, vars };
@@ -292,10 +389,11 @@ function decide({ command, cwd, home, tmpdirs, env = {}, probe }) {
292
389
  ctx.cwd = next;
293
390
  vars.set("PWD", next);
294
391
  };
392
+ const deny = (why) => ({ ...out, deny: denyReason("bash", why) });
295
393
  setCwd(cwd);
296
394
  const stack = [];
297
- const real = [];
298
- for (const { words, after } of commands) {
395
+ let piped = false;
396
+ for (const { words, after, here } of commands) {
299
397
  const rest = stripPrefixes(words);
300
398
  if (!rest.length) {
301
399
  for (const w of words) if (ASSIGNMENT.test(w.value)) assign(w, vars, home);
@@ -315,9 +413,11 @@ function decide({ command, cwd, home, tmpdirs, env = {}, probe }) {
315
413
  if (rest.slice(1).some((t) => {
316
414
  const n = commandName(t);
317
415
  return n === "rm" || n === "rmdir";
318
- })) {
319
- return deny("xargs feeding rm");
320
- }
416
+ })) return deny("xargs feeding rm");
417
+ if (rest.slice(1).some((t) => SHELLS.has(commandName(t) ?? "")) && DELETE_WORDS.test(rest.slice(1).map((t) => t.value).join(" "))) return deny("xargs feeding a shell that deletes");
418
+ } else if (name2 !== null && SHELLS.has(name2)) {
419
+ const args = rest.slice(1).map((t) => t.brace ? null : expand(t, vars));
420
+ out.shells.push({ name: name2, args: NATIVE_SHELLS.has(name2) ? args.map((a) => nativeArg(a, tmpdirs[0])) : args, cwd: ctx.cwd, raw: rest.map((t) => t.value).join(" "), ...piped || here ? { piped: true } : {} });
321
421
  } else if (name2 === "rm" || name2 === "rmdir") {
322
422
  let options = true;
323
423
  for (const t of rest.slice(1)) {
@@ -328,42 +428,23 @@ function decide({ command, cwd, home, tmpdirs, env = {}, probe }) {
328
428
  if (options && !t.dynamic && t.value.length > 1 && t.value.startsWith("-")) continue;
329
429
  const c = classify(t, ctx);
330
430
  if (c.unresolvable) return deny(c.unresolvable);
331
- if (c.real) real.push(c.real);
431
+ if (c.real) out.targets.push(c.real);
332
432
  }
333
433
  }
334
434
  }
435
+ piped = after === "|" || after === "|&" || piped && !words.length && after === "(";
335
436
  if (after === "(") stack.push(ctx.cwd);
336
437
  if (after === ")" && stack.length) setCwd(stack.pop() ?? null);
337
438
  }
338
- if (!real.length) return null;
339
- const listed = real.slice(0, MAX_LISTED).map((t) => t.shown).join(", ");
340
- const more = real.length > MAX_LISTED ? ` \uC678 ${real.length - MAX_LISTED}\uAC1C` : "";
341
- return { decision: "ask", reason: `\uC2E4\uC81C \uD30C\uC77C \uC0AD\uC81C: ${listed}${more}`, targets: real };
342
- }
343
- function hookOutput({ decision, reason }) {
344
- return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: decision, permissionDecisionReason: reason } });
439
+ return out;
345
440
  }
346
- var realProbe = {
347
- exists(p) {
348
- try {
349
- fs.lstatSync(p);
350
- return true;
351
- } catch {
352
- return false;
353
- }
354
- },
355
- isIgnored(p) {
356
- const r = spawnSync("git", ["-C", win.dirname(p), "check-ignore", "-q", "--", p], { timeout: 3e3, windowsHide: true });
357
- return r.status === 0;
358
- }
359
- };
360
441
 
361
442
  // src/rm/run.ts
362
- import path7 from "path";
443
+ import path10 from "path";
363
444
 
364
445
  // src/config.ts
365
446
  import fs2 from "fs";
366
- import path3 from "path";
447
+ import path4 from "path";
367
448
 
368
449
  // src/config-shape.ts
369
450
  var CONFIG_VERSION = 2;
@@ -371,6 +452,7 @@ var DEFAULT_CONFIG = {
371
452
  lang: "ko",
372
453
  keyFile: null,
373
454
  display: "mod",
455
+ shortcut: null,
374
456
  context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
375
457
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
376
458
  };
@@ -398,17 +480,18 @@ function normalizeConfig(raw) {
398
480
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
399
481
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
400
482
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
483
+ shortcut: typeof r.shortcut === "string" && r.shortcut !== "" ? r.shortcut : null,
401
484
  context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
402
485
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
403
486
  };
404
487
  }
405
488
 
406
489
  // src/paths.ts
407
- import path2 from "path";
408
- var claudeDir = (home) => path2.join(home, ".claude");
409
- var dataDir = (home) => path2.join(claudeDir(home), "claude-jev-advisor");
410
- var configPath = (home) => path2.join(dataDir(home), "config.json");
411
- var logDir = (home) => path2.join(dataDir(home), "log");
490
+ import path3 from "path";
491
+ var claudeDir = (home) => path3.join(home, ".claude");
492
+ var dataDir = (home) => path3.join(claudeDir(home), "claude-jev-advisor");
493
+ var configPath = (home) => path3.join(dataDir(home), "config.json");
494
+ var logDir = (home) => path3.join(dataDir(home), "log");
412
495
 
413
496
  // src/config.ts
414
497
  function readConfig(home) {
@@ -421,13 +504,13 @@ function readConfig(home) {
421
504
 
422
505
  // src/context/files.ts
423
506
  import fs3 from "fs";
424
- import path4 from "path";
507
+ import path5 from "path";
425
508
  function logFile(home, now) {
426
- return path4.join(logDir(home), `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}.jsonl`);
509
+ return path5.join(logDir(home), `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}.jsonl`);
427
510
  }
428
511
  function appendLog(home, now, record, secrets = []) {
429
512
  const file = logFile(home, now);
430
- fs3.mkdirSync(path4.dirname(file), { recursive: true });
513
+ fs3.mkdirSync(path5.dirname(file), { recursive: true });
431
514
  let line = JSON.stringify({ at: now.toISOString(), ...record });
432
515
  for (const secret of secrets) {
433
516
  if (!secret) continue;
@@ -506,16 +589,796 @@ async function callJev(request, opts) {
506
589
  }
507
590
  }
508
591
 
592
+ // src/rm/cmd.ts
593
+ import path6 from "path";
594
+ var win3 = path6.win32;
595
+ var SHELLS2 = /* @__PURE__ */ new Set(["sh", "bash", "cmd", "powershell", "pwsh"]);
596
+ var DELETES = /* @__PURE__ */ new Set(["del", "erase", "rd", "rmdir"]);
597
+ var CONTROL = /* @__PURE__ */ new Set(["if", "else", "for", "call", "start"]);
598
+ var UNKNOWN = "\0";
599
+ var EXCERPT_CHARS = 200;
600
+ function expandVars(line, vars) {
601
+ return line.replace(/%([^%\s"]+)%/g, (_m, name2) => vars.get(name2.toLowerCase()) ?? UNKNOWN).replace(/%~[^\s"]*/g, UNKNOWN).replace(/![^!\s"]+!/g, UNKNOWN);
602
+ }
603
+ function hasBlock(line) {
604
+ let quoted = false;
605
+ for (let i = 0; i < line.length; i++) {
606
+ if (line[i] === '"') quoted = !quoted;
607
+ else if (!quoted && line[i] === "^") i++;
608
+ else if (!quoted && (line[i] === "(" || line[i] === ")")) return true;
609
+ }
610
+ return false;
611
+ }
612
+ function segmentsOf(line) {
613
+ const out = [];
614
+ let current = "";
615
+ let piped = false;
616
+ let quoted = false;
617
+ for (let i = 0; i < line.length; i++) {
618
+ const c = line[i];
619
+ if (c === '"') quoted = !quoted;
620
+ if (!quoted && c === "^" && i + 1 < line.length) {
621
+ current += c + line[++i];
622
+ continue;
623
+ }
624
+ const redirect = c === "&" && /[<>]$/.test(current);
625
+ if (!quoted && !redirect && "&|\n".includes(c)) {
626
+ out.push({ text: current, piped });
627
+ current = "";
628
+ piped = c === "|" && line[i + 1] !== "|";
629
+ if ((c === "&" || c === "|") && line[i + 1] === c) i++;
630
+ continue;
631
+ }
632
+ current += c;
633
+ }
634
+ out.push({ text: current, piped });
635
+ return out.filter((s) => s.text.trim() !== "");
636
+ }
637
+ function wordsOf(segment, delimiters) {
638
+ const words = [];
639
+ let word = null;
640
+ let quoted = false;
641
+ let skipNext = false;
642
+ const start = () => word ??= { value: "", dynamic: false, shown: "", end: 0 };
643
+ const add = (c) => {
644
+ const w = start();
645
+ if (c === UNKNOWN) {
646
+ w.dynamic = true;
647
+ w.shown += "%?%";
648
+ } else {
649
+ w.value += c;
650
+ w.shown += c;
651
+ }
652
+ };
653
+ const finish = (at) => {
654
+ if (word === null) return;
655
+ word.end = at;
656
+ if (skipNext) skipNext = false;
657
+ else words.push(word);
658
+ word = null;
659
+ };
660
+ for (let i = 0; i < segment.length; i++) {
661
+ const c = segment[i];
662
+ if (c === '"') {
663
+ quoted = !quoted;
664
+ start();
665
+ continue;
666
+ }
667
+ if (quoted) {
668
+ add(c);
669
+ continue;
670
+ }
671
+ if (c === "^") {
672
+ if (i + 1 < segment.length) add(segment[++i]);
673
+ continue;
674
+ }
675
+ if (c === " " || c === " " || c === "\r" || delimiters.includes(c)) {
676
+ finish(i);
677
+ continue;
678
+ }
679
+ if (c === ">" || c === "<") {
680
+ if (word !== null && /^\d$/.test(word.value) && !word.dynamic) word = null;
681
+ else finish(i);
682
+ while (segment[i + 1] === ">") i++;
683
+ if (segment[i + 1] === "&") {
684
+ i++;
685
+ while (/\d/.test(segment[i + 1] ?? "")) i++;
686
+ continue;
687
+ }
688
+ skipNext = true;
689
+ continue;
690
+ }
691
+ add(c);
692
+ }
693
+ finish(segment.length);
694
+ return words;
695
+ }
696
+ function nameOf(word) {
697
+ if (word.dynamic && /%\?%[^\\/]*$/.test(word.shown)) return null;
698
+ const text = word.value.replace(/^@/, "");
699
+ const glued = /^(del|erase|rd|rmdir|cd|chdir|pushd|popd|if|else|for|call|start)([/,;=].*)$/i.exec(text) ?? /^(cd|chdir|pushd)([.\\].*)$/i.exec(text);
700
+ if (glued) return { name: glued[1].toLowerCase(), rest: glued[2] };
701
+ return { name: win3.basename(text.replace(/\//g, "\\")).replace(/\.(?:exe|com)$/i, "").toLowerCase(), rest: "" };
702
+ }
703
+ function classify2(word, cwd, everywhere, ctx) {
704
+ if (word.dynamic || word.value.includes("%")) return { unresolvable: `a variable that cannot be worked out (${word.shown})` };
705
+ let base = word.value;
706
+ let pattern = null;
707
+ if (everywhere || /[*?]/.test(word.value)) {
708
+ const split = splitWindowsPattern(word.value, /[*?]/);
709
+ if (split === null) return { unresolvable: `a wildcard in a folder name (${word.value})` };
710
+ base = split.dir;
711
+ pattern = split.pattern;
712
+ }
713
+ const resolved = resolveWindowsPath(base, cwd, null);
714
+ if ("unresolvable" in resolved) return { unresolvable: resolved.unresolvable };
715
+ if ("unc" in resolved) return { real: { shown: word.value, path: null } };
716
+ return { real: judgePath(resolved.path, pattern, ctx) ?? void 0 };
717
+ }
718
+ function readCmd(line, ctx) {
719
+ const out = emptyRead();
720
+ const vars = /* @__PURE__ */ new Map();
721
+ for (const name2 of ["TEMP", "TMP", "USERPROFILE"]) {
722
+ const value = ctx.env[name2];
723
+ if (value) vars.set(name2.toLowerCase(), value);
724
+ }
725
+ if (ctx.cwd !== null) vars.set("cd", ctx.cwd);
726
+ let cwd = ctx.cwd;
727
+ const deny = (why) => ({ ...out, deny: denyReason("cmd", why) });
728
+ const expanded = expandVars(line, vars);
729
+ const pieces = segmentsOf(expanded).map((s) => ({ ...s, text: s.text.replace(/^[\s@,;=]+/, "") })).filter((s) => s.text !== "");
730
+ const segments2 = pieces.map((s) => s.text);
731
+ const control = segments2.some((s) => {
732
+ const first = wordsOf(s, "")[0];
733
+ const named = first ? nameOf(first) : null;
734
+ return first !== void 0 && (named === null || CONTROL.has(named.name));
735
+ });
736
+ if (control || hasBlock(expanded)) {
737
+ if (!DELETE_WORDS.test(expanded)) return out;
738
+ const shown = expanded.trim().replace(/\s+/g, " ").slice(0, EXCERPT_CHARS).split(UNKNOWN).join("%?%");
739
+ return deny(`a delete on a cmd line with if, else, for, call, start, parentheses or a command that cannot be worked out (${shown}); write plain del or rd lines, with names that hold parentheses in double quotes`);
740
+ }
741
+ for (const { text: segment, piped } of pieces) {
742
+ const words = wordsOf(segment, "");
743
+ if (!words.length) continue;
744
+ const first = words[0];
745
+ const named = nameOf(first);
746
+ const shown = segment.trim().split(UNKNOWN).join("%?%");
747
+ const { name: name2, rest } = named;
748
+ const args = () => [...rest ? wordsOf(rest, ",;=") : [], ...wordsOf(segment.slice(first.end), ",;=")];
749
+ if (/^[a-z]:$/i.test(first.value.replace(/^@/, ""))) {
750
+ cwd = null;
751
+ } else if (SHELLS2.has(name2)) {
752
+ const msys = name2 === "sh" || name2 === "bash";
753
+ out.shells.push({ name: name2, args: words.slice(1).map((w) => w.dynamic || msys && w.value.includes("'") ? null : w.value), cwd, raw: shown, ...piped ? { piped } : {} });
754
+ } else if (name2 === "popd") {
755
+ cwd = null;
756
+ } else if (name2 === "cd" || name2 === "chdir" || name2 === "pushd") {
757
+ const given = args();
758
+ const switchDrive = name2 === "pushd" || given.some((w) => !w.dynamic && w.value.toLowerCase() === "/d");
759
+ const parts = given.filter((w) => w.dynamic || w.value.toLowerCase() !== "/d");
760
+ if (!parts.length) continue;
761
+ const resolved = parts.some((w) => w.dynamic) ? null : resolveWindowsPath(parts.map((w) => w.value).join(" "), cwd, null);
762
+ if (resolved === null || !("path" in resolved)) cwd = null;
763
+ else if (switchDrive || cwd === null || resolved.path.slice(0, 2).toLowerCase() === cwd.slice(0, 2).toLowerCase()) cwd = resolved.path;
764
+ } else if (DELETES.has(name2)) {
765
+ const given = args();
766
+ const switches = given.filter((w) => !w.dynamic && w.value.startsWith("/"));
767
+ const everywhere = (name2 === "del" || name2 === "erase") && switches.some((w) => w.value.split("/").some((s) => s.toLowerCase() === "s"));
768
+ for (const word of given) {
769
+ if (switches.includes(word)) continue;
770
+ const c = classify2(word, cwd, everywhere, ctx);
771
+ if (c.unresolvable) return deny(c.unresolvable);
772
+ if (c.real) out.targets.push(c.real);
773
+ }
774
+ }
775
+ }
776
+ return out;
777
+ }
778
+
779
+ // src/rm/powershell.ts
780
+ import fs5 from "fs";
781
+ import path7 from "path";
782
+ import { spawn } from "child_process";
783
+ var PS_PARSE_TIMEOUT_MS = 3e3;
784
+ var POWERSHELL_WORDS = /\b(?:remove-item|ri|rm|del|erase|rd|rmdir|sh|bash|cmd|powershell|pwsh)(?:\.exe)?\b|\bdelete/i;
785
+ var DELETE_WORD = new RegExp(`${DELETE_WORDS.source}|\\bdelete`, "i");
786
+ var OTHER_DRIVES = /* @__PURE__ */ new Set(["alias", "env", "function", "variable", "hklm", "hkcu", "cert", "wsman"]);
787
+ var EXCERPT_CHARS2 = 200;
788
+ var KNOWN_ENV2 = ["TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE"];
789
+ var READER = String.raw`
790
+ param($source)
791
+ $ErrorActionPreference = 'Stop'
792
+ $tokens = $null
793
+ $errors = $null
794
+ $root = [System.Management.Automation.Language.Parser]::ParseInput($source, [ref]$tokens, [ref]$errors)
795
+
796
+ function Value($ast) {
797
+ if ($null -eq $ast) { return @{ dynamic = '' } }
798
+ if ($ast -is [System.Management.Automation.Language.StringConstantExpressionAst]) { return @{ const = $ast.Value } }
799
+ if ($ast -is [System.Management.Automation.Language.ConstantExpressionAst]) { return @{ const = [string]$ast.Value } }
800
+ if ($ast -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) {
801
+ if ($ast.Extent.Text.Contains([string][char]96 + '$')) { return @{ dynamic = $ast.Extent.Text } }
802
+ $parts = New-Object System.Collections.ArrayList
803
+ $at = 0
804
+ foreach ($n in $ast.NestedExpressions) {
805
+ if (-not ($n -is [System.Management.Automation.Language.VariableExpressionAst]) -or $n.Splatted) { return @{ dynamic = $ast.Extent.Text } }
806
+ $i = $ast.Value.IndexOf($n.Extent.Text, $at, [System.StringComparison]::Ordinal)
807
+ if ($i -lt 0) { return @{ dynamic = $ast.Extent.Text } }
808
+ if ($i -gt $at) { [void]$parts.Add(@{ const = $ast.Value.Substring($at, $i - $at) }) }
809
+ [void]$parts.Add(@{ variable = $n.VariablePath.UserPath })
810
+ $at = $i + $n.Extent.Text.Length
811
+ }
812
+ if ($at -lt $ast.Value.Length) { [void]$parts.Add(@{ const = $ast.Value.Substring($at) }) }
813
+ return @{ expand = $parts.ToArray(); text = $ast.Extent.Text }
814
+ }
815
+ if ($ast -is [System.Management.Automation.Language.VariableExpressionAst] -and -not $ast.Splatted) { return @{ variable = $ast.VariablePath.UserPath } }
816
+ if ($ast -is [System.Management.Automation.Language.ArrayLiteralAst]) { return @{ array = @($ast.Elements | ForEach-Object { Value $_ }) } }
817
+ if ($ast -is [System.Management.Automation.Language.CommandParameterAst]) { return @{ const = $ast.Extent.Text } }
818
+ return @{ dynamic = $ast.Extent.Text }
819
+ }
820
+
821
+ function Constant($ast) {
822
+ if ($ast -is [System.Management.Automation.Language.CommandParameterAst]) { return ($null -eq $ast.Argument) -or (Constant $ast.Argument) }
823
+ if ($ast -is [System.Management.Automation.Language.ArrayLiteralAst]) { return @($ast.Elements | Where-Object { -not (Constant $_) }).Count -eq 0 }
824
+ return $ast -is [System.Management.Automation.Language.ConstantExpressionAst]
825
+ }
826
+
827
+ function NameOf($command) {
828
+ $name = $command.GetCommandName()
829
+ if ($null -eq $name) { return $null }
830
+ if ($name -eq 'cd..' -or $name -eq 'cd\') { return $name }
831
+ $name = $name -replace '^Microsoft\.PowerShell\.Management\\', ''
832
+ $name = ($name -split '[\\/]')[-1]
833
+ return ($name -replace '\.exe$', '').ToLowerInvariant()
834
+ }
835
+
836
+ function TopLevel($ast) {
837
+ for ($p = $ast.Parent; $null -ne $p; $p = $p.Parent) {
838
+ if ($p -eq $root) { return $true }
839
+ if (-not ($p -is [System.Management.Automation.Language.PipelineAst] -or $p -is [System.Management.Automation.Language.NamedBlockAst])) { return $false }
840
+ }
841
+ return $false
842
+ }
843
+
844
+ function InOrder($ast) {
845
+ for ($p = $ast.Parent; $null -ne $p -and $p -ne $root; $p = $p.Parent) {
846
+ if ($p -is [System.Management.Automation.Language.LoopStatementAst] -or $p -is [System.Management.Automation.Language.SwitchStatementAst] -or $p -is [System.Management.Automation.Language.ScriptBlockAst] -or $p -is [System.Management.Automation.Language.TrapStatementAst]) { return $false }
847
+ }
848
+ return $true
849
+ }
850
+
851
+ function Bound($command) {
852
+ $binding = [System.Management.Automation.Language.StaticParameterBinder]::BindCommand($command, $true)
853
+ $out = @{ bindError = ($binding.BindingExceptions.Count -gt 0); whatIf = $false }
854
+ foreach ($key in $binding.BoundParameters.Keys) {
855
+ $result = $binding.BoundParameters[$key]
856
+ switch ($key) {
857
+ 'Path' { $out.path = Value $result.Value }
858
+ 'LiteralPath' { $out.literalPath = Value $result.Value }
859
+ 'Name' { $out.name = $result }
860
+ 'MemberName' { $out.member = $result }
861
+ 'WhatIf' {
862
+ $v = $result.Value
863
+ if ($null -eq $v) { $out.whatIf = ($result.ConstantValue -eq $true) }
864
+ else { $out.whatIf = ($v -is [System.Management.Automation.Language.VariableExpressionAst]) -and ($v.VariablePath.UserPath -eq 'true') }
865
+ }
866
+ }
867
+ }
868
+ return $out
869
+ }
870
+
871
+ $deletes = @('remove-item', 'ri', 'rm', 'del', 'erase', 'rd', 'rmdir')
872
+ $moves = @('set-location', 'sl', 'cd', 'chdir', 'push-location', 'pushd')
873
+ $pops = @('pop-location', 'popd')
874
+ $shells = @('sh', 'bash', 'cmd', 'powershell', 'pwsh')
875
+ $dynamics = @('invoke-expression', 'iex', 'start-process', 'saps', 'start', 'invoke-command', 'icm', 'start-job', 'sajb', 'start-threadjob', 'set-alias', 'sal', 'new-alias', 'nal', 'add-type')
876
+ $members = @('foreach-object', '%', 'foreach')
877
+ $drives = @('new-psdrive', 'ndr', 'mount')
878
+ $variableCommands = @('set-variable', 'sv', 'new-variable', 'nv', 'remove-variable', 'rv', 'clear-variable', 'clv', 'get-variable', 'gv')
879
+ $variableParameters = @('outvariable', 'errorvariable', 'warningvariable', 'informationvariable', 'pipelinevariable')
880
+ $variableAliases = @('ov', 'ev', 'wv', 'iv', 'pv')
881
+ $nameParameters = @('variable', 'bindingvariable') + $variableParameters
882
+
883
+ function NamesVariable($element) {
884
+ if (-not ($element -is [System.Management.Automation.Language.CommandParameterAst])) { return $false }
885
+ $p = $element.ParameterName.ToLowerInvariant()
886
+ return $variableAliases -contains $p -or ($p.Length -ge 2 -and @($nameParameters | Where-Object { $_.StartsWith($p) }).Count -gt 0)
887
+ }
888
+ $codeMethods = @('invoke', 'invokereturnasis', 'invokewithcontext', 'invokescript', 'newscriptblock', 'addscript', 'addcommand', 'begininvoke')
889
+ $dotnet = @('system.io.file', 'io.file', 'file', 'system.io.directory', 'io.directory', 'directory')
890
+ $fileCommands = @('get-item', 'gi', 'get-childitem', 'gci', 'ls', 'dir', 'new-item', 'ni')
891
+ $unstable = New-Object System.Collections.ArrayList
892
+ $anyVariable = $false
893
+ # An instance Delete() deletes a file only on an object a file command, -PassThru or a FileInfo made; COM objects have
894
+ # one too.
895
+ $fileObjects = ($source -match 'FileInfo|DirectoryInfo|FileSystemInfo') -or @($root.FindAll({
896
+ param($x)
897
+ ($x -is [System.Management.Automation.Language.CommandAst] -and $fileCommands -contains (NameOf $x)) -or
898
+ ($x -is [System.Management.Automation.Language.CommandParameterAst] -and $x.ParameterName.Length -ge 3 -and 'passthru'.StartsWith($x.ParameterName.ToLowerInvariant()))
899
+ }, $true)).Count -gt 0
900
+ $processStart = $source -match 'ProcessStartInfo|Diagnostics\.Process'
901
+
902
+ # A name handed over as text (-OutVariable d, Tee-Object -Variable d, Set-Item variable:d, 'TEMP' handed to
903
+ # SetEnvironmentVariable, '$d = 1' handed to iex) can write a variable without the parser seeing a variable there.
904
+ $named = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
905
+ foreach ($s in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true)) {
906
+ foreach ($m in [regex]::Matches($s.Value, '\$\{?(?:\w+:)?([\w?]+)')) { [void]$named.Add($m.Groups[1].Value) }
907
+ $parent = $s.Parent
908
+ $at = if ($parent -is [System.Management.Automation.Language.CommandAst]) { $parent.CommandElements.IndexOf($s) } else { -1 }
909
+ # A bare Variable: or Env: drive (New-Item -Path Env: -Name TEMP, Set-Location Variable:) writes names given elsewhere.
910
+ if ($s.Value -match '^(?:[\w.]+\\)?(?:variable|env|environment):{1,2}[\\/]?$') { $anyVariable = $true }
911
+ elseif ($s.Value -match '^(?:[\w.]+\\)?(?:variable|env|environment):{1,2}[\\/]?(.+)$') { [void]$named.Add($Matches[1]) }
912
+ elseif (($at -gt 0 -and (NamesVariable $parent.CommandElements[$at - 1])) -or (NamesVariable $parent) -or ($parent -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -and $parent.Member -ne $s -and $parent.Member.Value -match 'variable|^set$|^get$')) {
913
+ [void]$named.Add(($s.Value -replace '^\+', '' -replace '^(?:script|global|local|private):', ''))
914
+ }
915
+ }
916
+ foreach ($d in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.DataStatementAst] }, $true)) { if ($d.Variable) { [void]$named.Add($d.Variable) } }
917
+ foreach ($v in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)) {
918
+ $path = $v.VariablePath.UserPath
919
+ if ($v.Parent -is [System.Management.Automation.Language.ParameterAst] -or $named.Contains(($path -split ':')[-1])) { [void]$unstable.Add($path) }
920
+ }
921
+
922
+ $items = New-Object System.Collections.ArrayList
923
+ $found = $root.FindAll({
924
+ param($a)
925
+ $a -is [System.Management.Automation.Language.CommandAst] -or
926
+ $a -is [System.Management.Automation.Language.AssignmentStatementAst] -or
927
+ $a -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -or
928
+ $a -is [System.Management.Automation.Language.UnaryExpressionAst] -or
929
+ $a -is [System.Management.Automation.Language.ForEachStatementAst] -or
930
+ $a -is [System.Management.Automation.Language.ConvertExpressionAst] -or
931
+ $a -is [System.Management.Automation.Language.CommandParameterAst]
932
+ }, $true)
933
+ foreach ($a in $found) {
934
+ if ($a -is [System.Management.Automation.Language.AssignmentStatementAst]) {
935
+ $left = $a.Left
936
+ if ($left -is [System.Management.Automation.Language.VariableExpressionAst] -and -not $left.Splatted -and ($left.VariablePath.IsUnqualified -or $left.VariablePath.IsDriveQualified) -and $a.Operator -eq 'Equals' -and (TopLevel $a)) {
937
+ $right = if ($a.Right -is [System.Management.Automation.Language.CommandExpressionAst]) { Value $a.Right.Expression } else { @{ dynamic = $a.Right.Extent.Text } }
938
+ [void]$items.Add(@{ kind = 'assign'; name = $left.VariablePath.UserPath; value = $right })
939
+ } else {
940
+ foreach ($v in $left.FindAll({ param($x) $x -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)) { [void]$unstable.Add($v.VariablePath.UserPath) }
941
+ }
942
+ continue
943
+ }
944
+ if ($a -is [System.Management.Automation.Language.UnaryExpressionAst]) {
945
+ if ($a.Child -is [System.Management.Automation.Language.VariableExpressionAst] -and @('PlusPlus', 'MinusMinus', 'PostfixPlusPlus', 'PostfixMinusMinus') -contains [string]$a.TokenKind) { [void]$unstable.Add($a.Child.VariablePath.UserPath) }
946
+ continue
947
+ }
948
+ if ($a -is [System.Management.Automation.Language.ForEachStatementAst]) {
949
+ [void]$unstable.Add($a.Variable.VariablePath.UserPath)
950
+ continue
951
+ }
952
+ if ($a -is [System.Management.Automation.Language.ConvertExpressionAst]) {
953
+ if ($a.Type.TypeName.Name -eq 'ref' -and $a.Child -is [System.Management.Automation.Language.VariableExpressionAst]) { [void]$unstable.Add($a.Child.VariablePath.UserPath) }
954
+ continue
955
+ }
956
+ if ($a -is [System.Management.Automation.Language.CommandParameterAst]) {
957
+ # PowerShell takes any unambiguous start of a parameter name, so -OutVar and -ErrorVar count too.
958
+ $p = $a.ParameterName.ToLowerInvariant()
959
+ if ($variableAliases -contains $p -or ($p.Length -ge 2 -and @($variableParameters | Where-Object { $_.StartsWith($p) }).Count -gt 0)) { $anyVariable = $true }
960
+ continue
961
+ }
962
+ if ($a -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) {
963
+ $member = if ($a.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $a.Member.Value.ToLowerInvariant() } else { $null }
964
+ $type = if ($a.Expression -is [System.Management.Automation.Language.TypeExpressionAst]) { $a.Expression.TypeName.FullName.ToLowerInvariant() } else { $null }
965
+ if ($a.Static -and $member -eq 'delete' -and $dotnet -contains $type) {
966
+ $first = if ($a.Arguments -and $a.Arguments.Count -gt 0) { Value $a.Arguments[0] } else { @{ dynamic = $a.Extent.Text } }
967
+ [void]$items.Add(@{ kind = 'delete'; path = $first; literalPath = $null; literal = $true; dotnet = $true; whatIf = $false; bindError = $false; inPipeline = $false; ordered = (InOrder $a) })
968
+ } elseif ($null -eq $member -or ($a.Static -and $null -eq $type) -or $codeMethods -contains $member -or
969
+ ($member -eq 'create' -and $type -match '(^|\.)(scriptblock|powershell)$') -or
970
+ ($member -eq 'start' -and ($type -match '(^|\.)process$' -or ($null -eq $type -and $processStart))) -or
971
+ ($member.StartsWith('delete') -and ($null -ne $type -or $fileObjects))) {
972
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
973
+ }
974
+ continue
975
+ }
976
+ if ($a.CommandElements[0] -is [System.Management.Automation.Language.ScriptBlockExpressionAst]) { continue }
977
+ $name = NameOf $a
978
+ if ($null -eq $name) {
979
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
980
+ continue
981
+ }
982
+ $pipeline = $a.Parent
983
+ $inPipeline = ($pipeline -is [System.Management.Automation.Language.PipelineAst]) -and ($pipeline.PipelineElements.IndexOf($a) -gt 0)
984
+ if ($deletes -contains $name) {
985
+ $b = Bound $a
986
+ [void]$items.Add(@{ kind = 'delete'; path = $b.path; literalPath = $b.literalPath; literal = ($null -ne $b.literalPath); dotnet = $false; whatIf = $b.whatIf; bindError = $b.bindError; inPipeline = $inPipeline; ordered = (InOrder $a) })
987
+ } elseif ($name -eq 'cd..' -or $name -eq 'cd\') {
988
+ [void]$items.Add(@{ kind = 'cd'; path = @{ const = $name.Substring(2) }; literal = $true; inBlock = -not (TopLevel $a) })
989
+ } elseif ($moves -contains $name) {
990
+ $b = Bound $a
991
+ $to = if ($null -ne $b.literalPath) { $b.literalPath } else { $b.path }
992
+ [void]$items.Add(@{ kind = 'cd'; path = $to; literal = ($null -ne $b.literalPath); inBlock = -not (TopLevel $a) })
993
+ } elseif ($pops -contains $name) {
994
+ [void]$items.Add(@{ kind = 'pop' })
995
+ } elseif ($shells -contains $name) {
996
+ $elements = @($a.CommandElements | Select-Object -Skip 1 | ForEach-Object { Value $_ })
997
+ [void]$items.Add(@{ kind = 'shell'; name = $name; args = $elements; raw = $a.Extent.Text; inPipeline = $inPipeline; ordered = (InOrder $a) })
998
+ } elseif ($dynamics -contains $name) {
999
+ if ($inPipeline) {
1000
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
1001
+ } else {
1002
+ $values = New-Object System.Collections.ArrayList
1003
+ foreach ($e in @($a.CommandElements | Select-Object -Skip 1)) {
1004
+ if (Constant $e) { continue }
1005
+ [void]$values.Add((Value $(if ($e -is [System.Management.Automation.Language.CommandParameterAst]) { $e.Argument } else { $e })))
1006
+ }
1007
+ [void]$items.Add(@{ kind = 'dynamic'; text = $a.Extent.Text; args = $values.ToArray() })
1008
+ }
1009
+ } elseif ($members -contains $name) {
1010
+ $m = (Bound $a).member
1011
+ if ($null -ne $m) {
1012
+ $v = $m.ConstantValue
1013
+ if ($null -eq $v -or ([string]$v) -match '^delete' -or $codeMethods -contains ([string]$v).ToLowerInvariant()) { [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() }) }
1014
+ }
1015
+ } elseif ($drives -contains $name) {
1016
+ # A drive made here can carry a one-letter name that reads like a real drive.
1017
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
1018
+ } elseif ($variableCommands -contains $name) {
1019
+ $b = Bound $a
1020
+ $names = if ($null -ne $b.name) { @($b.name.ConstantValue) } else { @() }
1021
+ if ($names.Count -eq 0 -or @($names | Where-Object { $null -eq $_ -or [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters([string]$_) }).Count -gt 0) { $anyVariable = $true }
1022
+ else { foreach ($n in $names) { [void]$unstable.Add([string]$n) } }
1023
+ }
1024
+ }
1025
+ $result = @{ errors = ($errors.Count -gt 0); items = $items.ToArray(); unstable = $unstable.ToArray(); anyVariable = $anyVariable }
1026
+ [Console]::Out.Write((ConvertTo-Json -InputObject $result -Depth 12 -Compress))
1027
+ `;
1028
+ var LOADER = String.raw`
1029
+ $utf8 = New-Object System.Text.UTF8Encoding $false
1030
+ [Console]::InputEncoding = $utf8
1031
+ [Console]::OutputEncoding = $utf8
1032
+ $all = [Console]::In.ReadToEnd()
1033
+ $cut = $all.IndexOf([char]0)
1034
+ & ([scriptblock]::Create($all.Substring(0, $cut))) $all.Substring($cut + 1)
1035
+ `;
1036
+ function powerShellExe(env) {
1037
+ for (const dir of (env.PATH ?? env.Path ?? "").split(path7.delimiter)) {
1038
+ if (dir && fs5.existsSync(path7.join(dir, "pwsh.exe"))) return path7.join(dir, "pwsh.exe");
1039
+ }
1040
+ return "powershell.exe";
1041
+ }
1042
+ function realPowerShell(env, timeoutMs = PS_PARSE_TIMEOUT_MS) {
1043
+ const encoded = Buffer.from(LOADER, "utf16le").toString("base64");
1044
+ return (source) => new Promise((resolve) => {
1045
+ let out = "";
1046
+ let done = false;
1047
+ const finish = (value) => {
1048
+ if (done) return;
1049
+ done = true;
1050
+ clearTimeout(timer);
1051
+ resolve(value);
1052
+ };
1053
+ const child = spawn(powerShellExe(env), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] });
1054
+ const timer = setTimeout(() => {
1055
+ child.kill();
1056
+ finish(null);
1057
+ }, timeoutMs);
1058
+ child.on("error", () => finish(null));
1059
+ child.on("close", (code) => finish(code === 0 ? out : null));
1060
+ child.stdout.setEncoding("utf8");
1061
+ child.stdout.on("data", (chunk) => {
1062
+ out += chunk;
1063
+ });
1064
+ child.stdin.on("error", () => {
1065
+ });
1066
+ child.stdin.end(`${READER}\0${source}`, "utf8");
1067
+ });
1068
+ }
1069
+ var list = (value) => Array.isArray(value) ? value : value == null ? [] : [value];
1070
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1071
+ function partOf(raw) {
1072
+ if (!isRecord2(raw)) throw new Error("bad value");
1073
+ if (typeof raw.const === "string") return { const: raw.const };
1074
+ if (typeof raw.variable === "string") return { variable: raw.variable };
1075
+ throw new Error("bad value");
1076
+ }
1077
+ function valueOf(raw) {
1078
+ if (!isRecord2(raw)) throw new Error("bad value");
1079
+ if ("expand" in raw) {
1080
+ if (!Array.isArray(raw.expand) || typeof raw.text !== "string") throw new Error("bad value");
1081
+ return { expand: raw.expand.map(partOf), text: raw.text };
1082
+ }
1083
+ if ("array" in raw) return { array: list(raw.array).map(valueOf) };
1084
+ if ("dynamic" in raw) {
1085
+ if (typeof raw.dynamic !== "string") throw new Error("bad value");
1086
+ return { dynamic: raw.dynamic };
1087
+ }
1088
+ return partOf(raw);
1089
+ }
1090
+ var optionalValue = (raw) => raw == null ? null : valueOf(raw);
1091
+ var flag = (raw) => {
1092
+ if (typeof raw !== "boolean") throw new Error("bad flag");
1093
+ return raw;
1094
+ };
1095
+ function itemOf(raw) {
1096
+ if (!isRecord2(raw) || typeof raw.kind !== "string") throw new Error("bad item");
1097
+ switch (raw.kind) {
1098
+ case "delete":
1099
+ return { kind: "delete", path: optionalValue(raw.path), literalPath: optionalValue(raw.literalPath), literal: flag(raw.literal), dotnet: flag(raw.dotnet), whatIf: flag(raw.whatIf), bindError: flag(raw.bindError), inPipeline: flag(raw.inPipeline), ordered: flag(raw.ordered) };
1100
+ case "cd":
1101
+ return { kind: "cd", path: optionalValue(raw.path), literal: flag(raw.literal), inBlock: flag(raw.inBlock) };
1102
+ case "pop":
1103
+ return { kind: "pop" };
1104
+ case "assign":
1105
+ if (typeof raw.name !== "string") throw new Error("bad item");
1106
+ return { kind: "assign", name: raw.name, value: valueOf(raw.value) };
1107
+ case "shell":
1108
+ if (typeof raw.name !== "string" || typeof raw.raw !== "string") throw new Error("bad item");
1109
+ return { kind: "shell", name: raw.name, args: list(raw.args).map(valueOf), raw: raw.raw, inPipeline: flag(raw.inPipeline), ordered: flag(raw.ordered) };
1110
+ case "dynamic":
1111
+ if (raw.text != null && typeof raw.text !== "string" || !Array.isArray(raw.args)) throw new Error("bad item");
1112
+ return { kind: "dynamic", text: typeof raw.text === "string" ? raw.text : null, args: raw.args.map(valueOf) };
1113
+ default:
1114
+ throw new Error("bad item");
1115
+ }
1116
+ }
1117
+ function parse(stdout) {
1118
+ const parsed = JSON.parse(stdout);
1119
+ if (!isRecord2(parsed) || parsed.errors !== false || !Array.isArray(parsed.items) || !Array.isArray(parsed.unstable) || typeof parsed.anyVariable !== "boolean") throw new Error("bad answer");
1120
+ const unstable = parsed.unstable.map((name2) => {
1121
+ if (typeof name2 !== "string") throw new Error("bad answer");
1122
+ return baseName(name2);
1123
+ });
1124
+ return { items: parsed.items.map(itemOf), unstable: new Set(unstable), anyVariable: parsed.anyVariable };
1125
+ }
1126
+ var baseName = (name2) => name2.toLowerCase().replace(/^(?:script|local|global|private|using|variable):/, "");
1127
+ function fileSystemPath(value) {
1128
+ const qualified = /^(?:[\w.]+\\)?(\w+)::/.exec(value);
1129
+ if (!qualified) return value;
1130
+ return qualified[1].toLowerCase() === "filesystem" ? value.slice(qualified[0].length) : null;
1131
+ }
1132
+ function classify3(value, literal, cwd, ctx) {
1133
+ const text = fileSystemPath(value);
1134
+ if (text === null) return {};
1135
+ const drive = /^([A-Za-z][A-Za-z0-9_]+):/.exec(text);
1136
+ if (drive) return OTHER_DRIVES.has(drive[1].toLowerCase()) ? {} : { unresolvable: `a path on a PowerShell drive that cannot be worked out (${value})` };
1137
+ let base = text;
1138
+ let pattern = null;
1139
+ if (!literal && /[*?[]/.test(text)) {
1140
+ const split = splitWindowsPattern(text, /[*?[]/);
1141
+ if (split === null) return { unresolvable: `a wildcard in a folder name (${value})` };
1142
+ base = split.dir;
1143
+ pattern = split.pattern;
1144
+ }
1145
+ const resolved = resolveWindowsPath(base, cwd, ctx.home);
1146
+ if ("unresolvable" in resolved) return { unresolvable: resolved.unresolvable };
1147
+ if ("unc" in resolved) return { real: { shown: value, path: null } };
1148
+ return { real: judgePath(resolved.path, pattern, ctx) ?? void 0 };
1149
+ }
1150
+ function folderOf(value, literal, cwd, home) {
1151
+ const text = fileSystemPath(value);
1152
+ if (text === null || text === "-" || text === "+" || /^[A-Za-z][A-Za-z0-9_]+:/.test(text) || !literal && /[*?[]/.test(text)) return null;
1153
+ const resolved = resolveWindowsPath(text, cwd, home);
1154
+ return "path" in resolved ? resolved.path : null;
1155
+ }
1156
+ async function readPowerShell(source, ctx, run) {
1157
+ const out = emptyRead();
1158
+ if (!POWERSHELL_WORDS.test(source)) return out;
1159
+ const excerpt = source.slice(0, EXCERPT_CHARS2);
1160
+ const failed = () => ({ ...emptyRead(), failed: [excerpt] });
1161
+ try {
1162
+ const stdout = await run(source);
1163
+ if (stdout === null) return failed();
1164
+ const { items, unstable, anyVariable } = parse(stdout);
1165
+ const assigned = /* @__PURE__ */ new Map();
1166
+ for (const item of items) if (item.kind === "assign") assigned.set(baseName(item.name), (assigned.get(baseName(item.name)) ?? 0) + 1);
1167
+ const moves = items.some((item) => item.kind === "cd" || item.kind === "pop");
1168
+ const processFolder = /CurrentDirectory/i.test(source) ? null : ctx.cwd;
1169
+ const vars = /* @__PURE__ */ new Map();
1170
+ for (const name2 of KNOWN_ENV2) {
1171
+ const value = ctx.env[name2];
1172
+ if (value) vars.set(`env:${name2.toLowerCase()}`, value);
1173
+ }
1174
+ vars.set("home", ctx.home);
1175
+ let cwd = ctx.cwd;
1176
+ const variable = (name2, here) => {
1177
+ const key = baseName(name2);
1178
+ if (anyVariable || unstable.has(key)) return null;
1179
+ if (key === "pwd") return assigned.has("pwd") ? null : here;
1180
+ return vars.get(key) ?? null;
1181
+ };
1182
+ const textOf = (value, here = cwd) => {
1183
+ if (!value) return null;
1184
+ if ("const" in value) return value.const;
1185
+ if ("variable" in value) return variable(value.variable, here);
1186
+ if ("expand" in value) {
1187
+ let text = "";
1188
+ for (const part of value.expand) {
1189
+ const piece = "const" in part ? part.const : variable(part.variable, here);
1190
+ if (piece === null) return null;
1191
+ text += piece;
1192
+ }
1193
+ return text;
1194
+ }
1195
+ return null;
1196
+ };
1197
+ const deny = (why) => ({ ...out, deny: denyReason("powershell", why) });
1198
+ for (const item of items) {
1199
+ if (item.kind === "assign") {
1200
+ const key = baseName(item.name);
1201
+ vars.set(key, assigned.get(key) === 1 ? textOf(item.value) : null);
1202
+ } else if (item.kind === "pop") {
1203
+ cwd = null;
1204
+ } else if (item.kind === "cd") {
1205
+ const to = item.inBlock ? null : textOf(item.path);
1206
+ cwd = to === null ? null : folderOf(to, item.literal, cwd, ctx.home);
1207
+ } else if (item.kind === "shell") {
1208
+ if (item.inPipeline && DELETE_WORD.test(source)) return deny(`pipeline input feeding a shell (${item.raw})`);
1209
+ const here = item.ordered || !moves ? cwd : null;
1210
+ const args = item.args.map((a) => textOf(a, here)).map((t) => t !== null && item.name !== "cmd" && t.includes('"') ? null : t);
1211
+ out.shells.push({ name: item.name, args, cwd: here, raw: item.raw });
1212
+ } else if (item.kind === "dynamic") {
1213
+ const values = item.text === null ? null : item.args.map((a) => textOf(a));
1214
+ const code = values === null || values.includes(null) ? source : [item.text, ...values].join(" ");
1215
+ if (DELETE_WORD.test(code) && !out.failed.length) out.failed.push(excerpt);
1216
+ } else {
1217
+ if (item.whatIf) continue;
1218
+ if (item.bindError) {
1219
+ if (!out.failed.length) out.failed.push(excerpt);
1220
+ continue;
1221
+ }
1222
+ if (item.inPipeline) return deny("pipeline input feeding Remove-Item");
1223
+ const here = item.ordered || !moves ? cwd : null;
1224
+ const values = list(item.literalPath ?? item.path).flatMap((v) => "array" in v ? v.array : [v]);
1225
+ for (const v of values) {
1226
+ if ("dynamic" in v || "array" in v) return deny(`an expression that cannot be worked out (${"dynamic" in v ? v.dynamic : "a nested list"})`);
1227
+ const text = textOf(v, here);
1228
+ if (text === null) return deny(`a variable that cannot be worked out (${"variable" in v ? `$${v.variable}` : "expand" in v ? v.text : ""})`);
1229
+ const c = classify3(text, item.literal, item.dotnet ? processFolder : here, ctx);
1230
+ if (c.unresolvable) return deny(c.unresolvable);
1231
+ if (c.real) out.targets.push(c.real);
1232
+ }
1233
+ }
1234
+ }
1235
+ return out;
1236
+ } catch {
1237
+ return failed();
1238
+ }
1239
+ }
1240
+
1241
+ // src/rm/dispatch.ts
1242
+ var MAX_NESTING = 3;
1243
+ var POWERSHELL_VALUE_PARAMS = [["executionpolicy", 2], ["windowstyle", 2], ["outputformat", 2], ["inputformat", 2], ["workingdirectory", 2], ["configurationname", 6], ["settingsfile", 2], ["version", 2], ["psconsolefile", 2], ["custompipename", 2]];
1244
+ var POWERSHELL_VALUE_ALIASES = ["ep", "ex", "w", "o", "of", "if", "wd", "v"];
1245
+ var CMD_RUN = /^\/{1,2}(?:[a-z]+\/)*[ck](.*)$/is;
1246
+ var paramName = (arg) => arg.replace(/^(?:--|-|\/)/, "").toLowerCase();
1247
+ var isParam = (arg) => /^(?:--|-|\/)[A-Za-z]/.test(arg);
1248
+ var startsWord = (name2, full, min) => name2.length >= min && full.startsWith(name2);
1249
+ function cmdLine(args) {
1250
+ const line = args.map((a) => /\s/.test(a) && !/^".*"$/.test(a) ? `"${a}"` : a).join(" ");
1251
+ const last = line.lastIndexOf('"');
1252
+ return line.startsWith('"') && last > 0 ? `${line.slice(1, last)}${line.slice(last + 1)}` : line;
1253
+ }
1254
+ function bashScript(args) {
1255
+ let hasC = false;
1256
+ let i = 0;
1257
+ for (; i < args.length; i++) {
1258
+ const a = args[i];
1259
+ if (a === null) return { shell: "bash", script: null };
1260
+ if (a === "--") {
1261
+ i++;
1262
+ break;
1263
+ }
1264
+ if (!/^[-+]/.test(a)) break;
1265
+ if (/^-[A-Za-z]*c[A-Za-z]*$/.test(a)) hasC = true;
1266
+ if (/^[-+][oO]$/.test(a) || a === "--rcfile" || a === "--init-file") i++;
1267
+ }
1268
+ if (!hasC || i >= args.length) return null;
1269
+ return { shell: "bash", script: args[i] };
1270
+ }
1271
+ function powerShellScript(name2, args) {
1272
+ let moved = false;
1273
+ const found = (script) => ({ shell: "powershell", script, ...moved ? { moved } : {} });
1274
+ for (let i = 0; i < args.length; i++) {
1275
+ const a = args[i];
1276
+ if (a === null) return found(null);
1277
+ if (a === "-") return null;
1278
+ if (!isParam(a)) {
1279
+ if (name2 === "pwsh") return null;
1280
+ const rest = args.slice(i);
1281
+ return found(rest.includes(null) ? null : rest.join(" "));
1282
+ }
1283
+ const p = paramName(a);
1284
+ if (p === "e" || p === "ec" || startsWord(p, "encodedcommand", 2)) {
1285
+ const encoded = args[i + 1];
1286
+ if (encoded === void 0) return null;
1287
+ return found(encoded === null ? null : Buffer.from(encoded, "base64").toString("utf16le"));
1288
+ }
1289
+ if (p === "cwa" || startsWord(p, "command", 1) || startsWord(p, "commandwithargs", 8)) {
1290
+ const rest = args.slice(i + 1);
1291
+ if (!rest.length || rest[0] === "-") return null;
1292
+ return found(rest.includes(null) ? null : rest.join(" "));
1293
+ }
1294
+ if (p === "f" || startsWord(p, "file", 2)) return null;
1295
+ if (POWERSHELL_VALUE_ALIASES.includes(p) || POWERSHELL_VALUE_PARAMS.some(([full, min]) => startsWord(p, full, min))) {
1296
+ if (p === "wd" || startsWord(p, "workingdirectory", 2)) moved = true;
1297
+ i++;
1298
+ }
1299
+ }
1300
+ return null;
1301
+ }
1302
+ function nestedScript({ name: name2, args }) {
1303
+ if (name2 === "sh" || name2 === "bash") return bashScript(args);
1304
+ if (name2 === "powershell" || name2 === "pwsh") return powerShellScript(name2, args);
1305
+ if (name2 === "cmd") {
1306
+ const at = args.findIndex((a) => a !== null && CMD_RUN.test(a));
1307
+ if (at === -1) return null;
1308
+ const glued = CMD_RUN.exec(args[at])[1];
1309
+ const rest = [...glued ? [glued] : [], ...args.slice(at + 1)];
1310
+ return rest.includes(null) ? { shell: "cmd", script: null } : { shell: "cmd", script: cmdLine(rest) };
1311
+ }
1312
+ return null;
1313
+ }
1314
+ function readsStdin({ name: name2, args }) {
1315
+ if (args.includes(null)) return false;
1316
+ const given = args;
1317
+ if (name2 === "sh" || name2 === "bash") {
1318
+ for (let i = 0; i < given.length; i++) {
1319
+ const a = given[i];
1320
+ if (a === "--") return i + 1 === given.length;
1321
+ if (!/^[-+]/.test(a)) return false;
1322
+ if (/^-[A-Za-z]*s[A-Za-z]*$/.test(a)) return true;
1323
+ if (/^[-+][oO]$/.test(a) || a === "--rcfile" || a === "--init-file") i++;
1324
+ }
1325
+ return true;
1326
+ }
1327
+ if (name2 === "powershell" || name2 === "pwsh") {
1328
+ for (let i = 0; i < given.length; i++) {
1329
+ const a = given[i];
1330
+ if (a === "-") return true;
1331
+ if (!isParam(a)) return false;
1332
+ const p = paramName(a);
1333
+ if (p === "cwa" || startsWord(p, "command", 1) || startsWord(p, "commandwithargs", 8)) return given[i + 1] === void 0 || given[i + 1] === "-";
1334
+ if (p === "f" || startsWord(p, "file", 2) || p === "e" || p === "ec" || startsWord(p, "encodedcommand", 2)) return false;
1335
+ if (POWERSHELL_VALUE_ALIASES.includes(p) || POWERSHELL_VALUE_PARAMS.some(([full, min]) => startsWord(p, full, min))) i++;
1336
+ }
1337
+ return true;
1338
+ }
1339
+ return name2 === "cmd" && !given.some((a) => CMD_RUN.test(a));
1340
+ }
1341
+ function readShell(shell, script, ctx, run) {
1342
+ if (shell === "bash") return readBash(script, ctx);
1343
+ if (shell === "cmd") return readCmd(script, ctx);
1344
+ return readPowerShell(script, ctx, run);
1345
+ }
1346
+ async function readNested(shell, script, ctx, run, depth) {
1347
+ const result = await readShell(shell, script, ctx, run);
1348
+ if (result.deny !== null) return result;
1349
+ const merged = { deny: null, targets: [...result.targets], failed: [...result.failed], shells: [] };
1350
+ for (const call of result.shells) {
1351
+ const nested = nestedScript(call);
1352
+ if (nested === null) {
1353
+ if ((call.piped || readsStdin(call)) && DELETE_WORDS.test(script)) return { ...merged, deny: denyReason(shell, `a shell reading its script from stdin (${call.raw})`) };
1354
+ continue;
1355
+ }
1356
+ if (nested.script === null || depth >= MAX_NESTING) {
1357
+ if (!DELETE_WORDS.test(call.raw) && !DELETE_WORDS.test(nested.script ?? "")) continue;
1358
+ const why = nested.script === null ? `a ${call.name} script that cannot be worked out (${call.raw})` : `shells nested more than ${MAX_NESTING} deep (${call.raw})`;
1359
+ return { ...merged, deny: denyReason(shell, why) };
1360
+ }
1361
+ const inner = await readNested(nested.shell, nested.script, { ...ctx, cwd: nested.moved ? null : call.cwd }, run, depth + 1);
1362
+ if (inner.deny !== null) return { ...merged, deny: inner.deny };
1363
+ merged.targets.push(...inner.targets);
1364
+ merged.failed.push(...inner.failed);
1365
+ }
1366
+ return merged;
1367
+ }
1368
+ async function decideTool(tool, command, ctx, run) {
1369
+ return decisionOf(await readNested(tool === "Bash" ? "bash" : "powershell", command, ctx, run, 0));
1370
+ }
1371
+
509
1372
  // src/rm/facts.ts
510
- import fs6 from "fs";
511
- import path5 from "path";
1373
+ import fs7 from "fs";
1374
+ import path8 from "path";
512
1375
  import { spawnSync as spawnSync2 } from "child_process";
513
1376
 
514
1377
  // src/context/transcript.ts
515
- import fs5 from "fs";
1378
+ import fs6 from "fs";
516
1379
  var TAIL_START_BYTES = 1024 * 1024;
517
1380
  var TAIL_MAX_BYTES = 8 * 1024 * 1024;
518
- function isRecord2(value) {
1381
+ function isRecord3(value) {
519
1382
  return typeof value === "object" && value !== null && !Array.isArray(value);
520
1383
  }
521
1384
  function parseLines(text) {
@@ -524,7 +1387,7 @@ function parseLines(text) {
524
1387
  if (!line.trim()) continue;
525
1388
  try {
526
1389
  const value = JSON.parse(line);
527
- if (isRecord2(value)) entries.push(value);
1390
+ if (isRecord3(value)) entries.push(value);
528
1391
  } catch {
529
1392
  }
530
1393
  }
@@ -534,11 +1397,13 @@ function parseLines(text) {
534
1397
  // src/rm/facts.ts
535
1398
  var SESSION_LOG_MAX_BYTES = 32 * 1024 * 1024;
536
1399
  var CALLS_PER_TARGET = 3;
537
- var win2 = path5.win32;
1400
+ var win4 = path8.win32;
538
1401
  var DELETING = /\b(?:rm|rmdir|del|erase|rd|ri|Remove-Item|xargs)\b/i;
539
1402
  var GIT_LOCATION_ENV = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"];
540
- var CHANGES_FOLDER = /(^|[\s;&|(])(?:cd|pushd|popd|chdir|Set-Location|sl)(?=\s|$)/i;
541
- function isRecord3(value) {
1403
+ var MOVES = ["cd", "pushd", "popd", "chdir", "set-location", "sl", "push-location", "pop-location"];
1404
+ var CHANGES_FOLDER = new RegExp(`(^|[\\s;&|(])(?:${MOVES.join("|")})(?=[\\s;&|)]|$)`, "i");
1405
+ var MOVING_TOOLS = /* @__PURE__ */ new Set(["EnterWorktree", "ExitWorktree"]);
1406
+ function isRecord4(value) {
542
1407
  return typeof value === "object" && value !== null && !Array.isArray(value);
543
1408
  }
544
1409
  function callOf(tool, input, result, cwd) {
@@ -550,7 +1415,7 @@ function callOf(tool, input, result, cwd) {
550
1415
  return { tool, file: text("file_path"), text: text("new_string"), kind: "change", cwd };
551
1416
  case "MultiEdit": {
552
1417
  const edits = Array.isArray(input.edits) ? input.edits : [];
553
- return { tool, file: text("file_path"), text: edits.map((e) => isRecord3(e) && typeof e.new_string === "string" ? e.new_string : "").join("\n"), kind: "change", cwd };
1418
+ return { tool, file: text("file_path"), text: edits.map((e) => isRecord4(e) && typeof e.new_string === "string" ? e.new_string : "").join("\n"), kind: "change", cwd };
554
1419
  }
555
1420
  case "NotebookEdit":
556
1421
  return { tool, file: text("notebook_path"), text: text("new_source"), kind: "change", cwd };
@@ -566,10 +1431,10 @@ function callOf(tool, input, result, cwd) {
566
1431
  function resultTypes(entries) {
567
1432
  const types = /* @__PURE__ */ new Map();
568
1433
  for (const entry of entries) {
569
- if (entry.type !== "user" || !isRecord3(entry.toolUseResult) || typeof entry.toolUseResult.type !== "string" || !isRecord3(entry.message)) continue;
1434
+ if (entry.type !== "user" || !isRecord4(entry.toolUseResult) || typeof entry.toolUseResult.type !== "string" || !isRecord4(entry.message)) continue;
570
1435
  const content = entry.message.content;
571
1436
  const block = Array.isArray(content) ? content[0] : void 0;
572
- if (isRecord3(block) && typeof block.tool_use_id === "string") types.set(block.tool_use_id, entry.toolUseResult.type);
1437
+ if (isRecord4(block) && typeof block.tool_use_id === "string") types.set(block.tool_use_id, entry.toolUseResult.type);
573
1438
  }
574
1439
  return types;
575
1440
  }
@@ -579,12 +1444,13 @@ function toolCalls(entries) {
579
1444
  let message;
580
1445
  let moved = false;
581
1446
  for (const entry of entries) {
582
- if (entry.type !== "assistant" || entry.isSidechain === true || !isRecord3(entry.message) || !Array.isArray(entry.message.content)) continue;
1447
+ if (entry.type !== "assistant" || entry.isSidechain === true || !isRecord4(entry.message) || !Array.isArray(entry.message.content)) continue;
583
1448
  if (entry.message.id === void 0 || entry.message.id !== message) moved = false;
584
1449
  message = entry.message.id;
585
1450
  const cwd = typeof entry.cwd === "string" ? entry.cwd : null;
586
1451
  for (const block of entry.message.content) {
587
- if (!isRecord3(block) || block.type !== "tool_use" || typeof block.name !== "string" || !isRecord3(block.input)) continue;
1452
+ if (!isRecord4(block) || block.type !== "tool_use" || typeof block.name !== "string" || !isRecord4(block.input)) continue;
1453
+ if (MOVING_TOOLS.has(block.name)) moved = true;
588
1454
  const call = callOf(block.name, block.input, typeof block.id === "string" ? results.get(block.id) : void 0, moved ? null : cwd);
589
1455
  if (!call) continue;
590
1456
  calls.push(call);
@@ -596,8 +1462,8 @@ function toolCalls(entries) {
596
1462
  function readSessionLog(file, maxBytes = SESSION_LOG_MAX_BYTES) {
597
1463
  let text;
598
1464
  try {
599
- if (fs6.statSync(file).size > maxBytes) return null;
600
- text = fs6.readFileSync(file, "utf8");
1465
+ if (fs7.statSync(file).size > maxBytes) return null;
1466
+ text = fs7.readFileSync(file, "utf8");
601
1467
  } catch {
602
1468
  return null;
603
1469
  }
@@ -611,8 +1477,8 @@ function normalized(text) {
611
1477
  var escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
612
1478
  function shellRefs(target, cwd) {
613
1479
  const refs = [`(?<![\\w.-])${escaped(normalized(target))}(?![\\w.-])`];
614
- const relative = cwd === null ? "" : win2.relative(cwd, target);
615
- if (relative && !relative.startsWith("..") && !win2.isAbsolute(relative)) {
1480
+ const relative = cwd === null ? "" : win4.relative(cwd, target);
1481
+ if (relative && !relative.startsWith("..") && !win4.isAbsolute(relative)) {
616
1482
  refs.push(`(?<![\\w.\\-/:])(?:\\./)?${escaped(normalized(relative))}(?![\\w.-])`);
617
1483
  }
618
1484
  return refs;
@@ -677,18 +1543,18 @@ var pathWord = (word) => word.replace(/\\/g, "/").replace(/^\/([A-Za-z])\//, "$1
677
1543
  function createsTarget(call, target) {
678
1544
  if (call.kind !== "shell") return call.kind === "create";
679
1545
  const full = pathWord(target);
680
- const relative = call.cwd === null ? "" : win2.relative(call.cwd, target);
681
- const short = relative && !relative.startsWith("..") && !win2.isAbsolute(relative) ? pathWord(relative) : null;
1546
+ const relative = call.cwd === null ? "" : win4.relative(call.cwd, target);
1547
+ const short = relative && !relative.startsWith("..") && !win4.isAbsolute(relative) ? pathWord(relative) : null;
682
1548
  let moved = false;
683
1549
  const isTarget = (word) => word !== void 0 && (pathWord(word) === full || !moved && pathWord(word) === short);
684
1550
  for (const { words, writes } of shellCommands(stripHeredocs(call.text))) {
685
1551
  if (writes.some(isTarget)) return true;
686
1552
  const start = words.findIndex((w) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(w));
687
1553
  const args = start === -1 ? [] : words.slice(start);
688
- const name2 = win2.basename((args[0] ?? "").replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
1554
+ const name2 = win4.basename((args[0] ?? "").replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
689
1555
  if ((name2 === "touch" || name2 === "mkdir") && args.slice(1).some(isTarget)) return true;
690
1556
  if (name2 === "curl" && args.some((w, i) => (w === "-o" || w === "--output") && isTarget(args[i + 1]))) return true;
691
- if (name2 === "cd" || name2 === "pushd" || name2 === "popd") moved = true;
1557
+ if (MOVES.includes(name2)) moved = true;
692
1558
  }
693
1559
  return false;
694
1560
  }
@@ -712,8 +1578,9 @@ function gatherFacts(target, session, probe, maxFiles) {
712
1578
  function candidateKind(facts, startedAt) {
713
1579
  const fresh = (bornAt2) => bornAt2 >= startedAt;
714
1580
  const freshInside = !facts.folder || facts.listing !== null && !facts.listing.more && facts.listing.entries.every((e) => fresh(e.bornAt));
715
- if (!facts.tracked && !facts.existedBefore && facts.createdBy.length > 0 && fresh(facts.bornAt) && freshInside) return "session";
716
- if (facts.folder && facts.ignored && !facts.listing?.repo) return "ignored";
1581
+ const repo = facts.folder && (facts.listing === null || facts.listing.repo);
1582
+ if (!facts.tracked && !facts.existedBefore && facts.createdBy.length > 0 && fresh(facts.bornAt) && freshInside && !repo) return "session";
1583
+ if (facts.folder && facts.ignored && !repo) return "ignored";
717
1584
  return null;
718
1585
  }
719
1586
  var bornAt = (stat) => stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
@@ -725,7 +1592,7 @@ function git(args, extraEnv = {}) {
725
1592
  var realFactProbe = {
726
1593
  info(p) {
727
1594
  try {
728
- const stat = fs6.lstatSync(p);
1595
+ const stat = fs7.lstatSync(p);
729
1596
  return { folder: stat.isDirectory(), bornAt: bornAt(stat) };
730
1597
  } catch {
731
1598
  return null;
@@ -737,7 +1604,7 @@ var realFactProbe = {
737
1604
  let more = false;
738
1605
  let repo = false;
739
1606
  const walk = (folder, prefix) => {
740
- for (const item of fs6.readdirSync(folder, { withFileTypes: true })) {
1607
+ for (const item of fs7.readdirSync(folder, { withFileTypes: true })) {
741
1608
  if (more) return;
742
1609
  if (seen >= limit) {
743
1610
  more = true;
@@ -745,33 +1612,36 @@ var realFactProbe = {
745
1612
  }
746
1613
  seen += 1;
747
1614
  if (item.name === ".git") repo = true;
748
- const full = path5.join(folder, item.name);
1615
+ const full = path8.join(folder, item.name);
749
1616
  const name2 = prefix ? `${prefix}/${item.name}` : item.name;
750
1617
  if (item.isDirectory()) walk(full, name2);
751
- else entries.push({ name: name2, bornAt: bornAt(fs6.lstatSync(full)) });
1618
+ else entries.push({ name: name2, bornAt: bornAt(fs7.lstatSync(full)) });
752
1619
  }
753
1620
  };
754
1621
  try {
755
1622
  walk(dir, "");
1623
+ if (!repo) {
1624
+ repo = fs7.readdirSync(dir, { withFileTypes: true }).some((item) => item.name === ".git" || item.isDirectory() && fs7.existsSync(path8.join(dir, item.name, ".git")));
1625
+ }
756
1626
  return { entries, more, repo };
757
1627
  } catch {
758
1628
  return null;
759
1629
  }
760
1630
  },
761
1631
  tracked(p) {
762
- if (fs6.existsSync(path5.join(p, ".git"))) return true;
763
- const r = git(["-C", win2.dirname(p), "ls-files", "--", p], { GIT_ICASE_PATHSPECS: "1" });
1632
+ if (fs7.existsSync(path8.join(p, ".git"))) return true;
1633
+ const r = git(["-C", win4.dirname(p), "ls-files", "--", p], { GIT_ICASE_PATHSPECS: "1" });
764
1634
  if (r.status === 0) return r.stdout.trim() !== "";
765
1635
  return !(r.status === 128 && /not a git repository/i.test(r.stderr ?? ""));
766
1636
  },
767
1637
  ignored(p) {
768
- if (fs6.existsSync(path5.join(p, ".git"))) return false;
769
- return git(["-C", win2.dirname(p), "check-ignore", "-q", "--", p]).status === 0;
1638
+ if (fs7.existsSync(path8.join(p, ".git"))) return false;
1639
+ return git(["-C", win4.dirname(p), "check-ignore", "-q", "--", p]).status === 0;
770
1640
  }
771
1641
  };
772
1642
 
773
1643
  // src/rm/judge.ts
774
- import path6 from "path";
1644
+ import path9 from "path";
775
1645
  var CALL_EXCERPT_CHARS = 600;
776
1646
  var SAMPLE_FILES = 10;
777
1647
  var RM_QUESTIONS = {
@@ -830,7 +1700,7 @@ ${c.text}` : c.text).slice(0, CALL_EXCERPT_CHARS) }));
830
1700
  }
831
1701
  return { state, questions: { ok: RM_QUESTIONS[kind] } };
832
1702
  }
833
- var name = (item) => path6.win32.basename(item.path);
1703
+ var name = (item) => path9.win32.basename(item.path);
834
1704
  var prob = (p) => p.toFixed(2);
835
1705
  function lifts(items, threshold) {
836
1706
  return items.length > 0 && items.every((item) => item.p !== null && item.p >= threshold);
@@ -850,6 +1720,7 @@ function askNote(lang, items) {
850
1720
 
851
1721
  // src/rm/run.ts
852
1722
  var MAX_JEV_TARGETS = 5;
1723
+ var READ_BUDGET_MS = 9e3;
853
1724
  var FACTS_BUDGET_MS = 5e3;
854
1725
  var JEV_PATH_BUDGET_MS = 12e3;
855
1726
  var MIN_JEV_MS = 1e3;
@@ -892,10 +1763,14 @@ async function runRmHook(raw, deps) {
892
1763
  const input = JSON.parse(raw);
893
1764
  if (typeof input !== "object" || input === null) return null;
894
1765
  const command = input.tool_input?.command;
895
- if (input.tool_name !== "Bash" || typeof command !== "string") return null;
896
- const tmpdirs = [...new Set([deps.tmpdir, deps.env.TEMP, deps.env.TMP].filter((t) => Boolean(t)).map((t) => path7.win32.resolve(t)))];
1766
+ const tool = input.tool_name;
1767
+ if (tool !== "Bash" && tool !== "PowerShell" || typeof command !== "string") return null;
1768
+ const tmpdirs = [...new Set([deps.tmpdir, deps.env.TEMP, deps.env.TMP].filter((t) => Boolean(t)).map((t) => path10.win32.resolve(t)))];
897
1769
  const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : deps.cwd;
898
- const result = decide({ command, cwd, home: deps.home, tmpdirs, env: deps.env, probe: deps.probe });
1770
+ const ctx = { cwd, home: deps.home, tmpdirs, env: deps.env, probe: deps.probe };
1771
+ const powershell = deps.powershell ?? realPowerShell(deps.env);
1772
+ const read = (source) => now().getTime() - started >= READ_BUDGET_MS ? Promise.resolve(null) : powershell(source);
1773
+ const result = await decideTool(tool, command, ctx, read);
899
1774
  if (!result) return null;
900
1775
  if (result.decision !== "ask" || !result.targets || !config.rm.jev) return hookOutput(result);
901
1776
  const key = readJevKey(deps.env, config.keyFile);
@@ -918,6 +1793,7 @@ async function runRmHook(raw, deps) {
918
1793
  event: "pre_tool_use",
919
1794
  sessionId: typeof input.session_id === "string" ? input.session_id : null,
920
1795
  transcriptPath: input.transcript_path,
1796
+ tool,
921
1797
  command,
922
1798
  cwd,
923
1799
  decision: pass ? "pass" : "ask",