@vola-trebla/croak 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +283 -2
- package/dist/cli.js +373 -0
- package/dist/crypto/der.js +136 -0
- package/dist/crypto/fields.js +60 -0
- package/dist/crypto/registry-signature.js +88 -0
- package/dist/crypto/rekor.js +70 -0
- package/dist/crypto/sigstore.js +121 -0
- package/dist/detectors/git-config.js +137 -0
- package/dist/detectors/hidden-code.js +58 -0
- package/dist/detectors/ide-autorun.js +136 -0
- package/dist/detectors/index.js +20 -0
- package/dist/detectors/lifecycle-script.js +337 -0
- package/dist/detectors/node-modules-present.js +32 -0
- package/dist/detectors/obfuscation.js +124 -0
- package/dist/detectors/unscanned.js +41 -0
- package/dist/hook/hook.js +391 -0
- package/dist/hook/install-commands.js +927 -0
- package/dist/install/content-cache.js +94 -0
- package/dist/install/content.js +42 -0
- package/dist/install/detectors.js +79 -0
- package/dist/install/installed.js +99 -0
- package/dist/install/lockfile.js +72 -0
- package/dist/install/metadata.js +159 -0
- package/dist/install/plan.js +31 -0
- package/dist/install/pnpm-builds.js +111 -0
- package/dist/install/pnpm-lockfile.js +102 -0
- package/dist/install/popular.js +177 -0
- package/dist/install/project.js +223 -0
- package/dist/install/provenance.js +77 -0
- package/dist/install/registry.js +274 -0
- package/dist/install/report.js +26 -0
- package/dist/install/sanitize.js +88 -0
- package/dist/install/scan.js +302 -0
- package/dist/install/signature.js +62 -0
- package/dist/install/tar.js +178 -0
- package/dist/install/workspace.js +31 -0
- package/dist/install/yaml.js +282 -0
- package/dist/llm/client.js +22 -0
- package/dist/llm/cost.js +26 -0
- package/dist/llm/diff.js +114 -0
- package/dist/llm/escalate.js +14 -0
- package/dist/llm/plan-step.js +91 -0
- package/dist/llm/select.js +21 -0
- package/dist/llm/triage.js +111 -0
- package/dist/repo-report.js +95 -0
- package/dist/repo-snapshot.js +157 -0
- package/dist/scan.js +48 -0
- package/dist/signals/code.js +136 -0
- package/dist/signals/git-config.js +58 -0
- package/dist/signals/hidden.js +77 -0
- package/dist/signals/jsonc.js +100 -0
- package/dist/signals/obfuscation.js +98 -0
- package/dist/signals/shell.js +717 -0
- package/dist/signals/text.js +2 -0
- package/dist/ui.js +12 -0
- package/dist/verdict.js +20 -0
- package/package.json +71 -4
- package/scripts/croak-hook.sh +59 -0
- package/scripts/croak-install.sh +262 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { shellCommands, } from "../signals/shell.js";
|
|
3
|
+
/** Agent commands can be long (heredocs); the parser is linear. */
|
|
4
|
+
const MAX_COMMAND_LENGTH = 100_000;
|
|
5
|
+
const MAX_DEPTH = 4;
|
|
6
|
+
/** Any of these in a command croak cannot follow makes it unchecked. */
|
|
7
|
+
const MENTIONS = /\b(?:npm|npx|pnpm|pnpx|yarn|bun|bunx|corepack)\b/;
|
|
8
|
+
const PACKAGE_MANAGERS = new Set([
|
|
9
|
+
"npm",
|
|
10
|
+
"npx",
|
|
11
|
+
"pnpm",
|
|
12
|
+
"pnpx",
|
|
13
|
+
"yarn",
|
|
14
|
+
"bun",
|
|
15
|
+
"bunx",
|
|
16
|
+
"corepack",
|
|
17
|
+
]);
|
|
18
|
+
// ------------------------------------------------------------ npm commands
|
|
19
|
+
/** npm's commands (npm 10/11 cmd-list), for its unique-prefix matching. */
|
|
20
|
+
const NPM_COMMANDS = [
|
|
21
|
+
"access",
|
|
22
|
+
"adduser",
|
|
23
|
+
"audit",
|
|
24
|
+
"bugs",
|
|
25
|
+
"cache",
|
|
26
|
+
"ci",
|
|
27
|
+
"completion",
|
|
28
|
+
"config",
|
|
29
|
+
"dedupe",
|
|
30
|
+
"deprecate",
|
|
31
|
+
"diff",
|
|
32
|
+
"dist-tag",
|
|
33
|
+
"docs",
|
|
34
|
+
"doctor",
|
|
35
|
+
"edit",
|
|
36
|
+
"exec",
|
|
37
|
+
"explain",
|
|
38
|
+
"explore",
|
|
39
|
+
"find-dupes",
|
|
40
|
+
"fund",
|
|
41
|
+
"get",
|
|
42
|
+
"help",
|
|
43
|
+
"help-search",
|
|
44
|
+
"hook",
|
|
45
|
+
"init",
|
|
46
|
+
"install",
|
|
47
|
+
"install-ci-test",
|
|
48
|
+
"install-test",
|
|
49
|
+
"link",
|
|
50
|
+
"ll",
|
|
51
|
+
"login",
|
|
52
|
+
"logout",
|
|
53
|
+
"ls",
|
|
54
|
+
"org",
|
|
55
|
+
"outdated",
|
|
56
|
+
"owner",
|
|
57
|
+
"pack",
|
|
58
|
+
"ping",
|
|
59
|
+
"pkg",
|
|
60
|
+
"prefix",
|
|
61
|
+
"profile",
|
|
62
|
+
"prune",
|
|
63
|
+
"publish",
|
|
64
|
+
"query",
|
|
65
|
+
"rebuild",
|
|
66
|
+
"repo",
|
|
67
|
+
"restart",
|
|
68
|
+
"root",
|
|
69
|
+
"run-script",
|
|
70
|
+
"sbom",
|
|
71
|
+
"search",
|
|
72
|
+
"set",
|
|
73
|
+
"shrinkwrap",
|
|
74
|
+
"star",
|
|
75
|
+
"stars",
|
|
76
|
+
"start",
|
|
77
|
+
"stop",
|
|
78
|
+
"team",
|
|
79
|
+
"test",
|
|
80
|
+
"token",
|
|
81
|
+
"undeprecate",
|
|
82
|
+
"uninstall",
|
|
83
|
+
"unpublish",
|
|
84
|
+
"unstar",
|
|
85
|
+
"update",
|
|
86
|
+
"version",
|
|
87
|
+
"view",
|
|
88
|
+
"whoami",
|
|
89
|
+
];
|
|
90
|
+
const NPM_ALIASES = {
|
|
91
|
+
i: "install",
|
|
92
|
+
in: "install",
|
|
93
|
+
ins: "install",
|
|
94
|
+
inst: "install",
|
|
95
|
+
insta: "install",
|
|
96
|
+
instal: "install",
|
|
97
|
+
isnt: "install",
|
|
98
|
+
isnta: "install",
|
|
99
|
+
isntal: "install",
|
|
100
|
+
isntall: "install",
|
|
101
|
+
add: "install",
|
|
102
|
+
it: "install-test",
|
|
103
|
+
cit: "install-ci-test",
|
|
104
|
+
"clean-install-test": "install-ci-test",
|
|
105
|
+
sit: "install-ci-test",
|
|
106
|
+
ic: "ci",
|
|
107
|
+
"clean-install": "ci",
|
|
108
|
+
"install-clean": "ci",
|
|
109
|
+
"isntall-clean": "ci",
|
|
110
|
+
up: "update",
|
|
111
|
+
upgrade: "update",
|
|
112
|
+
udpate: "update",
|
|
113
|
+
x: "exec",
|
|
114
|
+
create: "init",
|
|
115
|
+
innit: "init",
|
|
116
|
+
ddp: "dedupe",
|
|
117
|
+
un: "uninstall",
|
|
118
|
+
unlink: "uninstall",
|
|
119
|
+
remove: "uninstall",
|
|
120
|
+
rm: "uninstall",
|
|
121
|
+
r: "uninstall",
|
|
122
|
+
rb: "rebuild",
|
|
123
|
+
list: "ls",
|
|
124
|
+
la: "ls",
|
|
125
|
+
ln: "link",
|
|
126
|
+
run: "run-script",
|
|
127
|
+
rum: "run-script",
|
|
128
|
+
urn: "run-script",
|
|
129
|
+
t: "test",
|
|
130
|
+
tst: "test",
|
|
131
|
+
c: "config",
|
|
132
|
+
why: "explain",
|
|
133
|
+
info: "view",
|
|
134
|
+
show: "view",
|
|
135
|
+
v: "view",
|
|
136
|
+
s: "search",
|
|
137
|
+
se: "search",
|
|
138
|
+
find: "search",
|
|
139
|
+
};
|
|
140
|
+
const NPM_NAMES = [...NPM_COMMANDS, ...Object.keys(NPM_ALIASES)];
|
|
141
|
+
/** Subcommands that download or install packages. */
|
|
142
|
+
const INSTALLING = new Set([
|
|
143
|
+
"install",
|
|
144
|
+
"install-test",
|
|
145
|
+
"ci",
|
|
146
|
+
"install-ci-test",
|
|
147
|
+
"update",
|
|
148
|
+
"exec",
|
|
149
|
+
"init",
|
|
150
|
+
"dedupe",
|
|
151
|
+
"link",
|
|
152
|
+
]);
|
|
153
|
+
/** npm's deref: camelCase to kebab-case, aliases, then a unique prefix. */
|
|
154
|
+
function npmCommand(word) {
|
|
155
|
+
const name = word.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
156
|
+
const exact = NPM_ALIASES[name] ?? (NPM_COMMANDS.includes(name) ? name : undefined);
|
|
157
|
+
if (exact !== undefined)
|
|
158
|
+
return exact;
|
|
159
|
+
const matches = new Set(NPM_NAMES.filter((n) => n.startsWith(name)).map((n) => NPM_ALIASES[n] ?? n));
|
|
160
|
+
return matches.size === 1 ? [...matches][0] : undefined;
|
|
161
|
+
}
|
|
162
|
+
/** npm options that take a value (`--prefix dir`, `-C dir`, `-w pkg`). */
|
|
163
|
+
const VALUE_FLAGS = new Set([
|
|
164
|
+
"prefix",
|
|
165
|
+
"workspace",
|
|
166
|
+
"registry",
|
|
167
|
+
"userconfig",
|
|
168
|
+
"globalconfig",
|
|
169
|
+
"cache",
|
|
170
|
+
"loglevel",
|
|
171
|
+
"tag",
|
|
172
|
+
"package",
|
|
173
|
+
"call",
|
|
174
|
+
"omit",
|
|
175
|
+
"include",
|
|
176
|
+
"before",
|
|
177
|
+
"install-strategy",
|
|
178
|
+
"save-prefix",
|
|
179
|
+
"cpu",
|
|
180
|
+
"os",
|
|
181
|
+
"libc",
|
|
182
|
+
"location",
|
|
183
|
+
"otp",
|
|
184
|
+
"scope",
|
|
185
|
+
"proxy",
|
|
186
|
+
"https-proxy",
|
|
187
|
+
"noproxy",
|
|
188
|
+
"cafile",
|
|
189
|
+
"ca",
|
|
190
|
+
"cert",
|
|
191
|
+
"key",
|
|
192
|
+
"node-options",
|
|
193
|
+
"script-shell",
|
|
194
|
+
"shell",
|
|
195
|
+
"fetch-retries",
|
|
196
|
+
"fetch-timeout",
|
|
197
|
+
"maxsockets",
|
|
198
|
+
"replace-registry-host",
|
|
199
|
+
"auth-type",
|
|
200
|
+
"access",
|
|
201
|
+
"logs-dir",
|
|
202
|
+
"logs-max",
|
|
203
|
+
"tmp",
|
|
204
|
+
"editor",
|
|
205
|
+
"init-module",
|
|
206
|
+
"lockfile-version",
|
|
207
|
+
"depth",
|
|
208
|
+
"color",
|
|
209
|
+
"umask",
|
|
210
|
+
"user-agent",
|
|
211
|
+
"heading",
|
|
212
|
+
"browser",
|
|
213
|
+
"viewer",
|
|
214
|
+
"message",
|
|
215
|
+
"preid",
|
|
216
|
+
"sbom-format",
|
|
217
|
+
"sbom-type",
|
|
218
|
+
"expect-results",
|
|
219
|
+
"expect-result-count",
|
|
220
|
+
"audit-level",
|
|
221
|
+
"fetch-retry-factor",
|
|
222
|
+
"fetch-retry-mintimeout",
|
|
223
|
+
"fetch-retry-maxtimeout",
|
|
224
|
+
]);
|
|
225
|
+
/** npm options that are flags (no value). */
|
|
226
|
+
const BOOLEAN_FLAGS = new Set([
|
|
227
|
+
"save",
|
|
228
|
+
"save-dev",
|
|
229
|
+
"save-exact",
|
|
230
|
+
"save-optional",
|
|
231
|
+
"save-peer",
|
|
232
|
+
"save-prod",
|
|
233
|
+
"save-bundle",
|
|
234
|
+
"global",
|
|
235
|
+
"legacy-peer-deps",
|
|
236
|
+
"strict-peer-deps",
|
|
237
|
+
"force",
|
|
238
|
+
"dry-run",
|
|
239
|
+
"ignore-scripts",
|
|
240
|
+
"audit",
|
|
241
|
+
"fund",
|
|
242
|
+
"prefer-offline",
|
|
243
|
+
"prefer-online",
|
|
244
|
+
"offline",
|
|
245
|
+
"foreground-scripts",
|
|
246
|
+
"include-workspace-root",
|
|
247
|
+
"workspaces",
|
|
248
|
+
"package-lock",
|
|
249
|
+
"package-lock-only",
|
|
250
|
+
"yes",
|
|
251
|
+
"quiet",
|
|
252
|
+
"silent",
|
|
253
|
+
"verbose",
|
|
254
|
+
"json",
|
|
255
|
+
"parseable",
|
|
256
|
+
"long",
|
|
257
|
+
"all",
|
|
258
|
+
"production",
|
|
259
|
+
"dev",
|
|
260
|
+
"bin-links",
|
|
261
|
+
"strict-ssl",
|
|
262
|
+
"update-notifier",
|
|
263
|
+
"global-style",
|
|
264
|
+
"legacy-bundling",
|
|
265
|
+
"rebuild-bundle",
|
|
266
|
+
"engine-strict",
|
|
267
|
+
"if-present",
|
|
268
|
+
"ignore-existing",
|
|
269
|
+
"timing",
|
|
270
|
+
"usage",
|
|
271
|
+
"version",
|
|
272
|
+
"versions",
|
|
273
|
+
"help",
|
|
274
|
+
"link",
|
|
275
|
+
"progress",
|
|
276
|
+
"unicode",
|
|
277
|
+
"install-links",
|
|
278
|
+
"provenance",
|
|
279
|
+
"sign-git-commit",
|
|
280
|
+
"sign-git-tag",
|
|
281
|
+
"git-tag-version",
|
|
282
|
+
"commit-hooks",
|
|
283
|
+
"shrinkwrap",
|
|
284
|
+
]);
|
|
285
|
+
/** npm's short flags that take a value; every other short flag is a boolean. */
|
|
286
|
+
const SHORT_VALUES = {
|
|
287
|
+
"-C": "prefix",
|
|
288
|
+
"-L": "location",
|
|
289
|
+
"-w": "workspace",
|
|
290
|
+
"-c": "call",
|
|
291
|
+
};
|
|
292
|
+
/** npx (not npm, where -p is --parseable) reads -p as --package. */
|
|
293
|
+
const NPX_SHORT_VALUES = {
|
|
294
|
+
...SHORT_VALUES,
|
|
295
|
+
"-p": "package",
|
|
296
|
+
};
|
|
297
|
+
const SHORT_BOOLEANS = {
|
|
298
|
+
"-g": "global",
|
|
299
|
+
"-D": "save-dev",
|
|
300
|
+
"-E": "save-exact",
|
|
301
|
+
"-O": "save-optional",
|
|
302
|
+
"-P": "save-prod",
|
|
303
|
+
"-S": "save",
|
|
304
|
+
"-B": "save-bundle",
|
|
305
|
+
"-f": "force",
|
|
306
|
+
"-y": "yes",
|
|
307
|
+
};
|
|
308
|
+
function readNpmArgs(words, npx) {
|
|
309
|
+
const flags = new Map();
|
|
310
|
+
const positional = [];
|
|
311
|
+
const shortValues = npx ? NPX_SHORT_VALUES : SHORT_VALUES;
|
|
312
|
+
const add = (key, value) => {
|
|
313
|
+
flags.set(key, [...(flags.get(key) ?? []), value]);
|
|
314
|
+
};
|
|
315
|
+
let ambiguous;
|
|
316
|
+
for (let i = 0; i < words.length; i++) {
|
|
317
|
+
const word = words[i] ?? "";
|
|
318
|
+
if (word === "--" || (npx && positional.length > 0)) {
|
|
319
|
+
// npx stops reading its own options at the command it runs.
|
|
320
|
+
positional.push(...words.slice(word === "--" ? i + 1 : i));
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
if (!word.startsWith("-") || word === "-") {
|
|
324
|
+
positional.push(word);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (word.startsWith("--")) {
|
|
328
|
+
const eq = word.indexOf("=");
|
|
329
|
+
const key = word.slice(2, eq === -1 ? undefined : eq);
|
|
330
|
+
const next = words[i + 1];
|
|
331
|
+
if (eq !== -1)
|
|
332
|
+
add(key, word.slice(eq + 1));
|
|
333
|
+
else if (key.startsWith("no-") || BOOLEAN_FLAGS.has(key))
|
|
334
|
+
add(key, "true");
|
|
335
|
+
else if (VALUE_FLAGS.has(key) || key.endsWith("registry"))
|
|
336
|
+
add(key, words[++i] ?? "");
|
|
337
|
+
else {
|
|
338
|
+
if (next !== undefined && !next.startsWith("-"))
|
|
339
|
+
ambiguous ??= word;
|
|
340
|
+
add(key, "true");
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const value = shortValues[word];
|
|
345
|
+
if (value !== undefined)
|
|
346
|
+
add(value, words[++i] ?? "");
|
|
347
|
+
else
|
|
348
|
+
add(SHORT_BOOLEANS[word] ?? word, "true");
|
|
349
|
+
}
|
|
350
|
+
return { flags, positional, ambiguous };
|
|
351
|
+
}
|
|
352
|
+
/** npm init foo → create-foo; @s → @s/create; @s/foo@1 → @s/create-foo@1 (npm's rule). */
|
|
353
|
+
function initializer(spec) {
|
|
354
|
+
const at = spec.indexOf("@", 1);
|
|
355
|
+
const [name, version] = at === -1 ? [spec, ""] : [spec.slice(0, at), spec.slice(at)];
|
|
356
|
+
if (name.startsWith("@")) {
|
|
357
|
+
const slash = name.indexOf("/");
|
|
358
|
+
return slash === -1
|
|
359
|
+
? `${name}/create${version}`
|
|
360
|
+
: `${name.slice(0, slash)}/create-${name.slice(slash + 1)}${version}`;
|
|
361
|
+
}
|
|
362
|
+
return `create-${name}${version}`;
|
|
363
|
+
}
|
|
364
|
+
function npmIntents(name, args, state, home, depth, raw) {
|
|
365
|
+
const unchecked = (reason) => [
|
|
366
|
+
{ kind: "unchecked", command: raw, reason },
|
|
367
|
+
];
|
|
368
|
+
const { flags, positional, ambiguous } = readNpmArgs(args, name === "npx");
|
|
369
|
+
if ([...flags.keys()].some((k) => k.endsWith("registry") ||
|
|
370
|
+
["replace-registry-host", "userconfig", "globalconfig"].includes(k)))
|
|
371
|
+
return unchecked("it points npm at a registry or config croak does not check");
|
|
372
|
+
const prefix = flags.get("prefix")?.at(-1);
|
|
373
|
+
const dir = prefix === undefined || state.dir === undefined
|
|
374
|
+
? state.dir
|
|
375
|
+
: posix.resolve(state.dir, prefix);
|
|
376
|
+
const call = flags.get("call")?.at(-1);
|
|
377
|
+
const where = (intents) => dir === undefined && intents.length > 0
|
|
378
|
+
? unchecked("the directory it runs in is only known when the shell runs")
|
|
379
|
+
: intents;
|
|
380
|
+
if (name === "npx") {
|
|
381
|
+
if (call !== undefined)
|
|
382
|
+
return intentsFor(call, { dir }, home, depth + 1);
|
|
383
|
+
const packages = flags.get("package") ?? positional.slice(0, 1);
|
|
384
|
+
return where(packages.length === 0 ? [] : [{ kind: "run", dir: dir ?? "", packages }]);
|
|
385
|
+
}
|
|
386
|
+
const [word, ...rest] = positional;
|
|
387
|
+
if (word === undefined)
|
|
388
|
+
return [];
|
|
389
|
+
const sub = npmCommand(word);
|
|
390
|
+
if (sub === "audit" && rest[0] === "fix")
|
|
391
|
+
return unchecked("croak does not check npm audit fix yet");
|
|
392
|
+
if (sub === undefined ||
|
|
393
|
+
!INSTALLING.has(sub) ||
|
|
394
|
+
((sub === "init" || sub === "link") && rest.length === 0)) {
|
|
395
|
+
// An unknown option may have swallowed the real subcommand (npm --proxy x install).
|
|
396
|
+
if (ambiguous !== undefined &&
|
|
397
|
+
rest.some((w) => INSTALLING.has(npmCommand(w) ?? "")))
|
|
398
|
+
return unchecked(`croak cannot tell whether ${ambiguous} takes a value`);
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
if (ambiguous !== undefined)
|
|
402
|
+
return unchecked(`croak cannot tell whether ${ambiguous} takes a value`);
|
|
403
|
+
const at = dir ?? "";
|
|
404
|
+
const global = flags.has("global") || flags.get("location")?.at(-1) === "global";
|
|
405
|
+
switch (sub) {
|
|
406
|
+
case "install":
|
|
407
|
+
case "install-test":
|
|
408
|
+
return where([{ kind: "install", dir: at, packages: rest, global }]);
|
|
409
|
+
case "ci":
|
|
410
|
+
case "install-ci-test":
|
|
411
|
+
return where([{ kind: "ci", dir: at }]);
|
|
412
|
+
case "update":
|
|
413
|
+
return global
|
|
414
|
+
? unchecked("croak does not check global updates yet")
|
|
415
|
+
: where([{ kind: "update", dir: at, packages: rest }]);
|
|
416
|
+
case "exec": {
|
|
417
|
+
if (call !== undefined)
|
|
418
|
+
return intentsFor(call, { dir }, home, depth + 1);
|
|
419
|
+
const packages = flags.get("package") ?? rest.slice(0, 1);
|
|
420
|
+
return where(packages.length === 0 ? [] : [{ kind: "run", dir: at, packages }]);
|
|
421
|
+
}
|
|
422
|
+
case "init":
|
|
423
|
+
return where([
|
|
424
|
+
{ kind: "run", dir: at, packages: [initializer(rest[0] ?? "")] },
|
|
425
|
+
]);
|
|
426
|
+
default:
|
|
427
|
+
return unchecked(`croak does not check npm ${sub} yet`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// ------------------------------------------------------------ pnpm commands
|
|
431
|
+
const PNPM_INSTALL = new Set(["install", "i", "add", "install-test", "it"]);
|
|
432
|
+
const PNPM_UPDATE = new Set(["update", "up", "upgrade"]);
|
|
433
|
+
/** Subcommands that install, or run another pnpm, in ways croak does not plan. */
|
|
434
|
+
const PNPM_UNCHECKED = new Set([
|
|
435
|
+
"dedupe",
|
|
436
|
+
"deploy",
|
|
437
|
+
"with",
|
|
438
|
+
"fetch",
|
|
439
|
+
"link",
|
|
440
|
+
"ln",
|
|
441
|
+
"import",
|
|
442
|
+
"patch",
|
|
443
|
+
"patch-commit",
|
|
444
|
+
"patch-remove",
|
|
445
|
+
"self-update",
|
|
446
|
+
]);
|
|
447
|
+
/** pnpm options that take a value. */
|
|
448
|
+
const PNPM_VALUE_FLAGS = new Set([
|
|
449
|
+
"-C",
|
|
450
|
+
"--dir",
|
|
451
|
+
"--filter",
|
|
452
|
+
"-F",
|
|
453
|
+
"--registry",
|
|
454
|
+
"--reporter",
|
|
455
|
+
"--loglevel",
|
|
456
|
+
"--store-dir",
|
|
457
|
+
"--virtual-store-dir",
|
|
458
|
+
"--package",
|
|
459
|
+
"--workspace-concurrency",
|
|
460
|
+
"--network-concurrency",
|
|
461
|
+
"--child-concurrency",
|
|
462
|
+
"--resolution-mode",
|
|
463
|
+
"--node-linker",
|
|
464
|
+
"--cache-dir",
|
|
465
|
+
]);
|
|
466
|
+
/** pnpm options that are flags. */
|
|
467
|
+
const PNPM_BOOLEAN_FLAGS = new Set([
|
|
468
|
+
"-D",
|
|
469
|
+
"-P",
|
|
470
|
+
"-O",
|
|
471
|
+
"-E",
|
|
472
|
+
"-g",
|
|
473
|
+
"-w",
|
|
474
|
+
"-r",
|
|
475
|
+
"--save-dev",
|
|
476
|
+
"--save-prod",
|
|
477
|
+
"--save-optional",
|
|
478
|
+
"--save-exact",
|
|
479
|
+
"--save-peer",
|
|
480
|
+
"--global",
|
|
481
|
+
"--workspace-root",
|
|
482
|
+
"--recursive",
|
|
483
|
+
"--workspace",
|
|
484
|
+
"--frozen-lockfile",
|
|
485
|
+
"--no-frozen-lockfile",
|
|
486
|
+
"--prefer-offline",
|
|
487
|
+
"--offline",
|
|
488
|
+
"--ignore-scripts",
|
|
489
|
+
"--lockfile-only",
|
|
490
|
+
"--fix-lockfile",
|
|
491
|
+
"--force",
|
|
492
|
+
"--silent",
|
|
493
|
+
"--latest",
|
|
494
|
+
"-L",
|
|
495
|
+
"--interactive",
|
|
496
|
+
"-i",
|
|
497
|
+
"--dev",
|
|
498
|
+
"--prod",
|
|
499
|
+
"--no-optional",
|
|
500
|
+
"--strict-peer-dependencies",
|
|
501
|
+
"--shamefully-hoist",
|
|
502
|
+
"--aggregate-output",
|
|
503
|
+
"--stream",
|
|
504
|
+
"--use-stderr",
|
|
505
|
+
"--yes",
|
|
506
|
+
"-y",
|
|
507
|
+
"--shell-mode",
|
|
508
|
+
"-c",
|
|
509
|
+
]);
|
|
510
|
+
function pnpmIntents(args, state, raw, dlx) {
|
|
511
|
+
const unchecked = (reason) => [
|
|
512
|
+
{ kind: "unchecked", command: raw, reason },
|
|
513
|
+
];
|
|
514
|
+
const flags = new Map();
|
|
515
|
+
const positional = [];
|
|
516
|
+
// An allowlist: pnpm has flags that allow every build, load a pnpmfile or
|
|
517
|
+
// move the lockfile; a flag croak does not know is not guessed at (M6 review).
|
|
518
|
+
let unknown;
|
|
519
|
+
for (let i = 0; i < args.length; i++) {
|
|
520
|
+
const word = args[i] ?? "";
|
|
521
|
+
if (word === "--" || (dlx && positional.length > 0)) {
|
|
522
|
+
positional.push(...args.slice(word === "--" ? i + 1 : i));
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
if (!word.startsWith("-") || word === "-") {
|
|
526
|
+
positional.push(word);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const eq = word.indexOf("=");
|
|
530
|
+
const key = eq === -1 ? word : word.slice(0, eq);
|
|
531
|
+
if (!PNPM_VALUE_FLAGS.has(key) && !PNPM_BOOLEAN_FLAGS.has(key))
|
|
532
|
+
unknown ??= word;
|
|
533
|
+
if (eq !== -1)
|
|
534
|
+
flags.set(key, [...(flags.get(key) ?? []), word.slice(eq + 1)]);
|
|
535
|
+
else if (PNPM_VALUE_FLAGS.has(key))
|
|
536
|
+
flags.set(key, [...(flags.get(key) ?? []), args[++i] ?? ""]);
|
|
537
|
+
else
|
|
538
|
+
flags.set(key, ["true"]);
|
|
539
|
+
}
|
|
540
|
+
if (flags.has("--registry"))
|
|
541
|
+
return unchecked("it points pnpm at a registry or config croak does not check");
|
|
542
|
+
const target = flags.get("-C")?.at(-1) ?? flags.get("--dir")?.at(-1);
|
|
543
|
+
const dir = target === undefined || state.dir === undefined
|
|
544
|
+
? state.dir
|
|
545
|
+
: posix.resolve(state.dir, target);
|
|
546
|
+
const where = (intents) => dir === undefined && intents.length > 0
|
|
547
|
+
? unchecked("the directory it runs in is only known when the shell runs")
|
|
548
|
+
: intents;
|
|
549
|
+
const global = flags.has("-g") || flags.has("--global");
|
|
550
|
+
if (dlx) {
|
|
551
|
+
if (unknown !== undefined)
|
|
552
|
+
return unchecked(`croak does not know pnpm's ${unknown}, so it cannot vouch for this command`);
|
|
553
|
+
const packages = flags.get("--package") ?? positional.slice(0, 1);
|
|
554
|
+
return where(packages.length === 0
|
|
555
|
+
? []
|
|
556
|
+
: [{ kind: "run", manager: "pnpm", dir: dir ?? "", packages }]);
|
|
557
|
+
}
|
|
558
|
+
const [sub, ...rest] = positional;
|
|
559
|
+
if (sub !== undefined && PNPM_UNCHECKED.has(sub))
|
|
560
|
+
return unchecked(`croak does not check pnpm ${sub} yet`);
|
|
561
|
+
const installing = sub === undefined ||
|
|
562
|
+
PNPM_INSTALL.has(sub) ||
|
|
563
|
+
PNPM_UPDATE.has(sub) ||
|
|
564
|
+
sub === "dlx" ||
|
|
565
|
+
sub === "create" ||
|
|
566
|
+
sub === "ci" ||
|
|
567
|
+
positional.some((w) => PNPM_INSTALL.has(w) || PNPM_UNCHECKED.has(w));
|
|
568
|
+
if (unknown !== undefined && installing)
|
|
569
|
+
return unchecked(`croak does not know pnpm's ${unknown}, so it cannot vouch for this install`);
|
|
570
|
+
if (sub === undefined)
|
|
571
|
+
return []; // bare pnpm prints its help
|
|
572
|
+
if (sub === "dlx")
|
|
573
|
+
return pnpmIntents(rest, { dir }, raw, true);
|
|
574
|
+
if (sub === "create")
|
|
575
|
+
return where(rest[0] === undefined
|
|
576
|
+
? []
|
|
577
|
+
: [
|
|
578
|
+
{
|
|
579
|
+
kind: "run",
|
|
580
|
+
manager: "pnpm",
|
|
581
|
+
dir: dir ?? "",
|
|
582
|
+
packages: [initializer(rest[0])],
|
|
583
|
+
},
|
|
584
|
+
]);
|
|
585
|
+
if (PNPM_UPDATE.has(sub))
|
|
586
|
+
return global
|
|
587
|
+
? unchecked("croak does not check global updates yet")
|
|
588
|
+
: where([
|
|
589
|
+
{ kind: "update", manager: "pnpm", dir: dir ?? "", packages: rest },
|
|
590
|
+
]);
|
|
591
|
+
if (sub === "ci" ||
|
|
592
|
+
(PNPM_INSTALL.has(sub) &&
|
|
593
|
+
flags.has("--frozen-lockfile") &&
|
|
594
|
+
rest.length === 0))
|
|
595
|
+
return where([{ kind: "ci", manager: "pnpm", dir: dir ?? "" }]);
|
|
596
|
+
if (!PNPM_INSTALL.has(sub))
|
|
597
|
+
return [];
|
|
598
|
+
return where([
|
|
599
|
+
{
|
|
600
|
+
kind: "install",
|
|
601
|
+
manager: "pnpm",
|
|
602
|
+
dir: dir ?? "",
|
|
603
|
+
packages: rest,
|
|
604
|
+
global,
|
|
605
|
+
},
|
|
606
|
+
]);
|
|
607
|
+
}
|
|
608
|
+
// ------------------------------------------------------------ shell commands
|
|
609
|
+
/** Wrappers and the options of theirs that take a value. */
|
|
610
|
+
const WRAPPERS = {
|
|
611
|
+
sudo: new Set([
|
|
612
|
+
"-u",
|
|
613
|
+
"-g",
|
|
614
|
+
"-p",
|
|
615
|
+
"-C",
|
|
616
|
+
"-h",
|
|
617
|
+
"-r",
|
|
618
|
+
"-t",
|
|
619
|
+
"-U",
|
|
620
|
+
"-D",
|
|
621
|
+
"-R",
|
|
622
|
+
"-T",
|
|
623
|
+
]),
|
|
624
|
+
doas: new Set(["-u", "-C"]),
|
|
625
|
+
nice: new Set(["-n"]),
|
|
626
|
+
ionice: new Set(["-c", "-n", "-p"]),
|
|
627
|
+
stdbuf: new Set(["-i", "-o", "-e"]),
|
|
628
|
+
timeout: new Set(["-s", "-k", "--signal", "--kill-after"]),
|
|
629
|
+
time: new Set(["-f", "-o"]),
|
|
630
|
+
exec: new Set(["-a"]),
|
|
631
|
+
env: new Set(["-u", "--unset"]),
|
|
632
|
+
command: new Set(),
|
|
633
|
+
builtin: new Set(),
|
|
634
|
+
nohup: new Set(),
|
|
635
|
+
setsid: new Set(),
|
|
636
|
+
caffeinate: new Set(["-t", "-w"]),
|
|
637
|
+
};
|
|
638
|
+
/** Shell keywords that start or end a compound command, not a command name. */
|
|
639
|
+
const KEYWORDS = new Set([
|
|
640
|
+
"{",
|
|
641
|
+
"}",
|
|
642
|
+
"!",
|
|
643
|
+
"if",
|
|
644
|
+
"then",
|
|
645
|
+
"elif",
|
|
646
|
+
"else",
|
|
647
|
+
"fi",
|
|
648
|
+
"do",
|
|
649
|
+
"done",
|
|
650
|
+
"while",
|
|
651
|
+
"until",
|
|
652
|
+
"for",
|
|
653
|
+
"case",
|
|
654
|
+
"esac",
|
|
655
|
+
"in",
|
|
656
|
+
"select",
|
|
657
|
+
"function",
|
|
658
|
+
]);
|
|
659
|
+
const SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash"]);
|
|
660
|
+
/** Commands that take a package manager's name as plain text, not to run it. */
|
|
661
|
+
const TEXT_ONLY = new Set([
|
|
662
|
+
"echo",
|
|
663
|
+
"printf",
|
|
664
|
+
"grep",
|
|
665
|
+
"egrep",
|
|
666
|
+
"fgrep",
|
|
667
|
+
"rg",
|
|
668
|
+
"ag",
|
|
669
|
+
"git",
|
|
670
|
+
"which",
|
|
671
|
+
"type",
|
|
672
|
+
"whereis",
|
|
673
|
+
"man",
|
|
674
|
+
"info",
|
|
675
|
+
"cat",
|
|
676
|
+
"less",
|
|
677
|
+
"more",
|
|
678
|
+
"head",
|
|
679
|
+
"tail",
|
|
680
|
+
"wc",
|
|
681
|
+
"sort",
|
|
682
|
+
"uniq",
|
|
683
|
+
"ls",
|
|
684
|
+
"test",
|
|
685
|
+
"[",
|
|
686
|
+
"printenv",
|
|
687
|
+
"alias",
|
|
688
|
+
"hash",
|
|
689
|
+
"sed",
|
|
690
|
+
"awk",
|
|
691
|
+
"jq",
|
|
692
|
+
"nvm",
|
|
693
|
+
"fnm",
|
|
694
|
+
"volta",
|
|
695
|
+
]);
|
|
696
|
+
const OTHER_MANAGERS = {
|
|
697
|
+
yarn: new Set(["", "install", "add", "up", "upgrade", "dlx", "create"]),
|
|
698
|
+
bun: new Set(["install", "i", "add", "update", "x", "create"]),
|
|
699
|
+
};
|
|
700
|
+
const OTHER_RUNNERS = new Set(["bunx", "corepack"]);
|
|
701
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
702
|
+
const basename = (word) => word.slice(word.lastIndexOf("/") + 1);
|
|
703
|
+
/** The command's real head and arguments, past keywords, assignments and wrappers. */
|
|
704
|
+
function resolve(words) {
|
|
705
|
+
let rest = words;
|
|
706
|
+
let foreignEnv = false;
|
|
707
|
+
const assignment = (word) => {
|
|
708
|
+
if (word === undefined || !ASSIGNMENT.test(word.text))
|
|
709
|
+
return false;
|
|
710
|
+
if (/^p?npm_config_/i.test(word.text))
|
|
711
|
+
foreignEnv = true;
|
|
712
|
+
return true;
|
|
713
|
+
};
|
|
714
|
+
for (;;) {
|
|
715
|
+
const first = rest[0];
|
|
716
|
+
if (first === undefined)
|
|
717
|
+
return { words: rest, foreignEnv, problem: undefined };
|
|
718
|
+
const name = basename(first.text);
|
|
719
|
+
const values = WRAPPERS[name];
|
|
720
|
+
if (KEYWORDS.has(first.text) ||
|
|
721
|
+
/^[\w.:-]+\(\)$/.test(first.text) ||
|
|
722
|
+
assignment(first)) {
|
|
723
|
+
rest = rest.slice(1);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (values === undefined)
|
|
727
|
+
return { words: rest, foreignEnv, problem: undefined };
|
|
728
|
+
rest = rest.slice(1);
|
|
729
|
+
while (rest[0]?.text.startsWith("-")) {
|
|
730
|
+
const flag = rest[0]?.text ?? "";
|
|
731
|
+
if (flag === "--") {
|
|
732
|
+
rest = rest.slice(1);
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
if (name === "env" &&
|
|
736
|
+
/^-(?:[a-zA-Z]*[SC]|-split-string|-chdir)/.test(flag))
|
|
737
|
+
return {
|
|
738
|
+
words: rest,
|
|
739
|
+
foreignEnv,
|
|
740
|
+
problem: "env changes how or where the command runs",
|
|
741
|
+
};
|
|
742
|
+
rest = rest.slice(values.has(flag) ? 2 : 1);
|
|
743
|
+
}
|
|
744
|
+
if (name === "timeout" && /^\d/.test(rest[0]?.text ?? ""))
|
|
745
|
+
rest = rest.slice(1);
|
|
746
|
+
if (name === "env")
|
|
747
|
+
while (assignment(rest[0]))
|
|
748
|
+
rest = rest.slice(1);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** The script a shell runs with -c (also -lc, -ec, -e -c). */
|
|
752
|
+
function shellScript(args) {
|
|
753
|
+
for (let i = 0; i < args.length; i++) {
|
|
754
|
+
const arg = args[i]?.text ?? "";
|
|
755
|
+
if (!arg.startsWith("-"))
|
|
756
|
+
return undefined;
|
|
757
|
+
if (/^-[a-zA-Z]*c[a-zA-Z]*$/.test(arg))
|
|
758
|
+
return args[i + 1];
|
|
759
|
+
}
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
function intentsOf(commands, state, home, depth, text) {
|
|
763
|
+
const found = [];
|
|
764
|
+
for (const command of commands) {
|
|
765
|
+
if (command.group !== undefined) {
|
|
766
|
+
// A subshell's cd does not leak out; a { …; } group's does. Either way,
|
|
767
|
+
// after a cd inside one, the outer directory is no longer certain.
|
|
768
|
+
const inner = { dir: state.dir };
|
|
769
|
+
found.push(...intentsOf(command.group, inner, home, depth, text));
|
|
770
|
+
if (inner.dir !== state.dir)
|
|
771
|
+
state.dir = undefined;
|
|
772
|
+
if (command.words.length === 0)
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const raw = command.words.map((w) => w.text).join(" ");
|
|
776
|
+
const unchecked = (reason) => {
|
|
777
|
+
found.push({ kind: "unchecked", command: raw, reason });
|
|
778
|
+
};
|
|
779
|
+
const { words, foreignEnv, problem } = resolve(command.words);
|
|
780
|
+
const [head, ...argWords] = words;
|
|
781
|
+
if (head === undefined)
|
|
782
|
+
continue;
|
|
783
|
+
if (problem !== undefined) {
|
|
784
|
+
if (MENTIONS.test(raw))
|
|
785
|
+
unchecked(problem);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (head.dynamic) {
|
|
789
|
+
// n=npm; $n install x — the command is only known when the shell runs.
|
|
790
|
+
if (MENTIONS.test(text))
|
|
791
|
+
unchecked("the command it runs is only known when the shell runs");
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const name = basename(head.text);
|
|
795
|
+
const args = argWords.map((w) => w.text);
|
|
796
|
+
if (name === "cd" || name === "pushd") {
|
|
797
|
+
const target = argWords[0];
|
|
798
|
+
state.dir =
|
|
799
|
+
target === undefined
|
|
800
|
+
? home
|
|
801
|
+
: target.dynamic || state.dir === undefined || target.text === "-"
|
|
802
|
+
? undefined
|
|
803
|
+
: posix.resolve(state.dir, target.text.replace(/^~(?=\/|$)/, home));
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (SHELLS.has(name)) {
|
|
807
|
+
const script = shellScript(argWords);
|
|
808
|
+
if (script === undefined) {
|
|
809
|
+
// A script from stdin (a pipe) that mentions a package manager: croak
|
|
810
|
+
// does not see it. A script file is like `npm run x`: a documented gap.
|
|
811
|
+
const fromStdin = !argWords.some((w) => !w.text.startsWith("-"));
|
|
812
|
+
if (fromStdin && MENTIONS.test(text))
|
|
813
|
+
unchecked(`${name} runs a script from stdin croak cannot see`);
|
|
814
|
+
}
|
|
815
|
+
else if (script.dynamic) {
|
|
816
|
+
if (MENTIONS.test(script.text))
|
|
817
|
+
unchecked("the script it runs is only known when the shell runs");
|
|
818
|
+
}
|
|
819
|
+
else
|
|
820
|
+
found.push(...intentsFor(script.text, state, home, depth + 1));
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (name === "eval" || name === "source" || name === ".") {
|
|
824
|
+
if (MENTIONS.test(raw))
|
|
825
|
+
unchecked(`${name} hides what it runs`);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (name === "npm" || name === "npx") {
|
|
829
|
+
if (foreignEnv) {
|
|
830
|
+
unchecked("npm settings are changed through the environment");
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const intents = npmIntents(name, args, state, home, depth, raw);
|
|
834
|
+
if (intents.some((i) => i.kind !== "unchecked") &&
|
|
835
|
+
words.some((w) => w.dynamic))
|
|
836
|
+
unchecked("a package or path in it is only known when the shell runs");
|
|
837
|
+
else
|
|
838
|
+
found.push(...intents);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (name === "pnpm" || name === "pnpx") {
|
|
842
|
+
if (foreignEnv) {
|
|
843
|
+
unchecked("pnpm settings are changed through the environment");
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const intents = pnpmIntents(args, state, raw, name === "pnpx");
|
|
847
|
+
if (intents.some((i) => i.kind !== "unchecked") &&
|
|
848
|
+
words.some((w) => w.dynamic))
|
|
849
|
+
unchecked("a package or path in it is only known when the shell runs");
|
|
850
|
+
else
|
|
851
|
+
found.push(...intents);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const other = OTHER_MANAGERS[name];
|
|
855
|
+
if (OTHER_RUNNERS.has(name) ||
|
|
856
|
+
other?.has(args.find((a) => !a.startsWith("-")) ?? "") === true) {
|
|
857
|
+
unchecked(`croak checks npm and pnpm; ${name} is not supported yet`);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
// xargs npm i, find -exec npm i, watch npm i, docker run … npm i: a
|
|
861
|
+
// package manager run by another command, with arguments croak cannot follow.
|
|
862
|
+
if (!TEXT_ONLY.has(name) &&
|
|
863
|
+
argWords.some((w) => PACKAGE_MANAGERS.has(basename(w.text))))
|
|
864
|
+
unchecked(`it runs a package manager through ${name}`);
|
|
865
|
+
}
|
|
866
|
+
return found;
|
|
867
|
+
}
|
|
868
|
+
/** Heredoc bodies are data for most commands, and a script croak cannot see for a shell. */
|
|
869
|
+
function stripHeredocs(command) {
|
|
870
|
+
const lines = command.split("\n");
|
|
871
|
+
const out = [];
|
|
872
|
+
let shellHeredoc = false;
|
|
873
|
+
for (let i = 0; i < lines.length; i++) {
|
|
874
|
+
const line = lines[i] ?? "";
|
|
875
|
+
out.push(line);
|
|
876
|
+
const m = /<<(-?)\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\2/.exec(line);
|
|
877
|
+
if (m === null)
|
|
878
|
+
continue;
|
|
879
|
+
const body = [];
|
|
880
|
+
while (++i < lines.length) {
|
|
881
|
+
const next = lines[i] ?? "";
|
|
882
|
+
if ((m[1] === "-" ? next.trim() : next) === m[3])
|
|
883
|
+
break;
|
|
884
|
+
body.push(next);
|
|
885
|
+
}
|
|
886
|
+
if (/\b(?:sh|bash|zsh|dash|ksh)\b[^<]*<</.test(line) &&
|
|
887
|
+
MENTIONS.test(body.join("\n")))
|
|
888
|
+
shellHeredoc = true;
|
|
889
|
+
}
|
|
890
|
+
return { text: out.join("\n"), shellHeredoc };
|
|
891
|
+
}
|
|
892
|
+
/** Settings in the same command that move npm to another registry. */
|
|
893
|
+
const CONFIG_CHANGE = /\b(?:npm|pnpm)\s+(?:config\s+)?(?:set|edit)\b|\bexport\s+(?:npm|pnpm)_config_|>>?\s*\S*(?:\.npmrc|pnpm-workspace\.yaml)\b/i;
|
|
894
|
+
function intentsFor(command, state, home, depth) {
|
|
895
|
+
const { text, shellHeredoc } = stripHeredocs(command);
|
|
896
|
+
const commands = depth > MAX_DEPTH ? undefined : shellCommands(text, MAX_COMMAND_LENGTH);
|
|
897
|
+
if (commands === undefined)
|
|
898
|
+
return MENTIONS.test(command)
|
|
899
|
+
? [
|
|
900
|
+
{
|
|
901
|
+
kind: "unchecked",
|
|
902
|
+
command: command.slice(0, 200),
|
|
903
|
+
reason: "croak could not parse the command",
|
|
904
|
+
},
|
|
905
|
+
]
|
|
906
|
+
: [];
|
|
907
|
+
const found = intentsOf(commands, state, home, depth, text);
|
|
908
|
+
if (shellHeredoc)
|
|
909
|
+
found.push({
|
|
910
|
+
kind: "unchecked",
|
|
911
|
+
command: command.slice(0, 200),
|
|
912
|
+
reason: "a shell runs a heredoc croak does not check",
|
|
913
|
+
});
|
|
914
|
+
if (found.some((i) => i.kind !== "unchecked") && CONFIG_CHANGE.test(text))
|
|
915
|
+
return [
|
|
916
|
+
{
|
|
917
|
+
kind: "unchecked",
|
|
918
|
+
command: command.slice(0, 200),
|
|
919
|
+
reason: "it changes npm's registry settings before installing",
|
|
920
|
+
},
|
|
921
|
+
];
|
|
922
|
+
return found;
|
|
923
|
+
}
|
|
924
|
+
/** Install intents in `command`, run from `cwd` (absolute) by a user whose home is `home`. */
|
|
925
|
+
export function installIntents(command, cwd, home) {
|
|
926
|
+
return intentsFor(command, { dir: cwd }, home, 0);
|
|
927
|
+
}
|