@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/README.md +27 -14
- package/dist/cli.js +15 -2
- package/dist/rm-hook.js +984 -110
- package/mod/.claude-plugin/plugin.json +1 -1
- package/package.json +2 -2
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
|
|
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:
|
|
239
|
-
if (/^[A-Za-z]:[\\/]/.test(v)) return { path:
|
|
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:
|
|
349
|
+
if (drive) return { path: win2.normalize(`${drive[1].toUpperCase()}:\\${drive[2] ?? ""}`) };
|
|
242
350
|
const tmp = /^\/tmp(?:\/(.*))?$/.exec(v);
|
|
243
|
-
if (tmp) return { path:
|
|
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:
|
|
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
|
|
254
|
-
|
|
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
|
|
264
|
-
if (
|
|
265
|
-
pattern =
|
|
266
|
-
w = { ...w, value:
|
|
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
|
-
|
|
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
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
443
|
+
import path10 from "path";
|
|
363
444
|
|
|
364
445
|
// src/config.ts
|
|
365
446
|
import fs2 from "fs";
|
|
366
|
-
import
|
|
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
|
|
408
|
-
var claudeDir = (home) =>
|
|
409
|
-
var dataDir = (home) =>
|
|
410
|
-
var configPath = (home) =>
|
|
411
|
-
var logDir = (home) =>
|
|
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
|
|
505
|
+
import path5 from "path";
|
|
425
506
|
function logFile(home, now) {
|
|
426
|
-
return
|
|
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(
|
|
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
|
|
511
|
-
import
|
|
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
|
|
1376
|
+
import fs6 from "fs";
|
|
516
1377
|
var TAIL_START_BYTES = 1024 * 1024;
|
|
517
1378
|
var TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
518
|
-
function
|
|
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 (
|
|
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
|
|
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
|
|
541
|
-
|
|
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) =>
|
|
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" || !
|
|
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 (
|
|
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 || !
|
|
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 (!
|
|
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 (
|
|
600
|
-
text =
|
|
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 ? "" :
|
|
615
|
-
if (relative && !relative.startsWith("..") && !
|
|
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 ? "" :
|
|
681
|
-
const short = relative && !relative.startsWith("..") && !
|
|
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 =
|
|
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
|
|
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
|
-
|
|
716
|
-
if (facts.
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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 (
|
|
763
|
-
const r = git(["-C",
|
|
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 (
|
|
769
|
-
return git(["-C",
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
896
|
-
|
|
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
|
|
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",
|