@nexrall/code-core 1.4.65 → 1.4.67
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 +2 -2
- package/dist/agent/agentTypes.d.ts +8 -4
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +20 -147
- package/dist/agent/askOnce.d.ts +12 -0
- package/dist/agent/askOnce.d.ts.map +1 -0
- package/dist/agent/askOnce.js +41 -0
- package/dist/agent/compaction.d.ts +244 -0
- package/dist/agent/compaction.d.ts.map +1 -0
- package/dist/agent/compaction.js +976 -0
- package/dist/agent/fileLocks.d.ts +34 -0
- package/dist/agent/fileLocks.d.ts.map +1 -0
- package/dist/agent/fileLocks.js +114 -0
- package/dist/agent/hooks.d.ts +324 -0
- package/dist/agent/hooks.d.ts.map +1 -0
- package/dist/agent/hooks.js +1228 -0
- package/dist/agent/iterationPolicy.d.ts +121 -0
- package/dist/agent/iterationPolicy.d.ts.map +1 -0
- package/dist/agent/iterationPolicy.js +297 -0
- package/dist/agent/lifecycleHost.d.ts +55 -0
- package/dist/agent/lifecycleHost.d.ts.map +1 -0
- package/dist/agent/lifecycleHost.js +294 -0
- package/dist/agent/loop.d.ts +11 -491
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +418 -3032
- package/dist/agent/memory.d.ts +11 -0
- package/dist/agent/memory.d.ts.map +1 -1
- package/dist/agent/memory.js +23 -3
- package/dist/agent/planMode.d.ts +14 -0
- package/dist/agent/planMode.d.ts.map +1 -1
- package/dist/agent/planMode.js +146 -17
- package/dist/agent/securityLint.js +2 -2
- package/dist/agent/sharedTasks.d.ts +7 -0
- package/dist/agent/sharedTasks.d.ts.map +1 -1
- package/dist/agent/sharedTasks.js +16 -0
- package/dist/agent/subAgentBudget.d.ts +65 -0
- package/dist/agent/subAgentBudget.d.ts.map +1 -0
- package/dist/agent/subAgentBudget.js +269 -0
- package/dist/agent/subTask.d.ts +6 -0
- package/dist/agent/subTask.d.ts.map +1 -0
- package/dist/agent/subTask.js +713 -0
- package/dist/agent/subTaskSupport.d.ts +156 -0
- package/dist/agent/subTaskSupport.d.ts.map +1 -0
- package/dist/agent/subTaskSupport.js +409 -0
- package/dist/agent/toolDescriptions.d.ts +3 -0
- package/dist/agent/toolDescriptions.d.ts.map +1 -0
- package/dist/agent/toolDescriptions.js +116 -0
- package/dist/api/client.d.ts +1 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/mcp/client.d.ts +104 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +136 -2
- package/dist/mcp/httpClient.d.ts +17 -1
- package/dist/mcp/httpClient.d.ts.map +1 -1
- package/dist/mcp/httpClient.js +120 -19
- package/dist/mcp/manager.d.ts +77 -2
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +275 -9
- package/dist/mcp/sseClient.d.ts +7 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +45 -1
- package/dist/mcp/stats.d.ts +41 -0
- package/dist/mcp/stats.d.ts.map +1 -0
- package/dist/mcp/stats.js +108 -0
- package/dist/permissions/destructive.d.ts +11 -7
- package/dist/permissions/destructive.d.ts.map +1 -1
- package/dist/permissions/destructive.js +59 -8
- package/dist/permissions/destructiveTokens.d.ts +34 -0
- package/dist/permissions/destructiveTokens.d.ts.map +1 -0
- package/dist/permissions/destructiveTokens.js +475 -0
- package/dist/permissions/modePolicy.d.ts +12 -8
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +19 -12
- package/dist/permissions/rules.d.ts +4 -1
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +29 -0
- package/dist/types.d.ts +47 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -7
|
@@ -24,18 +24,22 @@ export interface DestructiveMatch {
|
|
|
24
24
|
category: string;
|
|
25
25
|
/** Short human-readable reason shown in the confirmation prompt. */
|
|
26
26
|
reason: string;
|
|
27
|
+
/** Contained harm — `auto` mode may skip the confirmation (see TokenMatch.lowRisk). */
|
|
28
|
+
lowRisk?: boolean;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* Classify a bash command. Returns the FIRST matching destructive category, or
|
|
30
32
|
* null when the command is not recognised as destructive.
|
|
31
33
|
*
|
|
32
|
-
* Two
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* Two layers:
|
|
35
|
+
* 1. Regex rules (above). RAW_RULES run on the heredoc-stripped but quote-PRESERVING
|
|
36
|
+
* string (real SQL lives inside quotes) and are gated on a DB-client token;
|
|
37
|
+
* STRIPPED_RULES run on the fully stripped string so mentions in commit messages /
|
|
38
|
+
* echoes / heredoc'd docs never trip them.
|
|
39
|
+
* 2. A token pass (destructiveTokens.ts) that splits the command like a shell, peels
|
|
40
|
+
* wrappers (sudo/env/xargs/bash -c/ssh/docker exec …) and judges the real command.
|
|
41
|
+
* It catches what a regex cannot: `git -C x reset --hard`, `git push -fu`,
|
|
42
|
+
* `git push origin +main`, `rm -rf src`, `find . -delete`, `> file`.
|
|
39
43
|
*/
|
|
40
44
|
export declare function classifyDestructiveCommand(command: string): DestructiveMatch | null;
|
|
41
45
|
/** True when the given tool + input represents a destructive bash command. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"destructive.d.ts","sourceRoot":"","sources":["../../src/permissions/destructive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;
|
|
1
|
+
{"version":3,"file":"destructive.d.ts","sourceRoot":"","sources":["../../src/permissions/destructive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAiQD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAGnF;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,GAAG,IAAI,CAQvG"}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.classifyDestructiveCommand = classifyDestructiveCommand;
|
|
25
25
|
exports.isDestructiveBash = isDestructiveBash;
|
|
26
|
+
const destructiveTokens_1 = require("./destructiveTokens");
|
|
26
27
|
// ── Data-section stripping (naive; feeds a heuristic, not an executor) ────────
|
|
27
28
|
function stripHeredocs(command) {
|
|
28
29
|
return command.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\2(?=\s|;|&|$)/g, ' ');
|
|
@@ -66,6 +67,16 @@ const RAW_RULES = [
|
|
|
66
67
|
reason: 'drops a Postgres/MySQL role or user',
|
|
67
68
|
re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bdrop\s+(?:role|user)\b`, 'i'),
|
|
68
69
|
},
|
|
70
|
+
{
|
|
71
|
+
category: 'database',
|
|
72
|
+
reason: 'UPDATE without a WHERE clause (rewrites every row)',
|
|
73
|
+
re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bupdate\s+[\w.` + '`' + String.raw `"]+\s+set\b(?![\s\S]*\bwhere\b)`, 'i'),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
category: 'database',
|
|
77
|
+
reason: 'DELETE with a WHERE clause that is always true (deletes every row)',
|
|
78
|
+
re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bdelete\s+from\b[\s\S]*?\bwhere\s+(?:1\s*=\s*1|true)\b`, 'i'),
|
|
79
|
+
},
|
|
69
80
|
{
|
|
70
81
|
category: 'database',
|
|
71
82
|
reason: 'drops a MongoDB database or collection',
|
|
@@ -106,11 +117,6 @@ const STRIPPED_RULES = [
|
|
|
106
117
|
reason: 'deletes untracked files (git clean)',
|
|
107
118
|
re: /\bgit\s+clean\s+-[a-z]*f/i,
|
|
108
119
|
},
|
|
109
|
-
{
|
|
110
|
-
category: 'git-history',
|
|
111
|
-
reason: 'force-deletes a git branch',
|
|
112
|
-
re: /\bgit\s+branch\s+-[a-z]*D/i,
|
|
113
|
-
},
|
|
114
120
|
{
|
|
115
121
|
category: 'git-history',
|
|
116
122
|
reason: 'rewrites git history (filter-branch/filter-repo)',
|
|
@@ -213,9 +219,7 @@ const STRIPPED_RULES = [
|
|
|
213
219
|
* mentions inside commit messages / echoes / grep patterns / heredoc'd docs
|
|
214
220
|
* never trip the bare-command rules (git/infra/cloud/container/publish).
|
|
215
221
|
*/
|
|
216
|
-
function
|
|
217
|
-
if (!command || typeof command !== 'string')
|
|
218
|
-
return null;
|
|
222
|
+
function classifyRegex(command) {
|
|
219
223
|
const raw = stripHeredocs(command);
|
|
220
224
|
for (const rule of RAW_RULES) {
|
|
221
225
|
if (rule.re.test(raw)) {
|
|
@@ -230,6 +234,53 @@ function classifyDestructiveCommand(command) {
|
|
|
230
234
|
}
|
|
231
235
|
return null;
|
|
232
236
|
}
|
|
237
|
+
/** Only the quote-preserving SQL rules — used for SQL a wrapper (ssh/docker exec/env) hid from CMD_POS. */
|
|
238
|
+
function classifyRawSql(command) {
|
|
239
|
+
const raw = stripHeredocs(command);
|
|
240
|
+
for (const rule of RAW_RULES) {
|
|
241
|
+
if (rule.re.test(raw))
|
|
242
|
+
return { category: rule.category, reason: rule.reason };
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const MAX_DEPTH = 4;
|
|
247
|
+
function classify(command, depth) {
|
|
248
|
+
if (depth > MAX_DEPTH)
|
|
249
|
+
return null;
|
|
250
|
+
const viaRegex = classifyRegex(command);
|
|
251
|
+
if (viaRegex)
|
|
252
|
+
return viaRegex;
|
|
253
|
+
// Token pass: understands option order, bundled flags, wrappers and `bash -c "…"`.
|
|
254
|
+
// A low-risk hit (scratch-file truncation) must not mask a real one later in the chain.
|
|
255
|
+
let soft = null;
|
|
256
|
+
for (const tokens of (0, destructiveTokens_1.tokenize)(stripHeredocs(command))) {
|
|
257
|
+
const m = (0, destructiveTokens_1.classifyTokens)(tokens, (inner) => classify(inner, depth + 1), classifyRawSql);
|
|
258
|
+
if (m && !m.lowRisk)
|
|
259
|
+
return m;
|
|
260
|
+
if (m && !soft)
|
|
261
|
+
soft = m;
|
|
262
|
+
}
|
|
263
|
+
return soft;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Classify a bash command. Returns the FIRST matching destructive category, or
|
|
267
|
+
* null when the command is not recognised as destructive.
|
|
268
|
+
*
|
|
269
|
+
* Two layers:
|
|
270
|
+
* 1. Regex rules (above). RAW_RULES run on the heredoc-stripped but quote-PRESERVING
|
|
271
|
+
* string (real SQL lives inside quotes) and are gated on a DB-client token;
|
|
272
|
+
* STRIPPED_RULES run on the fully stripped string so mentions in commit messages /
|
|
273
|
+
* echoes / heredoc'd docs never trip them.
|
|
274
|
+
* 2. A token pass (destructiveTokens.ts) that splits the command like a shell, peels
|
|
275
|
+
* wrappers (sudo/env/xargs/bash -c/ssh/docker exec …) and judges the real command.
|
|
276
|
+
* It catches what a regex cannot: `git -C x reset --hard`, `git push -fu`,
|
|
277
|
+
* `git push origin +main`, `rm -rf src`, `find . -delete`, `> file`.
|
|
278
|
+
*/
|
|
279
|
+
function classifyDestructiveCommand(command) {
|
|
280
|
+
if (!command || typeof command !== 'string')
|
|
281
|
+
return null;
|
|
282
|
+
return classify(command, 0);
|
|
283
|
+
}
|
|
233
284
|
/** True when the given tool + input represents a destructive bash command. */
|
|
234
285
|
function isDestructiveBash(tool, input) {
|
|
235
286
|
if (tool !== 'bash' && tool !== 'run_command' && tool !== 'execute_command')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-level half of the destructive-command classifier.
|
|
3
|
+
*
|
|
4
|
+
* destructive.ts started life as a pile of regexes run over the command STRING. That
|
|
5
|
+
* missed most real-world spellings, because a regex does not understand shell:
|
|
6
|
+
* - global options between the tool and its verb: git -C repo reset --hard
|
|
7
|
+
* - combined short flags: git push -fu origin main
|
|
8
|
+
* - refspec force syntax: git push origin +main
|
|
9
|
+
* - wrappers that hide the real command: sudo / env X=1 / xargs / nohup
|
|
10
|
+
* - interpreters that take the command as a string: bash -c "…", eval "…", ssh host "…"
|
|
11
|
+
* - containers that run it elsewhere: docker exec db psql … / kubectl exec -- …
|
|
12
|
+
* and it could not express "rm -rf node_modules is fine, rm -rf src is not".
|
|
13
|
+
*
|
|
14
|
+
* So this module splits the command into simple commands the way a shell would (honouring
|
|
15
|
+
* quotes, so a commit message that MENTIONS `git push --force` is one argument of
|
|
16
|
+
* `git commit`, never a command), peels wrappers off, and judges (binary, args).
|
|
17
|
+
*
|
|
18
|
+
* It is a heuristic, not a shell: no variable expansion, no aliases, no functions. When it
|
|
19
|
+
* cannot tell (an `rm -rf $DIR`), it errs toward asking — the cost of a false positive is
|
|
20
|
+
* one confirmation, the cost of a false negative is unrecoverable.
|
|
21
|
+
*/
|
|
22
|
+
export interface TokenMatch {
|
|
23
|
+
category: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
/**
|
|
26
|
+
* True when the harm is contained (e.g. truncating a scratch file under /tmp or a
|
|
27
|
+
* relative path). `auto` mode may run these without asking; every other mode still does.
|
|
28
|
+
*/
|
|
29
|
+
lowRisk?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Split `src` into simple commands, each a list of unquoted words. */
|
|
32
|
+
export declare function tokenize(src: string): string[][];
|
|
33
|
+
export declare function classifyTokens(tokens: string[], recurse: (command: string) => TokenMatch | null, rawSql: (command: string) => TokenMatch | null): TokenMatch | null;
|
|
34
|
+
//# sourceMappingURL=destructiveTokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destructiveTokens.d.ts","sourceRoot":"","sources":["../../src/permissions/destructiveTokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA2BhD;AAgQD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,EAC/C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,GAC7C,UAAU,GAAG,IAAI,CA8DnB"}
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token-level half of the destructive-command classifier.
|
|
4
|
+
*
|
|
5
|
+
* destructive.ts started life as a pile of regexes run over the command STRING. That
|
|
6
|
+
* missed most real-world spellings, because a regex does not understand shell:
|
|
7
|
+
* - global options between the tool and its verb: git -C repo reset --hard
|
|
8
|
+
* - combined short flags: git push -fu origin main
|
|
9
|
+
* - refspec force syntax: git push origin +main
|
|
10
|
+
* - wrappers that hide the real command: sudo / env X=1 / xargs / nohup
|
|
11
|
+
* - interpreters that take the command as a string: bash -c "…", eval "…", ssh host "…"
|
|
12
|
+
* - containers that run it elsewhere: docker exec db psql … / kubectl exec -- …
|
|
13
|
+
* and it could not express "rm -rf node_modules is fine, rm -rf src is not".
|
|
14
|
+
*
|
|
15
|
+
* So this module splits the command into simple commands the way a shell would (honouring
|
|
16
|
+
* quotes, so a commit message that MENTIONS `git push --force` is one argument of
|
|
17
|
+
* `git commit`, never a command), peels wrappers off, and judges (binary, args).
|
|
18
|
+
*
|
|
19
|
+
* It is a heuristic, not a shell: no variable expansion, no aliases, no functions. When it
|
|
20
|
+
* cannot tell (an `rm -rf $DIR`), it errs toward asking — the cost of a false positive is
|
|
21
|
+
* one confirmation, the cost of a false negative is unrecoverable.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.tokenize = tokenize;
|
|
25
|
+
exports.classifyTokens = classifyTokens;
|
|
26
|
+
const hit = (category, reason) => ({ category, reason });
|
|
27
|
+
/** Split `src` into simple commands, each a list of unquoted words. */
|
|
28
|
+
function tokenize(src) {
|
|
29
|
+
const cmds = [];
|
|
30
|
+
let cur = [];
|
|
31
|
+
let word = '';
|
|
32
|
+
let has = false;
|
|
33
|
+
let q = null;
|
|
34
|
+
const pushWord = () => { if (has)
|
|
35
|
+
cur.push(word); word = ''; has = false; };
|
|
36
|
+
const pushCmd = () => { pushWord(); if (cur.length)
|
|
37
|
+
cmds.push(cur); cur = []; };
|
|
38
|
+
for (let i = 0; i < src.length; i++) {
|
|
39
|
+
const ch = src[i];
|
|
40
|
+
if (q === "'") {
|
|
41
|
+
if (ch === "'")
|
|
42
|
+
q = null;
|
|
43
|
+
else
|
|
44
|
+
word += ch;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (q === '"') {
|
|
48
|
+
if (ch === '\\' && i + 1 < src.length) {
|
|
49
|
+
word += src[++i];
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (ch === '"') {
|
|
53
|
+
q = null;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
word += ch;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === "'" || ch === '"') {
|
|
60
|
+
q = ch;
|
|
61
|
+
has = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === '\\') {
|
|
65
|
+
if (i + 1 < src.length) {
|
|
66
|
+
word += src[++i];
|
|
67
|
+
has = true;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Command separators. `(` `)` and backtick make $(…) / `…` bodies their own commands.
|
|
72
|
+
if (ch === ';' || ch === '&' || ch === '|' || ch === '\n' || ch === '(' || ch === ')' || ch === '`') {
|
|
73
|
+
pushCmd();
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === ' ' || ch === '\t') {
|
|
77
|
+
pushWord();
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
word += ch;
|
|
81
|
+
has = true;
|
|
82
|
+
}
|
|
83
|
+
pushCmd();
|
|
84
|
+
return cmds;
|
|
85
|
+
}
|
|
86
|
+
const ENV_ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
87
|
+
const CONTROL_WORDS = new Set(['{', '}', '!', 'if', 'then', 'else', 'elif', 'do', 'while', 'until', 'fi', 'done']);
|
|
88
|
+
const SHELLS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh']);
|
|
89
|
+
const base = (w) => {
|
|
90
|
+
const b = w.includes('/') ? w.slice(w.lastIndexOf('/') + 1) : w;
|
|
91
|
+
return b;
|
|
92
|
+
};
|
|
93
|
+
/** Drop leading options from `args`, consuming a value for those named in `withArg`. */
|
|
94
|
+
function dropOpts(args, withArg = new Set()) {
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < args.length && args[i].startsWith('-') && args[i] !== '--') {
|
|
97
|
+
i += withArg.has(args[i]) ? 2 : 1;
|
|
98
|
+
}
|
|
99
|
+
if (args[i] === '--')
|
|
100
|
+
i++;
|
|
101
|
+
return args.slice(i);
|
|
102
|
+
}
|
|
103
|
+
/** True if `args` carries the long flag (or `--flag=value`) or a short-flag bundle containing `short`. */
|
|
104
|
+
function has(args, long, short) {
|
|
105
|
+
return args.some((a) => (long !== null && (a === long || a.startsWith(long + '='))) ||
|
|
106
|
+
(short !== null && /^-[A-Za-z]+$/.test(a) && a.includes(short)));
|
|
107
|
+
}
|
|
108
|
+
const BROAD = new Set(['.', './', '*', './*', ':/', ':/', '..', '../', '/', '~', '~/', '$HOME', '${HOME}', '.*']);
|
|
109
|
+
/** Directories that are regenerated by a build/install and are routine to delete. */
|
|
110
|
+
const DISPOSABLE = new Set([
|
|
111
|
+
'node_modules', 'dist', 'build', 'out', '.next', '.nuxt', '.svelte-kit', '.turbo', '.cache', '.parcel-cache',
|
|
112
|
+
'coverage', '.nyc_output', 'target', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox',
|
|
113
|
+
'venv', '.venv', '.gradle', '.dart_tool', 'tmp', '.tmp', '.vite', '.angular', '.expo', 'DerivedData',
|
|
114
|
+
]);
|
|
115
|
+
const SCRATCH_PREFIXES = ['/tmp/', '/private/tmp/', '/var/folders/', '/private/var/folders/'];
|
|
116
|
+
function isDisposableTarget(raw) {
|
|
117
|
+
let t = raw;
|
|
118
|
+
if (t === '')
|
|
119
|
+
return false;
|
|
120
|
+
// Anything we cannot resolve statically is treated as unsafe.
|
|
121
|
+
if (/[$~`]/.test(t) || t.includes('..'))
|
|
122
|
+
return false;
|
|
123
|
+
if (t.startsWith('/'))
|
|
124
|
+
return SCRATCH_PREFIXES.some((p) => t.startsWith(p) && t.length > p.length);
|
|
125
|
+
t = t.replace(/^(?:\.\/)+/, '').replace(/\/+$/, '');
|
|
126
|
+
if (t === '' || t === '.')
|
|
127
|
+
return false;
|
|
128
|
+
const segs = t.split('/').filter(Boolean);
|
|
129
|
+
if (segs.length === 0)
|
|
130
|
+
return false;
|
|
131
|
+
return DISPOSABLE.has(segs[0]) || DISPOSABLE.has(segs[segs.length - 1]);
|
|
132
|
+
}
|
|
133
|
+
function checkGit(argsIn) {
|
|
134
|
+
// Skip git's GLOBAL options so `git -C repo reset --hard` is seen as `reset --hard`.
|
|
135
|
+
let i = 0;
|
|
136
|
+
while (i < argsIn.length && argsIn[i].startsWith('-')) {
|
|
137
|
+
const a = argsIn[i];
|
|
138
|
+
i += ['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path'].includes(a) ? 2 : 1;
|
|
139
|
+
}
|
|
140
|
+
const sub = argsIn[i];
|
|
141
|
+
const args = argsIn.slice(i + 1);
|
|
142
|
+
const operands = args.filter((a) => !a.startsWith('-'));
|
|
143
|
+
switch (sub) {
|
|
144
|
+
case 'push': {
|
|
145
|
+
if (has(args, '--force', 'f') || operands.some((a) => a.startsWith('+'))) {
|
|
146
|
+
return hit('git-history', 'force-pushes (can overwrite/destroy remote history)');
|
|
147
|
+
}
|
|
148
|
+
if (has(args, '--delete', 'd') || operands.some((a) => a.startsWith(':'))) {
|
|
149
|
+
return hit('git-history', 'deletes a remote branch or tag');
|
|
150
|
+
}
|
|
151
|
+
if (has(args, '--mirror', null)) {
|
|
152
|
+
return hit('git-history', 'mirror-pushes (overwrites and deletes remote refs to match local)');
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
case 'reset':
|
|
157
|
+
if (has(args, '--hard', null) || has(args, '--keep', null) || has(args, '--merge', null)) {
|
|
158
|
+
return hit('git-history', 'hard-resets the working tree (discards uncommitted work)');
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
case 'clean':
|
|
162
|
+
if (has(args, '--force', 'f') && !has(args, '--dry-run', 'n')) {
|
|
163
|
+
return hit('git-history', 'deletes untracked files (git clean)');
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
case 'branch':
|
|
167
|
+
if (args.some((a) => /^-[A-Za-z]*D/.test(a)) ||
|
|
168
|
+
((has(args, '--delete', 'd')) && has(args, '--force', 'f'))) {
|
|
169
|
+
return hit('git-history', 'force-deletes a git branch');
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
case 'checkout':
|
|
173
|
+
if (has(args, '--force', 'f'))
|
|
174
|
+
return hit('git-history', 'git checkout --force discards local changes');
|
|
175
|
+
if (args.some((a) => BROAD.has(a)))
|
|
176
|
+
return hit('git-history', 'discards every uncommitted change in the working tree');
|
|
177
|
+
return null;
|
|
178
|
+
case 'restore': {
|
|
179
|
+
const stagedOnly = has(args, '--staged', 'S') && !has(args, '--worktree', 'W');
|
|
180
|
+
if (!stagedOnly && args.some((a) => BROAD.has(a))) {
|
|
181
|
+
return hit('git-history', 'discards every uncommitted change in the working tree');
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
case 'stash':
|
|
186
|
+
if (args[0] === 'drop' || args[0] === 'clear')
|
|
187
|
+
return hit('git-history', 'drops stashed work (not recoverable without the commit hash)');
|
|
188
|
+
return null;
|
|
189
|
+
case 'update-ref':
|
|
190
|
+
if (has(args, '-d', 'd') || has(args, '--delete', null))
|
|
191
|
+
return hit('git-history', 'deletes a git ref');
|
|
192
|
+
return null;
|
|
193
|
+
case 'reflog':
|
|
194
|
+
if (args[0] === 'delete' || args[0] === 'expire')
|
|
195
|
+
return hit('git-history', 'reflog expire / gc prune (permanently drops recoverable commits)');
|
|
196
|
+
return null;
|
|
197
|
+
case 'gc':
|
|
198
|
+
if (args.some((a) => /^--prune=(?:now|all)$/.test(a)))
|
|
199
|
+
return hit('git-history', 'reflog expire / gc prune (permanently drops recoverable commits)');
|
|
200
|
+
return null;
|
|
201
|
+
case 'worktree':
|
|
202
|
+
if (args[0] === 'remove' && has(args, '--force', 'f'))
|
|
203
|
+
return hit('git-history', 'force-removes a worktree (discards its uncommitted work)');
|
|
204
|
+
return null;
|
|
205
|
+
case 'filter-branch':
|
|
206
|
+
case 'filter-repo':
|
|
207
|
+
return hit('git-history', 'rewrites git history (filter-branch/filter-repo)');
|
|
208
|
+
case 'rm':
|
|
209
|
+
// `git rm -r <dir>` is routine and recoverable from HEAD; only a bare `.`/`*` is a wipe.
|
|
210
|
+
if (operands.some((a) => BROAD.has(a))) {
|
|
211
|
+
return hit('git-history', 'removes every tracked file from the working tree and index');
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
default:
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function checkRm(args) {
|
|
219
|
+
const targets = [];
|
|
220
|
+
let recursive = false;
|
|
221
|
+
let endOpts = false;
|
|
222
|
+
for (const a of args) {
|
|
223
|
+
if (!endOpts && a === '--') {
|
|
224
|
+
endOpts = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (!endOpts && a.startsWith('-')) {
|
|
228
|
+
if (a === '--recursive' || /^-[A-Za-z]*[rR]/.test(a))
|
|
229
|
+
recursive = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
targets.push(a);
|
|
233
|
+
}
|
|
234
|
+
if (targets.some((t) => BROAD.has(t)))
|
|
235
|
+
return hit('filesystem', 'deletes the current directory, home, or everything matching a bare glob');
|
|
236
|
+
if (recursive && targets.some((t) => !isDisposableTarget(t))) {
|
|
237
|
+
return hit('filesystem', 'recursively deletes files or directories that are not a known build artefact');
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
function checkFind(args) {
|
|
242
|
+
if (args.includes('-delete'))
|
|
243
|
+
return hit('filesystem', 'find -delete removes every match');
|
|
244
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
245
|
+
if ((args[i] === '-exec' || args[i] === '-execdir' || args[i] === '-ok') && ['rm', 'shred', 'unlink'].includes(base(args[i + 1]))) {
|
|
246
|
+
return hit('filesystem', 'find -exec rm removes every match');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
/** Judge one already-unwrapped command. `recurse` re-enters the string classifier. */
|
|
252
|
+
function judge(bin, args) {
|
|
253
|
+
switch (bin) {
|
|
254
|
+
case 'git': return checkGit(args);
|
|
255
|
+
case 'rm':
|
|
256
|
+
case 'unlink': return checkRm(args);
|
|
257
|
+
case 'find': return checkFind(args);
|
|
258
|
+
case 'rsync':
|
|
259
|
+
if (args.some((a) => a === '--del' || /^--delete(?:-[a-z]+)?$/.test(a)))
|
|
260
|
+
return hit('filesystem', 'rsync --delete removes destination files that are absent from the source');
|
|
261
|
+
return null;
|
|
262
|
+
case 'truncate': return hit('filesystem', 'truncate cuts a file (contents lost)');
|
|
263
|
+
case 'shred': return hit('filesystem', 'shred irrecoverably overwrites a file');
|
|
264
|
+
case 'dd':
|
|
265
|
+
if (args.some((a) => /^of=\/dev\//.test(a)))
|
|
266
|
+
return hit('filesystem', 'dd writing to a device (overwrites the disk)');
|
|
267
|
+
return null;
|
|
268
|
+
case 'diskutil':
|
|
269
|
+
if (/^(?:erase|secureErase|zeroDisk|randomDisk|partitionDisk|reformat)/i.test(args[0] ?? ''))
|
|
270
|
+
return hit('filesystem', 'diskutil erases or repartitions a disk');
|
|
271
|
+
return null;
|
|
272
|
+
case 'chmod': {
|
|
273
|
+
if (!has(args, '--recursive', 'R'))
|
|
274
|
+
return null;
|
|
275
|
+
const operands = args.filter((a) => !a.startsWith('-'));
|
|
276
|
+
if (operands.slice(1).some((t) => BROAD.has(t)))
|
|
277
|
+
return hit('filesystem', 'recursive chmod over the current directory / home / root');
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
case 'terraform':
|
|
281
|
+
case 'tofu':
|
|
282
|
+
case 'terragrunt':
|
|
283
|
+
if (args.includes('destroy') || (args.includes('apply') && (args.includes('-destroy') || args.includes('-auto-approve')))) {
|
|
284
|
+
return hit('infra', 'destroys or applies Terraform-managed infrastructure without a plan review');
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
case 'kubectl':
|
|
288
|
+
case 'oc': {
|
|
289
|
+
const a = dropOpts(args, new Set(['-n', '--namespace', '--context', '--cluster', '--kubeconfig', '-s', '--server', '--user']));
|
|
290
|
+
if (a[0] === 'delete')
|
|
291
|
+
return hit('infra', 'deletes Kubernetes resources');
|
|
292
|
+
if (a[0] === 'drain')
|
|
293
|
+
return hit('infra', 'drains a node (evicts every pod)');
|
|
294
|
+
if (a[0] === 'replace' && has(a, '--force', null))
|
|
295
|
+
return hit('infra', 'kubectl replace --force deletes and recreates the resource');
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
case 'helm': {
|
|
299
|
+
const a = dropOpts(args, new Set(['-n', '--namespace', '--kube-context', '--kubeconfig']));
|
|
300
|
+
if (a[0] === 'uninstall' || a[0] === 'delete')
|
|
301
|
+
return hit('infra', 'helm uninstall / delete (removes a release)');
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
case 'aws':
|
|
305
|
+
if (args[0] === 's3' && args[1] === 'sync' && has(args, '--delete', null))
|
|
306
|
+
return hit('cloud', 'aws s3 sync --delete removes bucket objects missing locally');
|
|
307
|
+
return null;
|
|
308
|
+
case 'vercel':
|
|
309
|
+
if (args[0] === 'remove' || args[0] === 'rm')
|
|
310
|
+
return hit('cloud', 'removes a Vercel project or deployment');
|
|
311
|
+
return null;
|
|
312
|
+
case 'fly':
|
|
313
|
+
case 'flyctl':
|
|
314
|
+
if (args.includes('destroy'))
|
|
315
|
+
return hit('cloud', 'destroys a Fly.io app, volume or machine');
|
|
316
|
+
return null;
|
|
317
|
+
case 'heroku':
|
|
318
|
+
if (args.some((a) => /:destroy$/.test(a) || a === 'pg:reset'))
|
|
319
|
+
return hit('cloud', 'destroys a Heroku app or resets its database');
|
|
320
|
+
return null;
|
|
321
|
+
case 'supabase':
|
|
322
|
+
if ((args[0] === 'db' && args[1] === 'reset') || (args[0] === 'projects' && args[1] === 'delete'))
|
|
323
|
+
return hit('cloud', 'resets a Supabase database or deletes a project');
|
|
324
|
+
return null;
|
|
325
|
+
case 'firebase':
|
|
326
|
+
if (args.some((a) => /:delete$/.test(a)))
|
|
327
|
+
return hit('cloud', 'deletes Firebase data or a project');
|
|
328
|
+
return null;
|
|
329
|
+
case 'netlify':
|
|
330
|
+
if (args.some((a) => /:delete$/.test(a)))
|
|
331
|
+
return hit('cloud', 'deletes a Netlify site');
|
|
332
|
+
return null;
|
|
333
|
+
case 'npm':
|
|
334
|
+
case 'pnpm':
|
|
335
|
+
case 'yarn':
|
|
336
|
+
if (args[0] === 'unpublish')
|
|
337
|
+
return hit('publish', 'unpublishes a package version');
|
|
338
|
+
if (args[0] === 'dist-tag' && (args[1] === 'rm' || args[1] === 'remove'))
|
|
339
|
+
return hit('publish', 'removes an npm dist-tag (can break `npm i pkg@latest`)');
|
|
340
|
+
return null;
|
|
341
|
+
case 'gh': {
|
|
342
|
+
if ((args[0] === 'repo' || args[0] === 'release') && args[1] === 'delete')
|
|
343
|
+
return hit('cloud', 'deletes a GitHub repository or release');
|
|
344
|
+
if (args[0] === 'api') {
|
|
345
|
+
const m = args.findIndex((a) => a === '-X' || a === '--method');
|
|
346
|
+
const inline = args.find((a) => /^(?:-X|--method=)DELETE$/i.test(a));
|
|
347
|
+
if ((m >= 0 && /^delete$/i.test(args[m + 1] ?? '')) || inline)
|
|
348
|
+
return hit('cloud', 'sends an HTTP DELETE to the GitHub API');
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
case 'docker':
|
|
353
|
+
case 'docker-compose':
|
|
354
|
+
case 'podman': {
|
|
355
|
+
const a = bin === 'docker-compose' ? ['compose', ...args] : dropOpts(args, new Set(['-H', '--host', '--context', '-c']));
|
|
356
|
+
const verb = a[0] === 'container' ? a[1] : a[0];
|
|
357
|
+
const rest = a.slice(a[0] === 'container' ? 2 : 1);
|
|
358
|
+
if (verb === 'rm' && has(rest, '--force', 'f') && rest.some((x) => x.startsWith('$')))
|
|
359
|
+
return hit('container', 'force-removes many containers at once');
|
|
360
|
+
if (a[0] === 'compose' && rest.includes('down') && (has(rest, '--volumes', 'v')))
|
|
361
|
+
return hit('container', 'docker compose down with -v (deletes named volumes)');
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
case 'dropdb':
|
|
365
|
+
case 'dropuser': return hit('database', 'drops a Postgres database or role');
|
|
366
|
+
default:
|
|
367
|
+
if (bin.startsWith('mkfs'))
|
|
368
|
+
return hit('filesystem', 'mkfs formats a filesystem (destroys its contents)');
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Peel wrappers off one simple command and judge what is really being run.
|
|
374
|
+
* `recurse` classifies a nested command STRING (bash -c "…", ssh host "…", eval …).
|
|
375
|
+
*/
|
|
376
|
+
const DB_BINS = new Set([
|
|
377
|
+
'psql', 'mysql', 'mariadb', 'mongo', 'mongosh', 'sqlite3', 'cockroach', 'clickhouse-client', 'pgcli', 'mycli', 'usql', 'sqlcmd', 'redis-cli',
|
|
378
|
+
]);
|
|
379
|
+
function classifyTokens(tokens, recurse, rawSql) {
|
|
380
|
+
let t = tokens.slice();
|
|
381
|
+
for (let guard = 0; guard < 12; guard++) {
|
|
382
|
+
while (t.length && (ENV_ASSIGN.test(t[0]) || CONTROL_WORDS.has(t[0])))
|
|
383
|
+
t = t.slice(1);
|
|
384
|
+
if (t.length === 0)
|
|
385
|
+
return null;
|
|
386
|
+
// A bare redirect with no command (`> file`, `: > file`, `true > file`) truncates the file.
|
|
387
|
+
{
|
|
388
|
+
const r = /^>(?![>&])/.test(t[0]) ? t : ((t[0] === ':' || t[0] === 'true') && /^>(?![>&])/.test(t[1] ?? '')) ? t.slice(1) : null;
|
|
389
|
+
if (r) {
|
|
390
|
+
const target = r[0].length > 1 ? r[0].slice(1) : (r[1] ?? '');
|
|
391
|
+
if (target.startsWith('/dev/'))
|
|
392
|
+
return null;
|
|
393
|
+
const m = hit('filesystem', 'a bare `>` redirect truncates the file (contents lost)');
|
|
394
|
+
// Scratch/relative targets only: no `$var`, `~`, `..`; absolute paths must be under a temp dir.
|
|
395
|
+
const scratch = target !== '' && !/[$`~]|(^|\/)\.\.(\/|$)/.test(target)
|
|
396
|
+
&& (!target.startsWith('/') || /^\/(?:private\/)?(?:tmp|var\/folders)\//.test(target));
|
|
397
|
+
if (scratch)
|
|
398
|
+
m.lowRisk = true;
|
|
399
|
+
return m;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const bin = base(t[0]);
|
|
403
|
+
const args = t.slice(1);
|
|
404
|
+
// Real SQL lives inside quotes, which the tokenizer has already unwrapped; hand it back
|
|
405
|
+
// to the quote-preserving SQL rules as a normal `client "arg" "arg"` string.
|
|
406
|
+
if (DB_BINS.has(bin)) {
|
|
407
|
+
return rawSql(bin + ' ' + args.map((a) => JSON.stringify(a)).join(' '));
|
|
408
|
+
}
|
|
409
|
+
if (bin === 'sudo' || bin === 'doas') {
|
|
410
|
+
t = dropOpts(args, new Set(['-u', '-g', '-h', '-p', '-C', '-D', '-R', '-T', '-U']));
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (bin === 'env') {
|
|
414
|
+
t = dropOpts(args, new Set(['-u', '-C', '-S']));
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (bin === 'nice') {
|
|
418
|
+
t = dropOpts(args, new Set(['-n']));
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (bin === 'ionice') {
|
|
422
|
+
t = dropOpts(args, new Set(['-c', '-n', '-p']));
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (bin === 'timeout') {
|
|
426
|
+
t = dropOpts(args, new Set(['-s', '-k'])).slice(1);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (bin === 'xargs') {
|
|
430
|
+
t = dropOpts(args, new Set(['-I', '-n', '-P', '-L', '-s', '-d', '-E', '-a', '-J']));
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (['command', 'builtin', 'nohup', 'exec', 'time', 'setsid', 'caffeinate', 'stdbuf', 'unbuffer'].includes(bin)) {
|
|
434
|
+
t = dropOpts(args);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (SHELLS.has(bin)) {
|
|
438
|
+
const i = args.findIndex((a) => /^-[A-Za-z]*c[A-Za-z]*$/.test(a));
|
|
439
|
+
if (i >= 0 && args[i + 1] !== undefined)
|
|
440
|
+
return recurse(args[i + 1]);
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
if (bin === 'eval')
|
|
444
|
+
return recurse(args.join(' '));
|
|
445
|
+
if (bin === 'ssh') {
|
|
446
|
+
const a = dropOpts(args, new Set(['-p', '-i', '-l', '-o', '-F', '-J', '-L', '-R', '-D', '-b', '-c', '-E', '-e', '-I', '-m', '-O', '-Q', '-S', '-w', '-W']));
|
|
447
|
+
return a.length > 1 ? recurse(a.slice(1).join(' ')) : null;
|
|
448
|
+
}
|
|
449
|
+
if (bin === 'docker' || bin === 'podman' || bin === 'docker-compose') {
|
|
450
|
+
const a0 = bin === 'docker-compose' ? ['compose', ...args] : dropOpts(args, new Set(['-H', '--host', '--context', '-c']));
|
|
451
|
+
const isExec = a0[0] === 'exec' || (a0[0] === 'compose' && dropOpts(a0.slice(1), new Set(['-f', '-p', '--project-name', '--file', '--profile']))[0] === 'exec');
|
|
452
|
+
if (isExec) {
|
|
453
|
+
const afterVerb = a0[0] === 'exec'
|
|
454
|
+
? a0.slice(1)
|
|
455
|
+
: dropOpts(a0.slice(1), new Set(['-f', '-p', '--project-name', '--file', '--profile'])).slice(1);
|
|
456
|
+
const inner = dropOpts(afterVerb, new Set(['-e', '-u', '-w', '--env', '--user', '--workdir', '--env-file', '--detach-keys', '--index']));
|
|
457
|
+
if (inner.length > 1) {
|
|
458
|
+
t = inner.slice(1);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (bin === 'kubectl' || bin === 'oc') {
|
|
465
|
+
const i = args.indexOf('--');
|
|
466
|
+
if (i >= 0 && args.includes('exec') && i + 1 < args.length) {
|
|
467
|
+
t = args.slice(i + 1);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return judge(bin, args);
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
//# sourceMappingURL=destructiveTokens.js.map
|