@delt/claude-jev-advisor 0.2.0 → 0.3.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;
@@ -404,11 +485,11 @@ function normalizeConfig(raw) {
404
485
  }
405
486
 
406
487
  // 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");
488
+ import path3 from "path";
489
+ var claudeDir = (home) => path3.join(home, ".claude");
490
+ var dataDir = (home) => path3.join(claudeDir(home), "claude-jev-advisor");
491
+ var configPath = (home) => path3.join(dataDir(home), "config.json");
492
+ var logDir = (home) => path3.join(dataDir(home), "log");
412
493
 
413
494
  // src/config.ts
414
495
  function readConfig(home) {
@@ -421,13 +502,13 @@ function readConfig(home) {
421
502
 
422
503
  // src/context/files.ts
423
504
  import fs3 from "fs";
424
- import path4 from "path";
505
+ import path5 from "path";
425
506
  function logFile(home, now) {
426
- return path4.join(logDir(home), `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}.jsonl`);
507
+ return path5.join(logDir(home), `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}.jsonl`);
427
508
  }
428
509
  function appendLog(home, now, record, secrets = []) {
429
510
  const file = logFile(home, now);
430
- fs3.mkdirSync(path4.dirname(file), { recursive: true });
511
+ fs3.mkdirSync(path5.dirname(file), { recursive: true });
431
512
  let line = JSON.stringify({ at: now.toISOString(), ...record });
432
513
  for (const secret of secrets) {
433
514
  if (!secret) continue;
@@ -506,16 +587,796 @@ async function callJev(request, opts) {
506
587
  }
507
588
  }
508
589
 
590
+ // src/rm/cmd.ts
591
+ import path6 from "path";
592
+ var win3 = path6.win32;
593
+ var SHELLS2 = /* @__PURE__ */ new Set(["sh", "bash", "cmd", "powershell", "pwsh"]);
594
+ var DELETES = /* @__PURE__ */ new Set(["del", "erase", "rd", "rmdir"]);
595
+ var CONTROL = /* @__PURE__ */ new Set(["if", "else", "for", "call", "start"]);
596
+ var UNKNOWN = "\0";
597
+ var EXCERPT_CHARS = 200;
598
+ function expandVars(line, vars) {
599
+ return line.replace(/%([^%\s"]+)%/g, (_m, name2) => vars.get(name2.toLowerCase()) ?? UNKNOWN).replace(/%~[^\s"]*/g, UNKNOWN).replace(/![^!\s"]+!/g, UNKNOWN);
600
+ }
601
+ function hasBlock(line) {
602
+ let quoted = false;
603
+ for (let i = 0; i < line.length; i++) {
604
+ if (line[i] === '"') quoted = !quoted;
605
+ else if (!quoted && line[i] === "^") i++;
606
+ else if (!quoted && (line[i] === "(" || line[i] === ")")) return true;
607
+ }
608
+ return false;
609
+ }
610
+ function segmentsOf(line) {
611
+ const out = [];
612
+ let current = "";
613
+ let piped = false;
614
+ let quoted = false;
615
+ for (let i = 0; i < line.length; i++) {
616
+ const c = line[i];
617
+ if (c === '"') quoted = !quoted;
618
+ if (!quoted && c === "^" && i + 1 < line.length) {
619
+ current += c + line[++i];
620
+ continue;
621
+ }
622
+ const redirect = c === "&" && /[<>]$/.test(current);
623
+ if (!quoted && !redirect && "&|\n".includes(c)) {
624
+ out.push({ text: current, piped });
625
+ current = "";
626
+ piped = c === "|" && line[i + 1] !== "|";
627
+ if ((c === "&" || c === "|") && line[i + 1] === c) i++;
628
+ continue;
629
+ }
630
+ current += c;
631
+ }
632
+ out.push({ text: current, piped });
633
+ return out.filter((s) => s.text.trim() !== "");
634
+ }
635
+ function wordsOf(segment, delimiters) {
636
+ const words = [];
637
+ let word = null;
638
+ let quoted = false;
639
+ let skipNext = false;
640
+ const start = () => word ??= { value: "", dynamic: false, shown: "", end: 0 };
641
+ const add = (c) => {
642
+ const w = start();
643
+ if (c === UNKNOWN) {
644
+ w.dynamic = true;
645
+ w.shown += "%?%";
646
+ } else {
647
+ w.value += c;
648
+ w.shown += c;
649
+ }
650
+ };
651
+ const finish = (at) => {
652
+ if (word === null) return;
653
+ word.end = at;
654
+ if (skipNext) skipNext = false;
655
+ else words.push(word);
656
+ word = null;
657
+ };
658
+ for (let i = 0; i < segment.length; i++) {
659
+ const c = segment[i];
660
+ if (c === '"') {
661
+ quoted = !quoted;
662
+ start();
663
+ continue;
664
+ }
665
+ if (quoted) {
666
+ add(c);
667
+ continue;
668
+ }
669
+ if (c === "^") {
670
+ if (i + 1 < segment.length) add(segment[++i]);
671
+ continue;
672
+ }
673
+ if (c === " " || c === " " || c === "\r" || delimiters.includes(c)) {
674
+ finish(i);
675
+ continue;
676
+ }
677
+ if (c === ">" || c === "<") {
678
+ if (word !== null && /^\d$/.test(word.value) && !word.dynamic) word = null;
679
+ else finish(i);
680
+ while (segment[i + 1] === ">") i++;
681
+ if (segment[i + 1] === "&") {
682
+ i++;
683
+ while (/\d/.test(segment[i + 1] ?? "")) i++;
684
+ continue;
685
+ }
686
+ skipNext = true;
687
+ continue;
688
+ }
689
+ add(c);
690
+ }
691
+ finish(segment.length);
692
+ return words;
693
+ }
694
+ function nameOf(word) {
695
+ if (word.dynamic && /%\?%[^\\/]*$/.test(word.shown)) return null;
696
+ const text = word.value.replace(/^@/, "");
697
+ const glued = /^(del|erase|rd|rmdir|cd|chdir|pushd|popd|if|else|for|call|start)([/,;=].*)$/i.exec(text) ?? /^(cd|chdir|pushd)([.\\].*)$/i.exec(text);
698
+ if (glued) return { name: glued[1].toLowerCase(), rest: glued[2] };
699
+ return { name: win3.basename(text.replace(/\//g, "\\")).replace(/\.(?:exe|com)$/i, "").toLowerCase(), rest: "" };
700
+ }
701
+ function classify2(word, cwd, everywhere, ctx) {
702
+ if (word.dynamic || word.value.includes("%")) return { unresolvable: `a variable that cannot be worked out (${word.shown})` };
703
+ let base = word.value;
704
+ let pattern = null;
705
+ if (everywhere || /[*?]/.test(word.value)) {
706
+ const split = splitWindowsPattern(word.value, /[*?]/);
707
+ if (split === null) return { unresolvable: `a wildcard in a folder name (${word.value})` };
708
+ base = split.dir;
709
+ pattern = split.pattern;
710
+ }
711
+ const resolved = resolveWindowsPath(base, cwd, null);
712
+ if ("unresolvable" in resolved) return { unresolvable: resolved.unresolvable };
713
+ if ("unc" in resolved) return { real: { shown: word.value, path: null } };
714
+ return { real: judgePath(resolved.path, pattern, ctx) ?? void 0 };
715
+ }
716
+ function readCmd(line, ctx) {
717
+ const out = emptyRead();
718
+ const vars = /* @__PURE__ */ new Map();
719
+ for (const name2 of ["TEMP", "TMP", "USERPROFILE"]) {
720
+ const value = ctx.env[name2];
721
+ if (value) vars.set(name2.toLowerCase(), value);
722
+ }
723
+ if (ctx.cwd !== null) vars.set("cd", ctx.cwd);
724
+ let cwd = ctx.cwd;
725
+ const deny = (why) => ({ ...out, deny: denyReason("cmd", why) });
726
+ const expanded = expandVars(line, vars);
727
+ const pieces = segmentsOf(expanded).map((s) => ({ ...s, text: s.text.replace(/^[\s@,;=]+/, "") })).filter((s) => s.text !== "");
728
+ const segments2 = pieces.map((s) => s.text);
729
+ const control = segments2.some((s) => {
730
+ const first = wordsOf(s, "")[0];
731
+ const named = first ? nameOf(first) : null;
732
+ return first !== void 0 && (named === null || CONTROL.has(named.name));
733
+ });
734
+ if (control || hasBlock(expanded)) {
735
+ if (!DELETE_WORDS.test(expanded)) return out;
736
+ const shown = expanded.trim().replace(/\s+/g, " ").slice(0, EXCERPT_CHARS).split(UNKNOWN).join("%?%");
737
+ 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`);
738
+ }
739
+ for (const { text: segment, piped } of pieces) {
740
+ const words = wordsOf(segment, "");
741
+ if (!words.length) continue;
742
+ const first = words[0];
743
+ const named = nameOf(first);
744
+ const shown = segment.trim().split(UNKNOWN).join("%?%");
745
+ const { name: name2, rest } = named;
746
+ const args = () => [...rest ? wordsOf(rest, ",;=") : [], ...wordsOf(segment.slice(first.end), ",;=")];
747
+ if (/^[a-z]:$/i.test(first.value.replace(/^@/, ""))) {
748
+ cwd = null;
749
+ } else if (SHELLS2.has(name2)) {
750
+ const msys = name2 === "sh" || name2 === "bash";
751
+ 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 } : {} });
752
+ } else if (name2 === "popd") {
753
+ cwd = null;
754
+ } else if (name2 === "cd" || name2 === "chdir" || name2 === "pushd") {
755
+ const given = args();
756
+ const switchDrive = name2 === "pushd" || given.some((w) => !w.dynamic && w.value.toLowerCase() === "/d");
757
+ const parts = given.filter((w) => w.dynamic || w.value.toLowerCase() !== "/d");
758
+ if (!parts.length) continue;
759
+ const resolved = parts.some((w) => w.dynamic) ? null : resolveWindowsPath(parts.map((w) => w.value).join(" "), cwd, null);
760
+ if (resolved === null || !("path" in resolved)) cwd = null;
761
+ else if (switchDrive || cwd === null || resolved.path.slice(0, 2).toLowerCase() === cwd.slice(0, 2).toLowerCase()) cwd = resolved.path;
762
+ } else if (DELETES.has(name2)) {
763
+ const given = args();
764
+ const switches = given.filter((w) => !w.dynamic && w.value.startsWith("/"));
765
+ const everywhere = (name2 === "del" || name2 === "erase") && switches.some((w) => w.value.split("/").some((s) => s.toLowerCase() === "s"));
766
+ for (const word of given) {
767
+ if (switches.includes(word)) continue;
768
+ const c = classify2(word, cwd, everywhere, ctx);
769
+ if (c.unresolvable) return deny(c.unresolvable);
770
+ if (c.real) out.targets.push(c.real);
771
+ }
772
+ }
773
+ }
774
+ return out;
775
+ }
776
+
777
+ // src/rm/powershell.ts
778
+ import fs5 from "fs";
779
+ import path7 from "path";
780
+ import { spawn } from "child_process";
781
+ var PS_PARSE_TIMEOUT_MS = 3e3;
782
+ var POWERSHELL_WORDS = /\b(?:remove-item|ri|rm|del|erase|rd|rmdir|sh|bash|cmd|powershell|pwsh)(?:\.exe)?\b|\bdelete/i;
783
+ var DELETE_WORD = new RegExp(`${DELETE_WORDS.source}|\\bdelete`, "i");
784
+ var OTHER_DRIVES = /* @__PURE__ */ new Set(["alias", "env", "function", "variable", "hklm", "hkcu", "cert", "wsman"]);
785
+ var EXCERPT_CHARS2 = 200;
786
+ var KNOWN_ENV2 = ["TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE"];
787
+ var READER = String.raw`
788
+ param($source)
789
+ $ErrorActionPreference = 'Stop'
790
+ $tokens = $null
791
+ $errors = $null
792
+ $root = [System.Management.Automation.Language.Parser]::ParseInput($source, [ref]$tokens, [ref]$errors)
793
+
794
+ function Value($ast) {
795
+ if ($null -eq $ast) { return @{ dynamic = '' } }
796
+ if ($ast -is [System.Management.Automation.Language.StringConstantExpressionAst]) { return @{ const = $ast.Value } }
797
+ if ($ast -is [System.Management.Automation.Language.ConstantExpressionAst]) { return @{ const = [string]$ast.Value } }
798
+ if ($ast -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) {
799
+ if ($ast.Extent.Text.Contains([string][char]96 + '$')) { return @{ dynamic = $ast.Extent.Text } }
800
+ $parts = New-Object System.Collections.ArrayList
801
+ $at = 0
802
+ foreach ($n in $ast.NestedExpressions) {
803
+ if (-not ($n -is [System.Management.Automation.Language.VariableExpressionAst]) -or $n.Splatted) { return @{ dynamic = $ast.Extent.Text } }
804
+ $i = $ast.Value.IndexOf($n.Extent.Text, $at, [System.StringComparison]::Ordinal)
805
+ if ($i -lt 0) { return @{ dynamic = $ast.Extent.Text } }
806
+ if ($i -gt $at) { [void]$parts.Add(@{ const = $ast.Value.Substring($at, $i - $at) }) }
807
+ [void]$parts.Add(@{ variable = $n.VariablePath.UserPath })
808
+ $at = $i + $n.Extent.Text.Length
809
+ }
810
+ if ($at -lt $ast.Value.Length) { [void]$parts.Add(@{ const = $ast.Value.Substring($at) }) }
811
+ return @{ expand = $parts.ToArray(); text = $ast.Extent.Text }
812
+ }
813
+ if ($ast -is [System.Management.Automation.Language.VariableExpressionAst] -and -not $ast.Splatted) { return @{ variable = $ast.VariablePath.UserPath } }
814
+ if ($ast -is [System.Management.Automation.Language.ArrayLiteralAst]) { return @{ array = @($ast.Elements | ForEach-Object { Value $_ }) } }
815
+ if ($ast -is [System.Management.Automation.Language.CommandParameterAst]) { return @{ const = $ast.Extent.Text } }
816
+ return @{ dynamic = $ast.Extent.Text }
817
+ }
818
+
819
+ function Constant($ast) {
820
+ if ($ast -is [System.Management.Automation.Language.CommandParameterAst]) { return ($null -eq $ast.Argument) -or (Constant $ast.Argument) }
821
+ if ($ast -is [System.Management.Automation.Language.ArrayLiteralAst]) { return @($ast.Elements | Where-Object { -not (Constant $_) }).Count -eq 0 }
822
+ return $ast -is [System.Management.Automation.Language.ConstantExpressionAst]
823
+ }
824
+
825
+ function NameOf($command) {
826
+ $name = $command.GetCommandName()
827
+ if ($null -eq $name) { return $null }
828
+ if ($name -eq 'cd..' -or $name -eq 'cd\') { return $name }
829
+ $name = $name -replace '^Microsoft\.PowerShell\.Management\\', ''
830
+ $name = ($name -split '[\\/]')[-1]
831
+ return ($name -replace '\.exe$', '').ToLowerInvariant()
832
+ }
833
+
834
+ function TopLevel($ast) {
835
+ for ($p = $ast.Parent; $null -ne $p; $p = $p.Parent) {
836
+ if ($p -eq $root) { return $true }
837
+ if (-not ($p -is [System.Management.Automation.Language.PipelineAst] -or $p -is [System.Management.Automation.Language.NamedBlockAst])) { return $false }
838
+ }
839
+ return $false
840
+ }
841
+
842
+ function InOrder($ast) {
843
+ for ($p = $ast.Parent; $null -ne $p -and $p -ne $root; $p = $p.Parent) {
844
+ 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 }
845
+ }
846
+ return $true
847
+ }
848
+
849
+ function Bound($command) {
850
+ $binding = [System.Management.Automation.Language.StaticParameterBinder]::BindCommand($command, $true)
851
+ $out = @{ bindError = ($binding.BindingExceptions.Count -gt 0); whatIf = $false }
852
+ foreach ($key in $binding.BoundParameters.Keys) {
853
+ $result = $binding.BoundParameters[$key]
854
+ switch ($key) {
855
+ 'Path' { $out.path = Value $result.Value }
856
+ 'LiteralPath' { $out.literalPath = Value $result.Value }
857
+ 'Name' { $out.name = $result }
858
+ 'MemberName' { $out.member = $result }
859
+ 'WhatIf' {
860
+ $v = $result.Value
861
+ if ($null -eq $v) { $out.whatIf = ($result.ConstantValue -eq $true) }
862
+ else { $out.whatIf = ($v -is [System.Management.Automation.Language.VariableExpressionAst]) -and ($v.VariablePath.UserPath -eq 'true') }
863
+ }
864
+ }
865
+ }
866
+ return $out
867
+ }
868
+
869
+ $deletes = @('remove-item', 'ri', 'rm', 'del', 'erase', 'rd', 'rmdir')
870
+ $moves = @('set-location', 'sl', 'cd', 'chdir', 'push-location', 'pushd')
871
+ $pops = @('pop-location', 'popd')
872
+ $shells = @('sh', 'bash', 'cmd', 'powershell', 'pwsh')
873
+ $dynamics = @('invoke-expression', 'iex', 'start-process', 'saps', 'start', 'invoke-command', 'icm', 'start-job', 'sajb', 'start-threadjob', 'set-alias', 'sal', 'new-alias', 'nal', 'add-type')
874
+ $members = @('foreach-object', '%', 'foreach')
875
+ $drives = @('new-psdrive', 'ndr', 'mount')
876
+ $variableCommands = @('set-variable', 'sv', 'new-variable', 'nv', 'remove-variable', 'rv', 'clear-variable', 'clv', 'get-variable', 'gv')
877
+ $variableParameters = @('outvariable', 'errorvariable', 'warningvariable', 'informationvariable', 'pipelinevariable')
878
+ $variableAliases = @('ov', 'ev', 'wv', 'iv', 'pv')
879
+ $nameParameters = @('variable', 'bindingvariable') + $variableParameters
880
+
881
+ function NamesVariable($element) {
882
+ if (-not ($element -is [System.Management.Automation.Language.CommandParameterAst])) { return $false }
883
+ $p = $element.ParameterName.ToLowerInvariant()
884
+ return $variableAliases -contains $p -or ($p.Length -ge 2 -and @($nameParameters | Where-Object { $_.StartsWith($p) }).Count -gt 0)
885
+ }
886
+ $codeMethods = @('invoke', 'invokereturnasis', 'invokewithcontext', 'invokescript', 'newscriptblock', 'addscript', 'addcommand', 'begininvoke')
887
+ $dotnet = @('system.io.file', 'io.file', 'file', 'system.io.directory', 'io.directory', 'directory')
888
+ $fileCommands = @('get-item', 'gi', 'get-childitem', 'gci', 'ls', 'dir', 'new-item', 'ni')
889
+ $unstable = New-Object System.Collections.ArrayList
890
+ $anyVariable = $false
891
+ # An instance Delete() deletes a file only on an object a file command, -PassThru or a FileInfo made; COM objects have
892
+ # one too.
893
+ $fileObjects = ($source -match 'FileInfo|DirectoryInfo|FileSystemInfo') -or @($root.FindAll({
894
+ param($x)
895
+ ($x -is [System.Management.Automation.Language.CommandAst] -and $fileCommands -contains (NameOf $x)) -or
896
+ ($x -is [System.Management.Automation.Language.CommandParameterAst] -and $x.ParameterName.Length -ge 3 -and 'passthru'.StartsWith($x.ParameterName.ToLowerInvariant()))
897
+ }, $true)).Count -gt 0
898
+ $processStart = $source -match 'ProcessStartInfo|Diagnostics\.Process'
899
+
900
+ # A name handed over as text (-OutVariable d, Tee-Object -Variable d, Set-Item variable:d, 'TEMP' handed to
901
+ # SetEnvironmentVariable, '$d = 1' handed to iex) can write a variable without the parser seeing a variable there.
902
+ $named = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
903
+ foreach ($s in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true)) {
904
+ foreach ($m in [regex]::Matches($s.Value, '\$\{?(?:\w+:)?([\w?]+)')) { [void]$named.Add($m.Groups[1].Value) }
905
+ $parent = $s.Parent
906
+ $at = if ($parent -is [System.Management.Automation.Language.CommandAst]) { $parent.CommandElements.IndexOf($s) } else { -1 }
907
+ # A bare Variable: or Env: drive (New-Item -Path Env: -Name TEMP, Set-Location Variable:) writes names given elsewhere.
908
+ if ($s.Value -match '^(?:[\w.]+\\)?(?:variable|env|environment):{1,2}[\\/]?$') { $anyVariable = $true }
909
+ elseif ($s.Value -match '^(?:[\w.]+\\)?(?:variable|env|environment):{1,2}[\\/]?(.+)$') { [void]$named.Add($Matches[1]) }
910
+ 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$')) {
911
+ [void]$named.Add(($s.Value -replace '^\+', '' -replace '^(?:script|global|local|private):', ''))
912
+ }
913
+ }
914
+ foreach ($d in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.DataStatementAst] }, $true)) { if ($d.Variable) { [void]$named.Add($d.Variable) } }
915
+ foreach ($v in $root.FindAll({ param($x) $x -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)) {
916
+ $path = $v.VariablePath.UserPath
917
+ if ($v.Parent -is [System.Management.Automation.Language.ParameterAst] -or $named.Contains(($path -split ':')[-1])) { [void]$unstable.Add($path) }
918
+ }
919
+
920
+ $items = New-Object System.Collections.ArrayList
921
+ $found = $root.FindAll({
922
+ param($a)
923
+ $a -is [System.Management.Automation.Language.CommandAst] -or
924
+ $a -is [System.Management.Automation.Language.AssignmentStatementAst] -or
925
+ $a -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -or
926
+ $a -is [System.Management.Automation.Language.UnaryExpressionAst] -or
927
+ $a -is [System.Management.Automation.Language.ForEachStatementAst] -or
928
+ $a -is [System.Management.Automation.Language.ConvertExpressionAst] -or
929
+ $a -is [System.Management.Automation.Language.CommandParameterAst]
930
+ }, $true)
931
+ foreach ($a in $found) {
932
+ if ($a -is [System.Management.Automation.Language.AssignmentStatementAst]) {
933
+ $left = $a.Left
934
+ 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)) {
935
+ $right = if ($a.Right -is [System.Management.Automation.Language.CommandExpressionAst]) { Value $a.Right.Expression } else { @{ dynamic = $a.Right.Extent.Text } }
936
+ [void]$items.Add(@{ kind = 'assign'; name = $left.VariablePath.UserPath; value = $right })
937
+ } else {
938
+ foreach ($v in $left.FindAll({ param($x) $x -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)) { [void]$unstable.Add($v.VariablePath.UserPath) }
939
+ }
940
+ continue
941
+ }
942
+ if ($a -is [System.Management.Automation.Language.UnaryExpressionAst]) {
943
+ 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) }
944
+ continue
945
+ }
946
+ if ($a -is [System.Management.Automation.Language.ForEachStatementAst]) {
947
+ [void]$unstable.Add($a.Variable.VariablePath.UserPath)
948
+ continue
949
+ }
950
+ if ($a -is [System.Management.Automation.Language.ConvertExpressionAst]) {
951
+ if ($a.Type.TypeName.Name -eq 'ref' -and $a.Child -is [System.Management.Automation.Language.VariableExpressionAst]) { [void]$unstable.Add($a.Child.VariablePath.UserPath) }
952
+ continue
953
+ }
954
+ if ($a -is [System.Management.Automation.Language.CommandParameterAst]) {
955
+ # PowerShell takes any unambiguous start of a parameter name, so -OutVar and -ErrorVar count too.
956
+ $p = $a.ParameterName.ToLowerInvariant()
957
+ if ($variableAliases -contains $p -or ($p.Length -ge 2 -and @($variableParameters | Where-Object { $_.StartsWith($p) }).Count -gt 0)) { $anyVariable = $true }
958
+ continue
959
+ }
960
+ if ($a -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) {
961
+ $member = if ($a.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $a.Member.Value.ToLowerInvariant() } else { $null }
962
+ $type = if ($a.Expression -is [System.Management.Automation.Language.TypeExpressionAst]) { $a.Expression.TypeName.FullName.ToLowerInvariant() } else { $null }
963
+ if ($a.Static -and $member -eq 'delete' -and $dotnet -contains $type) {
964
+ $first = if ($a.Arguments -and $a.Arguments.Count -gt 0) { Value $a.Arguments[0] } else { @{ dynamic = $a.Extent.Text } }
965
+ [void]$items.Add(@{ kind = 'delete'; path = $first; literalPath = $null; literal = $true; dotnet = $true; whatIf = $false; bindError = $false; inPipeline = $false; ordered = (InOrder $a) })
966
+ } elseif ($null -eq $member -or ($a.Static -and $null -eq $type) -or $codeMethods -contains $member -or
967
+ ($member -eq 'create' -and $type -match '(^|\.)(scriptblock|powershell)$') -or
968
+ ($member -eq 'start' -and ($type -match '(^|\.)process$' -or ($null -eq $type -and $processStart))) -or
969
+ ($member.StartsWith('delete') -and ($null -ne $type -or $fileObjects))) {
970
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
971
+ }
972
+ continue
973
+ }
974
+ if ($a.CommandElements[0] -is [System.Management.Automation.Language.ScriptBlockExpressionAst]) { continue }
975
+ $name = NameOf $a
976
+ if ($null -eq $name) {
977
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
978
+ continue
979
+ }
980
+ $pipeline = $a.Parent
981
+ $inPipeline = ($pipeline -is [System.Management.Automation.Language.PipelineAst]) -and ($pipeline.PipelineElements.IndexOf($a) -gt 0)
982
+ if ($deletes -contains $name) {
983
+ $b = Bound $a
984
+ [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) })
985
+ } elseif ($name -eq 'cd..' -or $name -eq 'cd\') {
986
+ [void]$items.Add(@{ kind = 'cd'; path = @{ const = $name.Substring(2) }; literal = $true; inBlock = -not (TopLevel $a) })
987
+ } elseif ($moves -contains $name) {
988
+ $b = Bound $a
989
+ $to = if ($null -ne $b.literalPath) { $b.literalPath } else { $b.path }
990
+ [void]$items.Add(@{ kind = 'cd'; path = $to; literal = ($null -ne $b.literalPath); inBlock = -not (TopLevel $a) })
991
+ } elseif ($pops -contains $name) {
992
+ [void]$items.Add(@{ kind = 'pop' })
993
+ } elseif ($shells -contains $name) {
994
+ $elements = @($a.CommandElements | Select-Object -Skip 1 | ForEach-Object { Value $_ })
995
+ [void]$items.Add(@{ kind = 'shell'; name = $name; args = $elements; raw = $a.Extent.Text; inPipeline = $inPipeline; ordered = (InOrder $a) })
996
+ } elseif ($dynamics -contains $name) {
997
+ if ($inPipeline) {
998
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
999
+ } else {
1000
+ $values = New-Object System.Collections.ArrayList
1001
+ foreach ($e in @($a.CommandElements | Select-Object -Skip 1)) {
1002
+ if (Constant $e) { continue }
1003
+ [void]$values.Add((Value $(if ($e -is [System.Management.Automation.Language.CommandParameterAst]) { $e.Argument } else { $e })))
1004
+ }
1005
+ [void]$items.Add(@{ kind = 'dynamic'; text = $a.Extent.Text; args = $values.ToArray() })
1006
+ }
1007
+ } elseif ($members -contains $name) {
1008
+ $m = (Bound $a).member
1009
+ if ($null -ne $m) {
1010
+ $v = $m.ConstantValue
1011
+ if ($null -eq $v -or ([string]$v) -match '^delete' -or $codeMethods -contains ([string]$v).ToLowerInvariant()) { [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() }) }
1012
+ }
1013
+ } elseif ($drives -contains $name) {
1014
+ # A drive made here can carry a one-letter name that reads like a real drive.
1015
+ [void]$items.Add(@{ kind = 'dynamic'; text = $null; args = @() })
1016
+ } elseif ($variableCommands -contains $name) {
1017
+ $b = Bound $a
1018
+ $names = if ($null -ne $b.name) { @($b.name.ConstantValue) } else { @() }
1019
+ if ($names.Count -eq 0 -or @($names | Where-Object { $null -eq $_ -or [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters([string]$_) }).Count -gt 0) { $anyVariable = $true }
1020
+ else { foreach ($n in $names) { [void]$unstable.Add([string]$n) } }
1021
+ }
1022
+ }
1023
+ $result = @{ errors = ($errors.Count -gt 0); items = $items.ToArray(); unstable = $unstable.ToArray(); anyVariable = $anyVariable }
1024
+ [Console]::Out.Write((ConvertTo-Json -InputObject $result -Depth 12 -Compress))
1025
+ `;
1026
+ var LOADER = String.raw`
1027
+ $utf8 = New-Object System.Text.UTF8Encoding $false
1028
+ [Console]::InputEncoding = $utf8
1029
+ [Console]::OutputEncoding = $utf8
1030
+ $all = [Console]::In.ReadToEnd()
1031
+ $cut = $all.IndexOf([char]0)
1032
+ & ([scriptblock]::Create($all.Substring(0, $cut))) $all.Substring($cut + 1)
1033
+ `;
1034
+ function powerShellExe(env) {
1035
+ for (const dir of (env.PATH ?? env.Path ?? "").split(path7.delimiter)) {
1036
+ if (dir && fs5.existsSync(path7.join(dir, "pwsh.exe"))) return path7.join(dir, "pwsh.exe");
1037
+ }
1038
+ return "powershell.exe";
1039
+ }
1040
+ function realPowerShell(env, timeoutMs = PS_PARSE_TIMEOUT_MS) {
1041
+ const encoded = Buffer.from(LOADER, "utf16le").toString("base64");
1042
+ return (source) => new Promise((resolve) => {
1043
+ let out = "";
1044
+ let done = false;
1045
+ const finish = (value) => {
1046
+ if (done) return;
1047
+ done = true;
1048
+ clearTimeout(timer);
1049
+ resolve(value);
1050
+ };
1051
+ const child = spawn(powerShellExe(env), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] });
1052
+ const timer = setTimeout(() => {
1053
+ child.kill();
1054
+ finish(null);
1055
+ }, timeoutMs);
1056
+ child.on("error", () => finish(null));
1057
+ child.on("close", (code) => finish(code === 0 ? out : null));
1058
+ child.stdout.setEncoding("utf8");
1059
+ child.stdout.on("data", (chunk) => {
1060
+ out += chunk;
1061
+ });
1062
+ child.stdin.on("error", () => {
1063
+ });
1064
+ child.stdin.end(`${READER}\0${source}`, "utf8");
1065
+ });
1066
+ }
1067
+ var list = (value) => Array.isArray(value) ? value : value == null ? [] : [value];
1068
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1069
+ function partOf(raw) {
1070
+ if (!isRecord2(raw)) throw new Error("bad value");
1071
+ if (typeof raw.const === "string") return { const: raw.const };
1072
+ if (typeof raw.variable === "string") return { variable: raw.variable };
1073
+ throw new Error("bad value");
1074
+ }
1075
+ function valueOf(raw) {
1076
+ if (!isRecord2(raw)) throw new Error("bad value");
1077
+ if ("expand" in raw) {
1078
+ if (!Array.isArray(raw.expand) || typeof raw.text !== "string") throw new Error("bad value");
1079
+ return { expand: raw.expand.map(partOf), text: raw.text };
1080
+ }
1081
+ if ("array" in raw) return { array: list(raw.array).map(valueOf) };
1082
+ if ("dynamic" in raw) {
1083
+ if (typeof raw.dynamic !== "string") throw new Error("bad value");
1084
+ return { dynamic: raw.dynamic };
1085
+ }
1086
+ return partOf(raw);
1087
+ }
1088
+ var optionalValue = (raw) => raw == null ? null : valueOf(raw);
1089
+ var flag = (raw) => {
1090
+ if (typeof raw !== "boolean") throw new Error("bad flag");
1091
+ return raw;
1092
+ };
1093
+ function itemOf(raw) {
1094
+ if (!isRecord2(raw) || typeof raw.kind !== "string") throw new Error("bad item");
1095
+ switch (raw.kind) {
1096
+ case "delete":
1097
+ 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) };
1098
+ case "cd":
1099
+ return { kind: "cd", path: optionalValue(raw.path), literal: flag(raw.literal), inBlock: flag(raw.inBlock) };
1100
+ case "pop":
1101
+ return { kind: "pop" };
1102
+ case "assign":
1103
+ if (typeof raw.name !== "string") throw new Error("bad item");
1104
+ return { kind: "assign", name: raw.name, value: valueOf(raw.value) };
1105
+ case "shell":
1106
+ if (typeof raw.name !== "string" || typeof raw.raw !== "string") throw new Error("bad item");
1107
+ return { kind: "shell", name: raw.name, args: list(raw.args).map(valueOf), raw: raw.raw, inPipeline: flag(raw.inPipeline), ordered: flag(raw.ordered) };
1108
+ case "dynamic":
1109
+ if (raw.text != null && typeof raw.text !== "string" || !Array.isArray(raw.args)) throw new Error("bad item");
1110
+ return { kind: "dynamic", text: typeof raw.text === "string" ? raw.text : null, args: raw.args.map(valueOf) };
1111
+ default:
1112
+ throw new Error("bad item");
1113
+ }
1114
+ }
1115
+ function parse(stdout) {
1116
+ const parsed = JSON.parse(stdout);
1117
+ if (!isRecord2(parsed) || parsed.errors !== false || !Array.isArray(parsed.items) || !Array.isArray(parsed.unstable) || typeof parsed.anyVariable !== "boolean") throw new Error("bad answer");
1118
+ const unstable = parsed.unstable.map((name2) => {
1119
+ if (typeof name2 !== "string") throw new Error("bad answer");
1120
+ return baseName(name2);
1121
+ });
1122
+ return { items: parsed.items.map(itemOf), unstable: new Set(unstable), anyVariable: parsed.anyVariable };
1123
+ }
1124
+ var baseName = (name2) => name2.toLowerCase().replace(/^(?:script|local|global|private|using|variable):/, "");
1125
+ function fileSystemPath(value) {
1126
+ const qualified = /^(?:[\w.]+\\)?(\w+)::/.exec(value);
1127
+ if (!qualified) return value;
1128
+ return qualified[1].toLowerCase() === "filesystem" ? value.slice(qualified[0].length) : null;
1129
+ }
1130
+ function classify3(value, literal, cwd, ctx) {
1131
+ const text = fileSystemPath(value);
1132
+ if (text === null) return {};
1133
+ const drive = /^([A-Za-z][A-Za-z0-9_]+):/.exec(text);
1134
+ if (drive) return OTHER_DRIVES.has(drive[1].toLowerCase()) ? {} : { unresolvable: `a path on a PowerShell drive that cannot be worked out (${value})` };
1135
+ let base = text;
1136
+ let pattern = null;
1137
+ if (!literal && /[*?[]/.test(text)) {
1138
+ const split = splitWindowsPattern(text, /[*?[]/);
1139
+ if (split === null) return { unresolvable: `a wildcard in a folder name (${value})` };
1140
+ base = split.dir;
1141
+ pattern = split.pattern;
1142
+ }
1143
+ const resolved = resolveWindowsPath(base, cwd, ctx.home);
1144
+ if ("unresolvable" in resolved) return { unresolvable: resolved.unresolvable };
1145
+ if ("unc" in resolved) return { real: { shown: value, path: null } };
1146
+ return { real: judgePath(resolved.path, pattern, ctx) ?? void 0 };
1147
+ }
1148
+ function folderOf(value, literal, cwd, home) {
1149
+ const text = fileSystemPath(value);
1150
+ if (text === null || text === "-" || text === "+" || /^[A-Za-z][A-Za-z0-9_]+:/.test(text) || !literal && /[*?[]/.test(text)) return null;
1151
+ const resolved = resolveWindowsPath(text, cwd, home);
1152
+ return "path" in resolved ? resolved.path : null;
1153
+ }
1154
+ async function readPowerShell(source, ctx, run) {
1155
+ const out = emptyRead();
1156
+ if (!POWERSHELL_WORDS.test(source)) return out;
1157
+ const excerpt = source.slice(0, EXCERPT_CHARS2);
1158
+ const failed = () => ({ ...emptyRead(), failed: [excerpt] });
1159
+ try {
1160
+ const stdout = await run(source);
1161
+ if (stdout === null) return failed();
1162
+ const { items, unstable, anyVariable } = parse(stdout);
1163
+ const assigned = /* @__PURE__ */ new Map();
1164
+ for (const item of items) if (item.kind === "assign") assigned.set(baseName(item.name), (assigned.get(baseName(item.name)) ?? 0) + 1);
1165
+ const moves = items.some((item) => item.kind === "cd" || item.kind === "pop");
1166
+ const processFolder = /CurrentDirectory/i.test(source) ? null : ctx.cwd;
1167
+ const vars = /* @__PURE__ */ new Map();
1168
+ for (const name2 of KNOWN_ENV2) {
1169
+ const value = ctx.env[name2];
1170
+ if (value) vars.set(`env:${name2.toLowerCase()}`, value);
1171
+ }
1172
+ vars.set("home", ctx.home);
1173
+ let cwd = ctx.cwd;
1174
+ const variable = (name2, here) => {
1175
+ const key = baseName(name2);
1176
+ if (anyVariable || unstable.has(key)) return null;
1177
+ if (key === "pwd") return assigned.has("pwd") ? null : here;
1178
+ return vars.get(key) ?? null;
1179
+ };
1180
+ const textOf = (value, here = cwd) => {
1181
+ if (!value) return null;
1182
+ if ("const" in value) return value.const;
1183
+ if ("variable" in value) return variable(value.variable, here);
1184
+ if ("expand" in value) {
1185
+ let text = "";
1186
+ for (const part of value.expand) {
1187
+ const piece = "const" in part ? part.const : variable(part.variable, here);
1188
+ if (piece === null) return null;
1189
+ text += piece;
1190
+ }
1191
+ return text;
1192
+ }
1193
+ return null;
1194
+ };
1195
+ const deny = (why) => ({ ...out, deny: denyReason("powershell", why) });
1196
+ for (const item of items) {
1197
+ if (item.kind === "assign") {
1198
+ const key = baseName(item.name);
1199
+ vars.set(key, assigned.get(key) === 1 ? textOf(item.value) : null);
1200
+ } else if (item.kind === "pop") {
1201
+ cwd = null;
1202
+ } else if (item.kind === "cd") {
1203
+ const to = item.inBlock ? null : textOf(item.path);
1204
+ cwd = to === null ? null : folderOf(to, item.literal, cwd, ctx.home);
1205
+ } else if (item.kind === "shell") {
1206
+ if (item.inPipeline && DELETE_WORD.test(source)) return deny(`pipeline input feeding a shell (${item.raw})`);
1207
+ const here = item.ordered || !moves ? cwd : null;
1208
+ const args = item.args.map((a) => textOf(a, here)).map((t) => t !== null && item.name !== "cmd" && t.includes('"') ? null : t);
1209
+ out.shells.push({ name: item.name, args, cwd: here, raw: item.raw });
1210
+ } else if (item.kind === "dynamic") {
1211
+ const values = item.text === null ? null : item.args.map((a) => textOf(a));
1212
+ const code = values === null || values.includes(null) ? source : [item.text, ...values].join(" ");
1213
+ if (DELETE_WORD.test(code) && !out.failed.length) out.failed.push(excerpt);
1214
+ } else {
1215
+ if (item.whatIf) continue;
1216
+ if (item.bindError) {
1217
+ if (!out.failed.length) out.failed.push(excerpt);
1218
+ continue;
1219
+ }
1220
+ if (item.inPipeline) return deny("pipeline input feeding Remove-Item");
1221
+ const here = item.ordered || !moves ? cwd : null;
1222
+ const values = list(item.literalPath ?? item.path).flatMap((v) => "array" in v ? v.array : [v]);
1223
+ for (const v of values) {
1224
+ if ("dynamic" in v || "array" in v) return deny(`an expression that cannot be worked out (${"dynamic" in v ? v.dynamic : "a nested list"})`);
1225
+ const text = textOf(v, here);
1226
+ if (text === null) return deny(`a variable that cannot be worked out (${"variable" in v ? `$${v.variable}` : "expand" in v ? v.text : ""})`);
1227
+ const c = classify3(text, item.literal, item.dotnet ? processFolder : here, ctx);
1228
+ if (c.unresolvable) return deny(c.unresolvable);
1229
+ if (c.real) out.targets.push(c.real);
1230
+ }
1231
+ }
1232
+ }
1233
+ return out;
1234
+ } catch {
1235
+ return failed();
1236
+ }
1237
+ }
1238
+
1239
+ // src/rm/dispatch.ts
1240
+ var MAX_NESTING = 3;
1241
+ var POWERSHELL_VALUE_PARAMS = [["executionpolicy", 2], ["windowstyle", 2], ["outputformat", 2], ["inputformat", 2], ["workingdirectory", 2], ["configurationname", 6], ["settingsfile", 2], ["version", 2], ["psconsolefile", 2], ["custompipename", 2]];
1242
+ var POWERSHELL_VALUE_ALIASES = ["ep", "ex", "w", "o", "of", "if", "wd", "v"];
1243
+ var CMD_RUN = /^\/{1,2}(?:[a-z]+\/)*[ck](.*)$/is;
1244
+ var paramName = (arg) => arg.replace(/^(?:--|-|\/)/, "").toLowerCase();
1245
+ var isParam = (arg) => /^(?:--|-|\/)[A-Za-z]/.test(arg);
1246
+ var startsWord = (name2, full, min) => name2.length >= min && full.startsWith(name2);
1247
+ function cmdLine(args) {
1248
+ const line = args.map((a) => /\s/.test(a) && !/^".*"$/.test(a) ? `"${a}"` : a).join(" ");
1249
+ const last = line.lastIndexOf('"');
1250
+ return line.startsWith('"') && last > 0 ? `${line.slice(1, last)}${line.slice(last + 1)}` : line;
1251
+ }
1252
+ function bashScript(args) {
1253
+ let hasC = false;
1254
+ let i = 0;
1255
+ for (; i < args.length; i++) {
1256
+ const a = args[i];
1257
+ if (a === null) return { shell: "bash", script: null };
1258
+ if (a === "--") {
1259
+ i++;
1260
+ break;
1261
+ }
1262
+ if (!/^[-+]/.test(a)) break;
1263
+ if (/^-[A-Za-z]*c[A-Za-z]*$/.test(a)) hasC = true;
1264
+ if (/^[-+][oO]$/.test(a) || a === "--rcfile" || a === "--init-file") i++;
1265
+ }
1266
+ if (!hasC || i >= args.length) return null;
1267
+ return { shell: "bash", script: args[i] };
1268
+ }
1269
+ function powerShellScript(name2, args) {
1270
+ let moved = false;
1271
+ const found = (script) => ({ shell: "powershell", script, ...moved ? { moved } : {} });
1272
+ for (let i = 0; i < args.length; i++) {
1273
+ const a = args[i];
1274
+ if (a === null) return found(null);
1275
+ if (a === "-") return null;
1276
+ if (!isParam(a)) {
1277
+ if (name2 === "pwsh") return null;
1278
+ const rest = args.slice(i);
1279
+ return found(rest.includes(null) ? null : rest.join(" "));
1280
+ }
1281
+ const p = paramName(a);
1282
+ if (p === "e" || p === "ec" || startsWord(p, "encodedcommand", 2)) {
1283
+ const encoded = args[i + 1];
1284
+ if (encoded === void 0) return null;
1285
+ return found(encoded === null ? null : Buffer.from(encoded, "base64").toString("utf16le"));
1286
+ }
1287
+ if (p === "cwa" || startsWord(p, "command", 1) || startsWord(p, "commandwithargs", 8)) {
1288
+ const rest = args.slice(i + 1);
1289
+ if (!rest.length || rest[0] === "-") return null;
1290
+ return found(rest.includes(null) ? null : rest.join(" "));
1291
+ }
1292
+ if (p === "f" || startsWord(p, "file", 2)) return null;
1293
+ if (POWERSHELL_VALUE_ALIASES.includes(p) || POWERSHELL_VALUE_PARAMS.some(([full, min]) => startsWord(p, full, min))) {
1294
+ if (p === "wd" || startsWord(p, "workingdirectory", 2)) moved = true;
1295
+ i++;
1296
+ }
1297
+ }
1298
+ return null;
1299
+ }
1300
+ function nestedScript({ name: name2, args }) {
1301
+ if (name2 === "sh" || name2 === "bash") return bashScript(args);
1302
+ if (name2 === "powershell" || name2 === "pwsh") return powerShellScript(name2, args);
1303
+ if (name2 === "cmd") {
1304
+ const at = args.findIndex((a) => a !== null && CMD_RUN.test(a));
1305
+ if (at === -1) return null;
1306
+ const glued = CMD_RUN.exec(args[at])[1];
1307
+ const rest = [...glued ? [glued] : [], ...args.slice(at + 1)];
1308
+ return rest.includes(null) ? { shell: "cmd", script: null } : { shell: "cmd", script: cmdLine(rest) };
1309
+ }
1310
+ return null;
1311
+ }
1312
+ function readsStdin({ name: name2, args }) {
1313
+ if (args.includes(null)) return false;
1314
+ const given = args;
1315
+ if (name2 === "sh" || name2 === "bash") {
1316
+ for (let i = 0; i < given.length; i++) {
1317
+ const a = given[i];
1318
+ if (a === "--") return i + 1 === given.length;
1319
+ if (!/^[-+]/.test(a)) return false;
1320
+ if (/^-[A-Za-z]*s[A-Za-z]*$/.test(a)) return true;
1321
+ if (/^[-+][oO]$/.test(a) || a === "--rcfile" || a === "--init-file") i++;
1322
+ }
1323
+ return true;
1324
+ }
1325
+ if (name2 === "powershell" || name2 === "pwsh") {
1326
+ for (let i = 0; i < given.length; i++) {
1327
+ const a = given[i];
1328
+ if (a === "-") return true;
1329
+ if (!isParam(a)) return false;
1330
+ const p = paramName(a);
1331
+ if (p === "cwa" || startsWord(p, "command", 1) || startsWord(p, "commandwithargs", 8)) return given[i + 1] === void 0 || given[i + 1] === "-";
1332
+ if (p === "f" || startsWord(p, "file", 2) || p === "e" || p === "ec" || startsWord(p, "encodedcommand", 2)) return false;
1333
+ if (POWERSHELL_VALUE_ALIASES.includes(p) || POWERSHELL_VALUE_PARAMS.some(([full, min]) => startsWord(p, full, min))) i++;
1334
+ }
1335
+ return true;
1336
+ }
1337
+ return name2 === "cmd" && !given.some((a) => CMD_RUN.test(a));
1338
+ }
1339
+ function readShell(shell, script, ctx, run) {
1340
+ if (shell === "bash") return readBash(script, ctx);
1341
+ if (shell === "cmd") return readCmd(script, ctx);
1342
+ return readPowerShell(script, ctx, run);
1343
+ }
1344
+ async function readNested(shell, script, ctx, run, depth) {
1345
+ const result = await readShell(shell, script, ctx, run);
1346
+ if (result.deny !== null) return result;
1347
+ const merged = { deny: null, targets: [...result.targets], failed: [...result.failed], shells: [] };
1348
+ for (const call of result.shells) {
1349
+ const nested = nestedScript(call);
1350
+ if (nested === null) {
1351
+ if ((call.piped || readsStdin(call)) && DELETE_WORDS.test(script)) return { ...merged, deny: denyReason(shell, `a shell reading its script from stdin (${call.raw})`) };
1352
+ continue;
1353
+ }
1354
+ if (nested.script === null || depth >= MAX_NESTING) {
1355
+ if (!DELETE_WORDS.test(call.raw) && !DELETE_WORDS.test(nested.script ?? "")) continue;
1356
+ 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})`;
1357
+ return { ...merged, deny: denyReason(shell, why) };
1358
+ }
1359
+ const inner = await readNested(nested.shell, nested.script, { ...ctx, cwd: nested.moved ? null : call.cwd }, run, depth + 1);
1360
+ if (inner.deny !== null) return { ...merged, deny: inner.deny };
1361
+ merged.targets.push(...inner.targets);
1362
+ merged.failed.push(...inner.failed);
1363
+ }
1364
+ return merged;
1365
+ }
1366
+ async function decideTool(tool, command, ctx, run) {
1367
+ return decisionOf(await readNested(tool === "Bash" ? "bash" : "powershell", command, ctx, run, 0));
1368
+ }
1369
+
509
1370
  // src/rm/facts.ts
510
- import fs6 from "fs";
511
- import path5 from "path";
1371
+ import fs7 from "fs";
1372
+ import path8 from "path";
512
1373
  import { spawnSync as spawnSync2 } from "child_process";
513
1374
 
514
1375
  // src/context/transcript.ts
515
- import fs5 from "fs";
1376
+ import fs6 from "fs";
516
1377
  var TAIL_START_BYTES = 1024 * 1024;
517
1378
  var TAIL_MAX_BYTES = 8 * 1024 * 1024;
518
- function isRecord2(value) {
1379
+ function isRecord3(value) {
519
1380
  return typeof value === "object" && value !== null && !Array.isArray(value);
520
1381
  }
521
1382
  function parseLines(text) {
@@ -524,7 +1385,7 @@ function parseLines(text) {
524
1385
  if (!line.trim()) continue;
525
1386
  try {
526
1387
  const value = JSON.parse(line);
527
- if (isRecord2(value)) entries.push(value);
1388
+ if (isRecord3(value)) entries.push(value);
528
1389
  } catch {
529
1390
  }
530
1391
  }
@@ -534,11 +1395,13 @@ function parseLines(text) {
534
1395
  // src/rm/facts.ts
535
1396
  var SESSION_LOG_MAX_BYTES = 32 * 1024 * 1024;
536
1397
  var CALLS_PER_TARGET = 3;
537
- var win2 = path5.win32;
1398
+ var win4 = path8.win32;
538
1399
  var DELETING = /\b(?:rm|rmdir|del|erase|rd|ri|Remove-Item|xargs)\b/i;
539
1400
  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) {
1401
+ var MOVES = ["cd", "pushd", "popd", "chdir", "set-location", "sl", "push-location", "pop-location"];
1402
+ var CHANGES_FOLDER = new RegExp(`(^|[\\s;&|(])(?:${MOVES.join("|")})(?=[\\s;&|)]|$)`, "i");
1403
+ var MOVING_TOOLS = /* @__PURE__ */ new Set(["EnterWorktree", "ExitWorktree"]);
1404
+ function isRecord4(value) {
542
1405
  return typeof value === "object" && value !== null && !Array.isArray(value);
543
1406
  }
544
1407
  function callOf(tool, input, result, cwd) {
@@ -550,7 +1413,7 @@ function callOf(tool, input, result, cwd) {
550
1413
  return { tool, file: text("file_path"), text: text("new_string"), kind: "change", cwd };
551
1414
  case "MultiEdit": {
552
1415
  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 };
1416
+ 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
1417
  }
555
1418
  case "NotebookEdit":
556
1419
  return { tool, file: text("notebook_path"), text: text("new_source"), kind: "change", cwd };
@@ -566,10 +1429,10 @@ function callOf(tool, input, result, cwd) {
566
1429
  function resultTypes(entries) {
567
1430
  const types = /* @__PURE__ */ new Map();
568
1431
  for (const entry of entries) {
569
- if (entry.type !== "user" || !isRecord3(entry.toolUseResult) || typeof entry.toolUseResult.type !== "string" || !isRecord3(entry.message)) continue;
1432
+ if (entry.type !== "user" || !isRecord4(entry.toolUseResult) || typeof entry.toolUseResult.type !== "string" || !isRecord4(entry.message)) continue;
570
1433
  const content = entry.message.content;
571
1434
  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);
1435
+ if (isRecord4(block) && typeof block.tool_use_id === "string") types.set(block.tool_use_id, entry.toolUseResult.type);
573
1436
  }
574
1437
  return types;
575
1438
  }
@@ -579,12 +1442,13 @@ function toolCalls(entries) {
579
1442
  let message;
580
1443
  let moved = false;
581
1444
  for (const entry of entries) {
582
- if (entry.type !== "assistant" || entry.isSidechain === true || !isRecord3(entry.message) || !Array.isArray(entry.message.content)) continue;
1445
+ if (entry.type !== "assistant" || entry.isSidechain === true || !isRecord4(entry.message) || !Array.isArray(entry.message.content)) continue;
583
1446
  if (entry.message.id === void 0 || entry.message.id !== message) moved = false;
584
1447
  message = entry.message.id;
585
1448
  const cwd = typeof entry.cwd === "string" ? entry.cwd : null;
586
1449
  for (const block of entry.message.content) {
587
- if (!isRecord3(block) || block.type !== "tool_use" || typeof block.name !== "string" || !isRecord3(block.input)) continue;
1450
+ if (!isRecord4(block) || block.type !== "tool_use" || typeof block.name !== "string" || !isRecord4(block.input)) continue;
1451
+ if (MOVING_TOOLS.has(block.name)) moved = true;
588
1452
  const call = callOf(block.name, block.input, typeof block.id === "string" ? results.get(block.id) : void 0, moved ? null : cwd);
589
1453
  if (!call) continue;
590
1454
  calls.push(call);
@@ -596,8 +1460,8 @@ function toolCalls(entries) {
596
1460
  function readSessionLog(file, maxBytes = SESSION_LOG_MAX_BYTES) {
597
1461
  let text;
598
1462
  try {
599
- if (fs6.statSync(file).size > maxBytes) return null;
600
- text = fs6.readFileSync(file, "utf8");
1463
+ if (fs7.statSync(file).size > maxBytes) return null;
1464
+ text = fs7.readFileSync(file, "utf8");
601
1465
  } catch {
602
1466
  return null;
603
1467
  }
@@ -611,8 +1475,8 @@ function normalized(text) {
611
1475
  var escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
612
1476
  function shellRefs(target, cwd) {
613
1477
  const refs = [`(?<![\\w.-])${escaped(normalized(target))}(?![\\w.-])`];
614
- const relative = cwd === null ? "" : win2.relative(cwd, target);
615
- if (relative && !relative.startsWith("..") && !win2.isAbsolute(relative)) {
1478
+ const relative = cwd === null ? "" : win4.relative(cwd, target);
1479
+ if (relative && !relative.startsWith("..") && !win4.isAbsolute(relative)) {
616
1480
  refs.push(`(?<![\\w.\\-/:])(?:\\./)?${escaped(normalized(relative))}(?![\\w.-])`);
617
1481
  }
618
1482
  return refs;
@@ -677,18 +1541,18 @@ var pathWord = (word) => word.replace(/\\/g, "/").replace(/^\/([A-Za-z])\//, "$1
677
1541
  function createsTarget(call, target) {
678
1542
  if (call.kind !== "shell") return call.kind === "create";
679
1543
  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;
1544
+ const relative = call.cwd === null ? "" : win4.relative(call.cwd, target);
1545
+ const short = relative && !relative.startsWith("..") && !win4.isAbsolute(relative) ? pathWord(relative) : null;
682
1546
  let moved = false;
683
1547
  const isTarget = (word) => word !== void 0 && (pathWord(word) === full || !moved && pathWord(word) === short);
684
1548
  for (const { words, writes } of shellCommands(stripHeredocs(call.text))) {
685
1549
  if (writes.some(isTarget)) return true;
686
1550
  const start = words.findIndex((w) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(w));
687
1551
  const args = start === -1 ? [] : words.slice(start);
688
- const name2 = win2.basename((args[0] ?? "").replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
1552
+ const name2 = win4.basename((args[0] ?? "").replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
689
1553
  if ((name2 === "touch" || name2 === "mkdir") && args.slice(1).some(isTarget)) return true;
690
1554
  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;
1555
+ if (MOVES.includes(name2)) moved = true;
692
1556
  }
693
1557
  return false;
694
1558
  }
@@ -712,8 +1576,9 @@ function gatherFacts(target, session, probe, maxFiles) {
712
1576
  function candidateKind(facts, startedAt) {
713
1577
  const fresh = (bornAt2) => bornAt2 >= startedAt;
714
1578
  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";
1579
+ const repo = facts.folder && (facts.listing === null || facts.listing.repo);
1580
+ if (!facts.tracked && !facts.existedBefore && facts.createdBy.length > 0 && fresh(facts.bornAt) && freshInside && !repo) return "session";
1581
+ if (facts.folder && facts.ignored && !repo) return "ignored";
717
1582
  return null;
718
1583
  }
719
1584
  var bornAt = (stat) => stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
@@ -725,7 +1590,7 @@ function git(args, extraEnv = {}) {
725
1590
  var realFactProbe = {
726
1591
  info(p) {
727
1592
  try {
728
- const stat = fs6.lstatSync(p);
1593
+ const stat = fs7.lstatSync(p);
729
1594
  return { folder: stat.isDirectory(), bornAt: bornAt(stat) };
730
1595
  } catch {
731
1596
  return null;
@@ -737,7 +1602,7 @@ var realFactProbe = {
737
1602
  let more = false;
738
1603
  let repo = false;
739
1604
  const walk = (folder, prefix) => {
740
- for (const item of fs6.readdirSync(folder, { withFileTypes: true })) {
1605
+ for (const item of fs7.readdirSync(folder, { withFileTypes: true })) {
741
1606
  if (more) return;
742
1607
  if (seen >= limit) {
743
1608
  more = true;
@@ -745,33 +1610,36 @@ var realFactProbe = {
745
1610
  }
746
1611
  seen += 1;
747
1612
  if (item.name === ".git") repo = true;
748
- const full = path5.join(folder, item.name);
1613
+ const full = path8.join(folder, item.name);
749
1614
  const name2 = prefix ? `${prefix}/${item.name}` : item.name;
750
1615
  if (item.isDirectory()) walk(full, name2);
751
- else entries.push({ name: name2, bornAt: bornAt(fs6.lstatSync(full)) });
1616
+ else entries.push({ name: name2, bornAt: bornAt(fs7.lstatSync(full)) });
752
1617
  }
753
1618
  };
754
1619
  try {
755
1620
  walk(dir, "");
1621
+ if (!repo) {
1622
+ repo = fs7.readdirSync(dir, { withFileTypes: true }).some((item) => item.name === ".git" || item.isDirectory() && fs7.existsSync(path8.join(dir, item.name, ".git")));
1623
+ }
756
1624
  return { entries, more, repo };
757
1625
  } catch {
758
1626
  return null;
759
1627
  }
760
1628
  },
761
1629
  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" });
1630
+ if (fs7.existsSync(path8.join(p, ".git"))) return true;
1631
+ const r = git(["-C", win4.dirname(p), "ls-files", "--", p], { GIT_ICASE_PATHSPECS: "1" });
764
1632
  if (r.status === 0) return r.stdout.trim() !== "";
765
1633
  return !(r.status === 128 && /not a git repository/i.test(r.stderr ?? ""));
766
1634
  },
767
1635
  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;
1636
+ if (fs7.existsSync(path8.join(p, ".git"))) return false;
1637
+ return git(["-C", win4.dirname(p), "check-ignore", "-q", "--", p]).status === 0;
770
1638
  }
771
1639
  };
772
1640
 
773
1641
  // src/rm/judge.ts
774
- import path6 from "path";
1642
+ import path9 from "path";
775
1643
  var CALL_EXCERPT_CHARS = 600;
776
1644
  var SAMPLE_FILES = 10;
777
1645
  var RM_QUESTIONS = {
@@ -830,7 +1698,7 @@ ${c.text}` : c.text).slice(0, CALL_EXCERPT_CHARS) }));
830
1698
  }
831
1699
  return { state, questions: { ok: RM_QUESTIONS[kind] } };
832
1700
  }
833
- var name = (item) => path6.win32.basename(item.path);
1701
+ var name = (item) => path9.win32.basename(item.path);
834
1702
  var prob = (p) => p.toFixed(2);
835
1703
  function lifts(items, threshold) {
836
1704
  return items.length > 0 && items.every((item) => item.p !== null && item.p >= threshold);
@@ -850,6 +1718,7 @@ function askNote(lang, items) {
850
1718
 
851
1719
  // src/rm/run.ts
852
1720
  var MAX_JEV_TARGETS = 5;
1721
+ var READ_BUDGET_MS = 9e3;
853
1722
  var FACTS_BUDGET_MS = 5e3;
854
1723
  var JEV_PATH_BUDGET_MS = 12e3;
855
1724
  var MIN_JEV_MS = 1e3;
@@ -892,10 +1761,14 @@ async function runRmHook(raw, deps) {
892
1761
  const input = JSON.parse(raw);
893
1762
  if (typeof input !== "object" || input === null) return null;
894
1763
  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)))];
1764
+ const tool = input.tool_name;
1765
+ if (tool !== "Bash" && tool !== "PowerShell" || typeof command !== "string") return null;
1766
+ const tmpdirs = [...new Set([deps.tmpdir, deps.env.TEMP, deps.env.TMP].filter((t) => Boolean(t)).map((t) => path10.win32.resolve(t)))];
897
1767
  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 });
1768
+ const ctx = { cwd, home: deps.home, tmpdirs, env: deps.env, probe: deps.probe };
1769
+ const powershell = deps.powershell ?? realPowerShell(deps.env);
1770
+ const read = (source) => now().getTime() - started >= READ_BUDGET_MS ? Promise.resolve(null) : powershell(source);
1771
+ const result = await decideTool(tool, command, ctx, read);
899
1772
  if (!result) return null;
900
1773
  if (result.decision !== "ask" || !result.targets || !config.rm.jev) return hookOutput(result);
901
1774
  const key = readJevKey(deps.env, config.keyFile);
@@ -918,6 +1791,7 @@ async function runRmHook(raw, deps) {
918
1791
  event: "pre_tool_use",
919
1792
  sessionId: typeof input.session_id === "string" ? input.session_id : null,
920
1793
  transcriptPath: input.transcript_path,
1794
+ tool,
921
1795
  command,
922
1796
  cwd,
923
1797
  decision: pass ? "pass" : "ask",