@unscientificjszhai/howto 1.0.0 → 1.0.2
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.CN.md +74 -8
- package/README.md +74 -8
- package/THIRD_PARTY_NOTICES.md +6 -4
- package/dist/ai/command-schema.js +7 -1
- package/dist/ai/errors.js +7 -23
- package/dist/ai/gemini.js +67 -27
- package/dist/ai/openai.js +35 -27
- package/dist/cli.js +12 -1
- package/dist/config-file.js +97 -51
- package/dist/config.js +8 -7
- package/dist/errors.js +19 -17
- package/dist/execute.js +12 -34
- package/dist/index.js +100 -98
- package/dist/init/InitializationApp.js +177 -32
- package/dist/init/index.js +35 -33
- package/dist/init/state.js +46 -23
- package/dist/prompt.js +24 -6
- package/dist/safety/command-risk.js +11 -0
- package/dist/safety/dangerous-command.js +470 -92
- package/dist/shell/command-analysis.js +373 -0
- package/dist/shell/execution-environment.js +25 -0
- package/dist/shell/runtime-environment.js +61 -0
- package/dist/terminal-text.js +33 -0
- package/dist/ui/App.js +67 -28
- package/dist/ui/ConfirmView.js +163 -67
- package/dist/ui/InteractiveSessionProvider.js +19 -0
- package/dist/ui/ResolvePlaceholdersView.js +107 -27
- package/dist/ui/SelectCommandView.js +73 -23
- package/dist/ui/SelectedCommandDisplay.js +11 -9
- package/dist/ui/interactive-session.js +476 -0
- package/dist/ui/paste-framing.js +105 -0
- package/dist/ui/placeholder-logic.js +28 -16
- package/dist/ui/resize-safe-output.js +73 -0
- package/dist/ui/run-interactive-command.js +55 -0
- package/dist/ui/single-line-preview.js +28 -0
- package/dist/ui/text-input.js +53 -0
- package/dist/ui/use-keyboard-input.js +22 -0
- package/dist/ui/use-paste-aware-input.js +11 -0
- package/dist/user-visible-error.js +19 -0
- package/dist/validation/ai-response.js +34 -6
- package/dist/validation/command-tool.js +31 -132
- package/dist/validation/generated-commands.js +5 -16
- package/dist/version.js +38 -0
- package/package.json +31 -16
|
@@ -1,104 +1,482 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
]
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { isShellExecutable, parseShellCommand, resolveCommandPrefix, } from "../shell/command-analysis.js";
|
|
3
|
+
const INDETERMINATE = {
|
|
4
|
+
rule: "indeterminate-shell-command",
|
|
5
|
+
reason: "command structure cannot be assessed locally; additional confirmation required",
|
|
6
|
+
};
|
|
7
|
+
const DESTRUCTIVE_RM = {
|
|
8
|
+
rule: "destructive-rm",
|
|
9
|
+
reason: "recursive or forced rm against a high-risk target",
|
|
10
|
+
};
|
|
11
|
+
const DISK_OPERATION = {
|
|
12
|
+
rule: "disk-filesystem-operation",
|
|
13
|
+
reason: "disk, partition, filesystem, or raw block-device operation",
|
|
14
|
+
};
|
|
15
|
+
const PERMISSION_CHANGE = {
|
|
16
|
+
rule: "recursive-permission-ownership-change",
|
|
17
|
+
reason: "recursive chmod or chown against a high-risk target",
|
|
18
|
+
};
|
|
19
|
+
const DOWNLOAD_EXECUTION = {
|
|
20
|
+
rule: "download-and-execute",
|
|
21
|
+
reason: "network download piped or redirected directly into a shell",
|
|
22
|
+
};
|
|
23
|
+
const PACKAGE_OPERATION = {
|
|
24
|
+
rule: "package-manager-high-impact-operation",
|
|
25
|
+
reason: "package manager upgrade, uninstall, or global install operation",
|
|
26
|
+
};
|
|
27
|
+
const SERVICE_OPERATION = {
|
|
28
|
+
rule: "system-service-high-impact-operation",
|
|
29
|
+
reason: "system service stop, disable, restart, or configuration operation",
|
|
30
|
+
};
|
|
31
|
+
const RISKY_TARGET = /^(?:\/|~(?:\/|$)|\*|\.\.(?:\/|$))/;
|
|
32
|
+
const RM_FLAGS = /^(?:-[A-Za-z]*[rRfF][A-Za-z]*|--recursive|--force)$/;
|
|
33
|
+
const RECURSIVE_FLAGS = /^(?:-[A-Za-z]*R[A-Za-z]*|--recursive)$/;
|
|
34
|
+
const PACKAGE_MANAGERS = new Set(["apt", "apt-get", "yum", "dnf", "brew", "npm", "pip", "pip3"]);
|
|
35
|
+
const PACKAGE_ACTIONS = new Set([
|
|
36
|
+
"dist-upgrade",
|
|
37
|
+
"full-upgrade",
|
|
38
|
+
"upgrade",
|
|
39
|
+
"remove",
|
|
40
|
+
"purge",
|
|
41
|
+
"autoremove",
|
|
42
|
+
]);
|
|
43
|
+
// update 等动作在不同管理器中含义不同,只在对应工具内归一化。
|
|
44
|
+
const PACKAGE_ACTION_ALIASES = new Map([
|
|
45
|
+
[
|
|
46
|
+
"brew",
|
|
47
|
+
new Map([
|
|
48
|
+
["remove", "uninstall"],
|
|
49
|
+
["rm", "uninstall"],
|
|
50
|
+
["uninstal", "uninstall"],
|
|
51
|
+
]),
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"yum",
|
|
55
|
+
new Map([
|
|
56
|
+
["update", "upgrade"],
|
|
57
|
+
["update-to", "upgrade"],
|
|
58
|
+
["upgrade-to", "upgrade"],
|
|
59
|
+
["localupdate", "upgrade"],
|
|
60
|
+
["erase", "remove"],
|
|
61
|
+
]),
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"dnf",
|
|
65
|
+
new Map([
|
|
66
|
+
["up", "upgrade"],
|
|
67
|
+
["update", "upgrade"],
|
|
68
|
+
["upgrade-to", "upgrade"],
|
|
69
|
+
["update-to", "upgrade"],
|
|
70
|
+
["localupdate", "upgrade"],
|
|
71
|
+
["rm", "remove"],
|
|
72
|
+
["erase", "remove"],
|
|
73
|
+
["remove-n", "remove"],
|
|
74
|
+
["remove-na", "remove"],
|
|
75
|
+
["remove-nevra", "remove"],
|
|
76
|
+
["erase-n", "remove"],
|
|
77
|
+
["erase-na", "remove"],
|
|
78
|
+
["erase-nevra", "remove"],
|
|
79
|
+
]),
|
|
80
|
+
],
|
|
81
|
+
]);
|
|
82
|
+
const SERVICE_ACTIONS = new Set([
|
|
83
|
+
"start",
|
|
84
|
+
"stop",
|
|
85
|
+
"enable",
|
|
86
|
+
"disable",
|
|
87
|
+
"restart",
|
|
88
|
+
"reload",
|
|
89
|
+
"unload",
|
|
90
|
+
"bootout",
|
|
91
|
+
"remove",
|
|
92
|
+
]);
|
|
93
|
+
const PACKAGE_ZERO_OPTIONS = new Set([
|
|
94
|
+
"-g",
|
|
95
|
+
"--global",
|
|
96
|
+
"--user",
|
|
97
|
+
"--break-system-packages",
|
|
98
|
+
"-y",
|
|
99
|
+
"--yes",
|
|
100
|
+
"-q",
|
|
101
|
+
"--quiet",
|
|
102
|
+
"--verbose",
|
|
103
|
+
]);
|
|
104
|
+
const SERVICE_ZERO_OPTIONS = new Set(["--user", "--system"]);
|
|
105
|
+
const LINUX_PACKAGE_ACTIONS = new Map([
|
|
106
|
+
["apk", new Set(["add", "del", "fix", "upgrade"])],
|
|
107
|
+
[
|
|
108
|
+
"zypper",
|
|
109
|
+
new Set([
|
|
110
|
+
"install",
|
|
111
|
+
"in",
|
|
112
|
+
"remove",
|
|
113
|
+
"rm",
|
|
114
|
+
"update",
|
|
115
|
+
"up",
|
|
116
|
+
"dist-upgrade",
|
|
117
|
+
"dup",
|
|
118
|
+
"patch",
|
|
119
|
+
"verify",
|
|
120
|
+
"ve",
|
|
121
|
+
"install-new-recommends",
|
|
122
|
+
"inr",
|
|
123
|
+
]),
|
|
124
|
+
],
|
|
125
|
+
]);
|
|
126
|
+
const LINUX_PACKAGE_QUERIES = new Map([
|
|
127
|
+
["apk", new Set(["search", "info", "list", "policy", "version", "stats", "audit", "update"])],
|
|
128
|
+
[
|
|
129
|
+
"zypper",
|
|
130
|
+
new Set([
|
|
131
|
+
"search",
|
|
132
|
+
"se",
|
|
133
|
+
"info",
|
|
134
|
+
"if",
|
|
135
|
+
"list-updates",
|
|
136
|
+
"lu",
|
|
137
|
+
"list-patches",
|
|
138
|
+
"lp",
|
|
139
|
+
"packages",
|
|
140
|
+
"pa",
|
|
141
|
+
"repos",
|
|
142
|
+
"lr",
|
|
143
|
+
"refresh",
|
|
144
|
+
"ref",
|
|
145
|
+
"help",
|
|
146
|
+
]),
|
|
147
|
+
],
|
|
148
|
+
]);
|
|
149
|
+
const LINUX_PACKAGE_ZERO_OPTIONS = new Map([
|
|
150
|
+
["apk", new Set(["-q", "--quiet", "-v", "--verbose", "--no-cache", "--no-progress"])],
|
|
151
|
+
[
|
|
152
|
+
"zypper",
|
|
153
|
+
new Set(["-q", "--quiet", "-v", "--verbose", "-n", "--non-interactive", "--no-refresh"]),
|
|
154
|
+
],
|
|
155
|
+
]);
|
|
156
|
+
const PACMAN_LONG_OPTIONS = new Map([
|
|
157
|
+
["--query", "Q"],
|
|
158
|
+
["--files", "F"],
|
|
159
|
+
["--deptest", "T"],
|
|
160
|
+
["--remove", "R"],
|
|
161
|
+
["--sync", "S"],
|
|
162
|
+
["--upgrade", "U"],
|
|
163
|
+
["--database", "D"],
|
|
164
|
+
["--help", "h"],
|
|
165
|
+
["--version", "V"],
|
|
166
|
+
["--search", "s"],
|
|
167
|
+
["--info", "i"],
|
|
168
|
+
["--list", "l"],
|
|
169
|
+
["--groups", "g"],
|
|
170
|
+
["--quiet", "q"],
|
|
171
|
+
["--sysupgrade", "u"],
|
|
172
|
+
["--refresh", "y"],
|
|
173
|
+
// 不复用其他操作的查询短选项语义。
|
|
174
|
+
["--recursive", "recursive"],
|
|
175
|
+
["--nosave", "nosave"],
|
|
176
|
+
["--clean", "clean"],
|
|
177
|
+
["--print", "p"],
|
|
178
|
+
["--noconfirm", ""],
|
|
179
|
+
["--confirm", ""],
|
|
180
|
+
]);
|
|
181
|
+
const PACMAN_OPERATIONS = new Set("DFQRSTUVh");
|
|
182
|
+
const PACMAN_SHORT_OPTIONS = new Set("DFQRSTUVhcdegiklmnopqstuvwyx");
|
|
183
|
+
const RC_SERVICE_ZERO_OPTIONS = new Set([
|
|
184
|
+
"-c",
|
|
185
|
+
"--ifcrashed",
|
|
186
|
+
"-d",
|
|
187
|
+
"--debug",
|
|
188
|
+
"-D",
|
|
189
|
+
"--nodeps",
|
|
190
|
+
"-i",
|
|
191
|
+
"--ifexists",
|
|
192
|
+
"-I",
|
|
193
|
+
"--ifinactive",
|
|
194
|
+
"-N",
|
|
195
|
+
"--ifnotstarted",
|
|
196
|
+
"-s",
|
|
197
|
+
"--ifstarted",
|
|
198
|
+
"-S",
|
|
199
|
+
"--ifstopped",
|
|
200
|
+
"-q",
|
|
201
|
+
"--quiet",
|
|
202
|
+
"-v",
|
|
203
|
+
"--verbose",
|
|
204
|
+
]);
|
|
205
|
+
const RC_UPDATE_ZERO_OPTIONS = new Set(["-s", "--stack", "-a", "--all", "-v", "--verbose"]);
|
|
206
|
+
const NPM_ACTION_ALIASES = new Map([
|
|
207
|
+
["install", "install"],
|
|
208
|
+
["add", "install"],
|
|
209
|
+
["i", "install"],
|
|
210
|
+
["in", "install"],
|
|
211
|
+
["ins", "install"],
|
|
212
|
+
["inst", "install"],
|
|
213
|
+
["insta", "install"],
|
|
214
|
+
["instal", "install"],
|
|
215
|
+
["isnt", "install"],
|
|
216
|
+
["isnta", "install"],
|
|
217
|
+
["isntal", "install"],
|
|
218
|
+
["isntall", "install"],
|
|
219
|
+
["install-test", "install"],
|
|
220
|
+
["installTest", "install"],
|
|
221
|
+
["it", "install"],
|
|
222
|
+
["uninstall", "uninstall"],
|
|
223
|
+
["unlink", "uninstall"],
|
|
224
|
+
["remove", "uninstall"],
|
|
225
|
+
["rm", "uninstall"],
|
|
226
|
+
["r", "uninstall"],
|
|
227
|
+
["un", "uninstall"],
|
|
228
|
+
]);
|
|
62
229
|
export function detectDangerousCommand(command) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
230
|
+
return inspectCommand(command, false);
|
|
231
|
+
}
|
|
232
|
+
export function isDangerousCommand(command) {
|
|
233
|
+
return detectDangerousCommand(command) !== undefined;
|
|
234
|
+
}
|
|
235
|
+
function inspectCommand(command, insideShell) {
|
|
236
|
+
const parsed = parseShellCommand(command);
|
|
237
|
+
if (parsed.kind === "unsupported")
|
|
238
|
+
return INDETERMINATE;
|
|
239
|
+
const resolved = parsed.commands.map((segment) => resolveCommandPrefix(segment.words));
|
|
240
|
+
let uncertain = false;
|
|
241
|
+
for (let index = 0; index < resolved.length; index += 1) {
|
|
242
|
+
const current = resolved[index];
|
|
243
|
+
if (current.kind === "unsupported") {
|
|
244
|
+
uncertain = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const name = posix.basename(current.executable.value);
|
|
248
|
+
const next = resolved[index + 1];
|
|
249
|
+
if ((name === "curl" || name === "wget") &&
|
|
250
|
+
parsed.commands[index].separator === "|" &&
|
|
251
|
+
next?.kind === "parsed" &&
|
|
252
|
+
isShellExecutable(next.executable.value))
|
|
253
|
+
return DOWNLOAD_EXECUTION;
|
|
254
|
+
const match = isShellExecutable(current.executable.value)
|
|
255
|
+
? inspectShellBody(name, current.args, insideShell)
|
|
256
|
+
: inspectSimpleCommand(name, current.args);
|
|
257
|
+
if (match?.rule === INDETERMINATE.rule)
|
|
258
|
+
uncertain = true;
|
|
259
|
+
else if (match !== undefined)
|
|
260
|
+
return match;
|
|
261
|
+
}
|
|
262
|
+
return uncertain ? INDETERMINATE : undefined;
|
|
263
|
+
}
|
|
264
|
+
function inspectShellBody(shell, args, insideShell) {
|
|
265
|
+
if (insideShell ||
|
|
266
|
+
!["sh", "bash", "zsh"].includes(shell) ||
|
|
267
|
+
args[0]?.hasExpansion ||
|
|
268
|
+
(args[0]?.value !== "-c" && args[0]?.value !== "-lc") ||
|
|
269
|
+
args[1] === undefined ||
|
|
270
|
+
args[1].hasExpansion)
|
|
271
|
+
return INDETERMINATE;
|
|
272
|
+
// 只展开一层已是字面量的 shell 命令体;不模拟变量或嵌套解释器。
|
|
273
|
+
return inspectCommand(args[1].value, true);
|
|
274
|
+
}
|
|
275
|
+
function inspectSimpleCommand(name, args) {
|
|
276
|
+
// BusyBox 根据首个参数再分派工具;未分析 applet 前不能将其判为普通安全命令。
|
|
277
|
+
if (name === "busybox")
|
|
278
|
+
return INDETERMINATE;
|
|
279
|
+
if (name === "apk" || name === "zypper")
|
|
280
|
+
return inspectLinuxPackageCommand(name, args);
|
|
281
|
+
if (name === "pacman")
|
|
282
|
+
return inspectPacmanCommand(args);
|
|
283
|
+
if (name === "rc-service" || name === "rc-update")
|
|
284
|
+
return inspectOpenRcCommand(name, args);
|
|
285
|
+
const values = args.map((word) => word.value);
|
|
286
|
+
const dynamic = args.some((word) => word.hasExpansion);
|
|
287
|
+
if (name === "rm" || name === "chmod" || name === "chown") {
|
|
288
|
+
const flagPattern = name === "rm" ? RM_FLAGS : RECURSIVE_FLAGS;
|
|
289
|
+
const endOptions = values.indexOf("--");
|
|
290
|
+
const flags = endOptions < 0 ? values : values.slice(0, endOptions);
|
|
291
|
+
if (flags.some((value) => flagPattern.test(value)) &&
|
|
292
|
+
values.some((value) => RISKY_TARGET.test(normalizeRiskPath(value)))) {
|
|
293
|
+
return name === "rm" ? DESTRUCTIVE_RM : PERMISSION_CHANGE;
|
|
73
294
|
}
|
|
295
|
+
if (dynamic ||
|
|
296
|
+
flags.some((value) => value.startsWith("--") && !["--recursive", "--force", "--verbose"].includes(value)))
|
|
297
|
+
return INDETERMINATE;
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
if (/^mkfs(?:\.[\w-]+)?$/.test(name) || name === "fdisk" || name === "parted")
|
|
301
|
+
return DISK_OPERATION;
|
|
302
|
+
if (name === "dd") {
|
|
303
|
+
if (values.some((value) => {
|
|
304
|
+
if (!value.startsWith("of="))
|
|
305
|
+
return false;
|
|
306
|
+
const target = normalizeRiskPath(value.slice(3));
|
|
307
|
+
return target === "/dev" || target.startsWith("/dev/");
|
|
308
|
+
}))
|
|
309
|
+
return DISK_OPERATION;
|
|
310
|
+
return dynamic ? INDETERMINATE : undefined;
|
|
311
|
+
}
|
|
312
|
+
if (name === "diskutil") {
|
|
313
|
+
if (args[0]?.hasExpansion || values[0]?.startsWith("-"))
|
|
314
|
+
return INDETERMINATE;
|
|
315
|
+
return /^erase\w*$/.test(values[0] ?? "") ? DISK_OPERATION : undefined;
|
|
316
|
+
}
|
|
317
|
+
if (PACKAGE_MANAGERS.has(name))
|
|
318
|
+
return inspectPackageCommand(name, args);
|
|
319
|
+
if (name === "service" || name === "systemctl" || name === "launchctl") {
|
|
320
|
+
const action = name === "service" ? serviceAction(args) : leadingAction(args, SERVICE_ZERO_OPTIONS);
|
|
321
|
+
if (action === null)
|
|
322
|
+
return INDETERMINATE;
|
|
323
|
+
return action !== undefined && SERVICE_ACTIONS.has(action) ? SERVICE_OPERATION : undefined;
|
|
74
324
|
}
|
|
75
325
|
return undefined;
|
|
76
326
|
}
|
|
77
|
-
|
|
78
|
-
|
|
327
|
+
function normalizeRiskPath(value) {
|
|
328
|
+
// 只整理分析副本的分隔符和点段;保留 ..,避免忽略符号链接的实际解析语义。
|
|
329
|
+
const segments = value.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
330
|
+
return (value.startsWith("/") ? "/" : "") + segments.join("/");
|
|
331
|
+
}
|
|
332
|
+
function inspectPackageCommand(name, args) {
|
|
333
|
+
const parsedAction = leadingAction(args, PACKAGE_ZERO_OPTIONS);
|
|
334
|
+
if (parsedAction === null)
|
|
335
|
+
return INDETERMINATE;
|
|
336
|
+
if (parsedAction === undefined)
|
|
337
|
+
return undefined;
|
|
338
|
+
const action = name === "npm"
|
|
339
|
+
? resolveNpmAction(parsedAction)
|
|
340
|
+
: (PACKAGE_ACTION_ALIASES.get(name)?.get(parsedAction) ?? parsedAction);
|
|
341
|
+
if (action === null)
|
|
342
|
+
return INDETERMINATE;
|
|
343
|
+
if (name === "brew")
|
|
344
|
+
return ["upgrade", "uninstall"].includes(action) ? PACKAGE_OPERATION : undefined;
|
|
345
|
+
if (name !== "npm" && name !== "pip" && name !== "pip3") {
|
|
346
|
+
return PACKAGE_ACTIONS.has(action) ? PACKAGE_OPERATION : undefined;
|
|
347
|
+
}
|
|
348
|
+
if (!["install", "uninstall", "remove", "rm"].includes(action))
|
|
349
|
+
return undefined;
|
|
350
|
+
const globalFlags = name === "npm" ? ["-g", "--global"] : ["-g", "--user", "--break-system-packages"];
|
|
351
|
+
if (args.some((word) => globalFlags.includes(word.value)))
|
|
352
|
+
return PACKAGE_OPERATION;
|
|
353
|
+
return args.some((word) => word.hasExpansion ||
|
|
354
|
+
(word.value.startsWith("-") && word.value !== "--" && !PACKAGE_ZERO_OPTIONS.has(word.value)))
|
|
355
|
+
? INDETERMINATE
|
|
356
|
+
: undefined;
|
|
79
357
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
358
|
+
function resolveNpmAction(action) {
|
|
359
|
+
const canonical = NPM_ACTION_ALIASES.get(action);
|
|
360
|
+
if (canonical !== undefined)
|
|
361
|
+
return canonical;
|
|
362
|
+
// npm 也接受唯一前缀缩写;不复制完整命令表,相关前缀统一要求额外确认。
|
|
363
|
+
for (const knownAction of NPM_ACTION_ALIASES.keys()) {
|
|
364
|
+
if (knownAction.startsWith(action))
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
return action;
|
|
368
|
+
}
|
|
369
|
+
function inspectLinuxPackageCommand(name, args) {
|
|
370
|
+
const zeroOptions = LINUX_PACKAGE_ZERO_OPTIONS.get(name) ?? new Set();
|
|
371
|
+
const action = leadingAction(args, zeroOptions);
|
|
372
|
+
if (action === null)
|
|
373
|
+
return INDETERMINATE;
|
|
374
|
+
if (action === undefined)
|
|
375
|
+
return undefined;
|
|
376
|
+
if (LINUX_PACKAGE_ACTIONS.get(name)?.has(action))
|
|
377
|
+
return PACKAGE_OPERATION;
|
|
378
|
+
if (!LINUX_PACKAGE_QUERIES.get(name)?.has(action))
|
|
379
|
+
return INDETERMINATE;
|
|
380
|
+
// 查询也可能通过选项写文件(如 zypper repos --export),仅放行已知无参数选项。
|
|
381
|
+
return args.some((word) => word.hasExpansion ||
|
|
382
|
+
(word.value.startsWith("-") && word.value !== "--" && !zeroOptions.has(word.value)))
|
|
383
|
+
? INDETERMINATE
|
|
384
|
+
: undefined;
|
|
82
385
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
386
|
+
function inspectPacmanCommand(args) {
|
|
387
|
+
const flags = [];
|
|
388
|
+
let optionsEnded = false;
|
|
389
|
+
for (const word of args) {
|
|
390
|
+
if (word.hasExpansion)
|
|
391
|
+
return INDETERMINATE;
|
|
392
|
+
const value = word.value;
|
|
393
|
+
if (optionsEnded)
|
|
394
|
+
continue;
|
|
395
|
+
if (value === "--") {
|
|
396
|
+
optionsEnded = true;
|
|
397
|
+
}
|
|
398
|
+
else if (value.startsWith("--")) {
|
|
399
|
+
const flag = PACMAN_LONG_OPTIONS.get(value);
|
|
400
|
+
// 不猜测未知选项的参数个数,防止把 --config 等选项的值误当查询开关。
|
|
401
|
+
if (flag === undefined)
|
|
402
|
+
return INDETERMINATE;
|
|
403
|
+
if (flag !== "")
|
|
404
|
+
flags.push(flag);
|
|
92
405
|
}
|
|
406
|
+
else if (value.startsWith("-") && value !== "-") {
|
|
407
|
+
const shortFlags = [...value.slice(1)];
|
|
408
|
+
if (shortFlags.some((flag) => !PACMAN_SHORT_OPTIONS.has(flag)))
|
|
409
|
+
return INDETERMINATE;
|
|
410
|
+
flags.push(...shortFlags);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const operations = flags.filter((flag) => PACMAN_OPERATIONS.has(flag));
|
|
414
|
+
if (operations.length !== 1)
|
|
415
|
+
return INDETERMINATE;
|
|
416
|
+
const operation = operations[0];
|
|
417
|
+
const options = flags.filter((flag) => !PACMAN_OPERATIONS.has(flag));
|
|
418
|
+
if (operation === "R" || operation === "U" || operation === "D")
|
|
419
|
+
return PACKAGE_OPERATION;
|
|
420
|
+
if (operation === "S") {
|
|
421
|
+
// 只明确放行纯查询组合;升级、安装、缓存清理和混合选项仍需确认。
|
|
422
|
+
return options.some((flag) => "silgp".includes(flag)) &&
|
|
423
|
+
options.every((flag) => "silgpq".includes(flag))
|
|
424
|
+
? undefined
|
|
425
|
+
: PACKAGE_OPERATION;
|
|
426
|
+
}
|
|
427
|
+
const queryOptions = operation === "Q" ? "cdegiklmnopqstu" : operation === "F" ? "lqx" : "";
|
|
428
|
+
return options.every((flag) => queryOptions.includes(flag)) ? undefined : INDETERMINATE;
|
|
429
|
+
}
|
|
430
|
+
function inspectOpenRcCommand(name, args) {
|
|
431
|
+
if (name === "rc-update") {
|
|
432
|
+
const action = leadingAction(args, RC_UPDATE_ZERO_OPTIONS);
|
|
433
|
+
if (action === null)
|
|
434
|
+
return INDETERMINATE;
|
|
435
|
+
if (action === "add" || action === "del" || action === "delete")
|
|
436
|
+
return SERVICE_OPERATION;
|
|
437
|
+
if (action !== undefined && action !== "show")
|
|
438
|
+
return INDETERMINATE;
|
|
439
|
+
return args.some((word) => word.hasExpansion ||
|
|
440
|
+
(word.value.startsWith("-") &&
|
|
441
|
+
word.value !== "--" &&
|
|
442
|
+
!RC_UPDATE_ZERO_OPTIONS.has(word.value)))
|
|
443
|
+
? INDETERMINATE
|
|
444
|
+
: undefined;
|
|
93
445
|
}
|
|
94
|
-
|
|
446
|
+
if (args.length === 1 &&
|
|
447
|
+
!args[0].hasExpansion &&
|
|
448
|
+
["-l", "--list", "-h", "--help"].includes(args[0].value))
|
|
449
|
+
return undefined;
|
|
450
|
+
if (args.length === 2 &&
|
|
451
|
+
!args.some((word) => word.hasExpansion) &&
|
|
452
|
+
["-e", "--exists", "-r", "--resolve"].includes(args[0].value) &&
|
|
453
|
+
!args[1].value.startsWith("-"))
|
|
454
|
+
return undefined;
|
|
455
|
+
let index = 0;
|
|
456
|
+
while (args[index] && !args[index].hasExpansion && RC_SERVICE_ZERO_OPTIONS.has(args[index].value))
|
|
457
|
+
index++;
|
|
458
|
+
const action = serviceAction(args.slice(index));
|
|
459
|
+
if (action === null)
|
|
460
|
+
return INDETERMINATE;
|
|
461
|
+
if (action !== undefined && (SERVICE_ACTIONS.has(action) || action === "zap"))
|
|
462
|
+
return SERVICE_OPERATION;
|
|
463
|
+
// OpenRC 会把剩余参数传给服务脚本;status 后仍有命令时不能按只读查询放行。
|
|
464
|
+
return args.length === index + 2 && (action === "status" || action === "describe")
|
|
465
|
+
? undefined
|
|
466
|
+
: INDETERMINATE;
|
|
467
|
+
}
|
|
468
|
+
function serviceAction(args) {
|
|
469
|
+
if (args[0]?.hasExpansion || args[0]?.value.startsWith("-"))
|
|
470
|
+
return null;
|
|
471
|
+
return args[1]?.hasExpansion || args[1]?.value.startsWith("-") ? null : args[1]?.value;
|
|
95
472
|
}
|
|
96
|
-
function
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
473
|
+
function leadingAction(args, zeroOptions) {
|
|
474
|
+
for (const word of args) {
|
|
475
|
+
if (word.hasExpansion)
|
|
476
|
+
return null;
|
|
477
|
+
if (zeroOptions.has(word.value) || word.value === "--")
|
|
478
|
+
continue;
|
|
479
|
+
return word.value.startsWith("-") ? null : word.value;
|
|
102
480
|
}
|
|
103
|
-
return
|
|
481
|
+
return undefined;
|
|
104
482
|
}
|