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