@namzu/sdk 51.0.0 → 52.0.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/CHANGELOG.md +110 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/examples/SupervisorAgent.d.ts +42 -0
- package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/examples/SupervisorAgent.js +80 -4
- package/dist/agents/examples/SupervisorAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +36 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -0
- package/dist/authorization/protected-paths.js +107 -0
- package/dist/authorization/protected-paths.js.map +1 -0
- package/dist/authorization/readonly-commands.d.ts +39 -0
- package/dist/authorization/readonly-commands.d.ts.map +1 -0
- package/dist/authorization/readonly-commands.js +797 -0
- package/dist/authorization/readonly-commands.js.map +1 -0
- package/dist/authorization/readonly-flags.d.ts +19 -0
- package/dist/authorization/readonly-flags.d.ts.map +1 -0
- package/dist/authorization/readonly-flags.js +178 -0
- package/dist/authorization/readonly-flags.js.map +1 -0
- package/dist/authorization/removal.d.ts +52 -0
- package/dist/authorization/removal.d.ts.map +1 -0
- package/dist/authorization/removal.js +283 -0
- package/dist/authorization/removal.js.map +1 -0
- package/dist/authorization/shell-paths.d.ts +66 -0
- package/dist/authorization/shell-paths.d.ts.map +1 -0
- package/dist/authorization/shell-paths.js +697 -0
- package/dist/authorization/shell-paths.js.map +1 -0
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +1 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/compaction/context-window.d.ts +24 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +34 -0
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +10 -2
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/plan.d.ts +4 -0
- package/dist/compaction/plan.d.ts.map +1 -1
- package/dist/compaction/plan.js +6 -2
- package/dist/compaction/plan.js.map +1 -1
- package/dist/compaction/verifier.d.ts +11 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +19 -7
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/config/runtime.d.ts +55 -7
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +23 -2
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/web/guarded-fetch.d.ts.map +1 -1
- package/dist/connector/web/guarded-fetch.js +24 -0
- package/dist/connector/web/guarded-fetch.js.map +1 -1
- package/dist/connector/web/index.d.ts +1 -1
- package/dist/connector/web/index.d.ts.map +1 -1
- package/dist/connector/web/page.d.ts +59 -0
- package/dist/connector/web/page.d.ts.map +1 -0
- package/dist/connector/web/page.js +112 -0
- package/dist/connector/web/page.js.map +1 -0
- package/dist/connector/web/types.d.ts +47 -0
- package/dist/connector/web/types.d.ts.map +1 -1
- package/dist/connector/web/types.js.map +1 -1
- package/dist/constants/continuation.d.ts +8 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +8 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +1 -1
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +1 -1
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +7 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +11 -2
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +4 -2
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +6 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +98 -114
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook.d.ts +90 -22
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +434 -41
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/provider/errors.d.ts +10 -0
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +16 -0
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/retry.d.ts +27 -0
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +19 -3
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +4 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +15 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts +10 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +74 -13
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +41 -2
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +190 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts +6 -0
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +1 -1
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts +29 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +15 -3
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +31 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +216 -7
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +58 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +91 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +89 -7
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +165 -2
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +24 -1
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +36 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -0
- package/dist/runtime/query/path-escalation.js +184 -0
- package/dist/runtime/query/path-escalation.js.map +1 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +8 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/reminders.d.ts +35 -0
- package/dist/runtime/query/reminders.d.ts.map +1 -0
- package/dist/runtime/query/reminders.js +7 -0
- package/dist/runtime/query/reminders.js.map +1 -0
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +26 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +26 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +79 -5
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts +46 -0
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +85 -1
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +5 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/session/paths.d.ts +5 -1
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +8 -0
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +2 -2
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +2 -2
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/naming.d.ts +9 -0
- package/dist/store/memory/naming.d.ts.map +1 -1
- package/dist/store/memory/naming.js +12 -0
- package/dist/store/memory/naming.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +30 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +170 -26
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +2 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
- package/dist/tools/builtins/content-fingerprint.js +4 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +60 -18
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +8 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +152 -33
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +44 -5
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +52 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +143 -0
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/web.d.ts +13 -2
- package/dist/tools/builtins/web.d.ts.map +1 -1
- package/dist/tools/builtins/web.js +225 -32
- package/dist/tools/builtins/web.js.map +1 -1
- package/dist/tools/coordinator/ask-user-question.d.ts +2 -0
- package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
- package/dist/tools/coordinator/ask-user-question.js +256 -92
- package/dist/tools/coordinator/ask-user-question.js.map +1 -1
- package/dist/tools/coordinator/question-options.d.ts +1 -0
- package/dist/tools/coordinator/question-options.d.ts.map +1 -1
- package/dist/tools/coordinator/question-options.js +6 -1
- package/dist/tools/coordinator/question-options.js.map +1 -1
- package/dist/tools/defineTool.d.ts +4 -1
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +6 -1
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/paths.d.ts +6 -0
- package/dist/tools/paths.d.ts.map +1 -1
- package/dist/tools/paths.js +1 -1
- package/dist/tools/paths.js.map +1 -1
- package/dist/types/agent/query.d.ts +3 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +1 -1
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/hitl/index.d.ts +104 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +93 -19
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +3 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/error.d.ts +6 -0
- package/dist/types/provider/error.d.ts.map +1 -1
- package/dist/types/provider/errors.d.ts +19 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +39 -1
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +15 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +9 -9
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +1 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +5 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +52 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/utils/backoff.d.ts +12 -0
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +15 -0
- package/dist/utils/backoff.js.map +1 -1
- package/package.json +4 -2
- package/src/agents/QueryAgent.ts +5 -1
- package/src/agents/SupervisorAgent.ts +1 -1
- package/src/agents/examples/SupervisorAgent.ts +217 -106
- package/src/agents/index.ts +1 -1
- package/src/authorization/protected-paths.ts +110 -0
- package/src/authorization/readonly-commands.ts +771 -0
- package/src/authorization/readonly-flags.ts +196 -0
- package/src/authorization/removal.ts +326 -0
- package/src/authorization/shell-paths.ts +927 -0
- package/src/bridge/acp/update.ts +1 -0
- package/src/bridge/sse/mapper.ts +1 -0
- package/src/compaction/context-window.ts +41 -0
- package/src/compaction/manual.ts +10 -2
- package/src/compaction/plan.ts +11 -2
- package/src/compaction/verifier.ts +28 -3
- package/src/config/runtime.ts +23 -2
- package/src/connector/web/guarded-fetch.ts +26 -0
- package/src/connector/web/index.ts +3 -0
- package/src/connector/web/page.ts +130 -0
- package/src/connector/web/types.ts +47 -0
- package/src/constants/continuation.ts +10 -0
- package/src/constants/plugin/index.ts +1 -1
- package/src/manager/agent/lifecycle.ts +12 -4
- package/src/manager/session/turn-recorder.ts +10 -2
- package/src/plugin/index.ts +1 -0
- package/src/plugin/lifecycle.ts +107 -119
- package/src/plugin/shell-hook.ts +477 -43
- package/src/provider/errors.ts +18 -0
- package/src/provider/retry.ts +53 -3
- package/src/public-runtime.ts +8 -0
- package/src/public-types.ts +4 -0
- package/src/runtime/jobs/registry.ts +39 -2
- package/src/runtime/query/executor/tool-call-admission.ts +79 -13
- package/src/runtime/query/executor.ts +242 -8
- package/src/runtime/query/finalize-turn.ts +7 -1
- package/src/runtime/query/index.ts +45 -3
- package/src/runtime/query/iteration/index.ts +251 -12
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +106 -0
- package/src/runtime/query/iteration/phases/compaction.ts +99 -12
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +177 -2
- package/src/runtime/query/iteration/stream-turn.ts +24 -1
- package/src/runtime/query/path-escalation.ts +204 -0
- package/src/runtime/query/plugin-hooks.ts +8 -0
- package/src/runtime/query/reminders.ts +45 -0
- package/src/runtime/query/resume-pending.ts +27 -0
- package/src/runtime/query/review-policy.ts +106 -5
- package/src/runtime/query/tool-output-budget.ts +115 -1
- package/src/runtime/query/tooling.ts +6 -0
- package/src/session/paths.ts +11 -1
- package/src/store/memory/disk.ts +2 -1
- package/src/store/memory/markdown.ts +2 -1
- package/src/store/memory/naming.ts +13 -0
- package/src/tools/builtins/bash.ts +203 -34
- package/src/tools/builtins/content-fingerprint.ts +5 -0
- package/src/tools/builtins/edit.ts +69 -19
- package/src/tools/builtins/grep.ts +159 -38
- package/src/tools/builtins/read-file.ts +48 -5
- package/src/tools/builtins/read-render.ts +193 -0
- package/src/tools/builtins/web.ts +258 -33
- package/src/tools/coordinator/ask-user-question.ts +309 -103
- package/src/tools/coordinator/question-options.ts +8 -1
- package/src/tools/defineTool.ts +11 -2
- package/src/tools/paths.ts +1 -1
- package/src/types/agent/query.ts +3 -0
- package/src/types/errors/catalog.ts +1 -1
- package/src/types/hitl/index.ts +104 -0
- package/src/types/message/index.ts +2 -0
- package/src/types/plugin/index.ts +101 -18
- package/src/types/provider/error.ts +6 -0
- package/src/types/provider/errors.ts +49 -1
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +15 -2
- package/src/types/session/records.ts +1 -0
- package/src/types/session/stop-reason.ts +5 -0
- package/src/types/tool/index.ts +53 -2
- package/src/utils/backoff.ts +21 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-flag allowlists for the shell commands that may run without a prompt.
|
|
3
|
+
*
|
|
4
|
+
* Read from the reference terminal (version 2.1.302): for each command, the
|
|
5
|
+
* flags it vets and what each takes, `none` (a switch), `string` or `number`.
|
|
6
|
+
* A flag not listed here is not read-only, which is how `git diff --output=f`
|
|
7
|
+
* (writes a file), `git -c`, `--exec-path` and `--config-env` stay out: they
|
|
8
|
+
* are not in any table. Space-separated lists per kind keep the file readable;
|
|
9
|
+
* `readonly-commands.ts` holds the callbacks that need more than a flag name.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type FlagKind = 'none' | 'string' | 'number'
|
|
13
|
+
|
|
14
|
+
export interface FlagTable {
|
|
15
|
+
readonly flags: Readonly<Record<string, FlagKind>>
|
|
16
|
+
/** False when `--` does not end the options for this command. */
|
|
17
|
+
readonly doubleDash: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function table(spec: {
|
|
21
|
+
none?: string
|
|
22
|
+
string?: string
|
|
23
|
+
number?: string
|
|
24
|
+
doubleDash?: false
|
|
25
|
+
}): FlagTable {
|
|
26
|
+
const flags: Record<string, FlagKind> = {}
|
|
27
|
+
for (const kind of ['none', 'string', 'number'] as const) {
|
|
28
|
+
for (const flag of (spec[kind] ?? '').split(' ')) if (flag !== '') flags[flag] = kind
|
|
29
|
+
}
|
|
30
|
+
return { flags, doubleDash: spec.doubleDash !== false }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Keyed by the command words that select the table (`git diff`, `rg`). */
|
|
34
|
+
export const FLAG_TABLES: Readonly<Record<string, FlagTable>> = {
|
|
35
|
+
'git diff': table({
|
|
36
|
+
none: '--stat --numstat --shortstat --name-only --name-status --color --no-color --dirstat --summary --patch-with-stat --word-diff --color-words --no-renames --no-ext-diff --check --full-index --binary --break-rewrites --find-renames --find-copies --find-copies-harder --irreversible-delete --histogram --patience --minimal --ignore-space-at-eol --ignore-space-change --ignore-all-space --ignore-blank-lines --function-context --exit-code --quiet --cached --staged --pickaxe-regex --pickaxe-all --no-index -p -u -s -M -C -B -D -l -R',
|
|
37
|
+
string:
|
|
38
|
+
'--word-diff-regex --ws-error-highlight --diff-algorithm --relative --diff-filter -S -G -O',
|
|
39
|
+
number: '--abbrev --inter-hunk-context',
|
|
40
|
+
}),
|
|
41
|
+
'git log': table({
|
|
42
|
+
none: '--oneline --graph --decorate --no-decorate --relative-date --all --branches --tags --remotes --stat --numstat --shortstat --name-only --name-status --color --no-color --patch -p --no-patch --no-ext-diff -s --abbrev-commit --full-history --dense --sparse --simplify-merges --ancestry-path --source --first-parent --merges --no-merges --reverse --walk-reflogs --no-min-parents --no-max-parents --follow --no-walk --left-right --cherry-mark --cherry-pick --boundary --topo-order --date-order --author-date-order --pickaxe-regex --pickaxe-all',
|
|
43
|
+
string:
|
|
44
|
+
'--date --since --after --until --before --author --committer --grep --pretty --format --diff-filter -S -G',
|
|
45
|
+
number: '--max-count -n --skip --max-age --min-age',
|
|
46
|
+
}),
|
|
47
|
+
'git show': table({
|
|
48
|
+
none: '--oneline --graph --decorate --no-decorate --relative-date --stat --numstat --shortstat --name-only --name-status --color --no-color --patch -p --no-patch --no-ext-diff -s --abbrev-commit --word-diff --color-words --first-parent --raw -m --quiet',
|
|
49
|
+
string: '--date --word-diff-regex --pretty --format --diff-filter',
|
|
50
|
+
}),
|
|
51
|
+
'git shortlog': table({
|
|
52
|
+
none: '--all --branches --tags --remotes -s --summary -n --numbered -e --email -c --committer --no-merges',
|
|
53
|
+
string: '--since --after --until --before --group --format --author',
|
|
54
|
+
}),
|
|
55
|
+
'git reflog': table({
|
|
56
|
+
none: '--oneline --graph --decorate --no-decorate --relative-date --all --branches --tags --remotes',
|
|
57
|
+
string: '--date --since --after --until --before --author --committer --grep',
|
|
58
|
+
number: '--max-count -n',
|
|
59
|
+
}),
|
|
60
|
+
'git stash list': table({
|
|
61
|
+
none: '--oneline --graph --decorate --no-decorate --relative-date --all --branches --tags --remotes',
|
|
62
|
+
string: '--date',
|
|
63
|
+
number: '--max-count -n',
|
|
64
|
+
doubleDash: false,
|
|
65
|
+
}),
|
|
66
|
+
'git ls-remote': table({
|
|
67
|
+
none: '--branches -b --tags -t --heads -h --refs --quiet -q --exit-code --get-url --symref',
|
|
68
|
+
string: '--sort',
|
|
69
|
+
}),
|
|
70
|
+
'git status': table({
|
|
71
|
+
none: '--short -s --branch -b --porcelain --long --verbose -v --ignored --column --no-column --ahead-behind --no-ahead-behind --renames --no-renames',
|
|
72
|
+
string: '--untracked-files -u --ignore-submodules --find-renames -M',
|
|
73
|
+
}),
|
|
74
|
+
'git blame': table({
|
|
75
|
+
none: '--color --no-color --porcelain -p --line-porcelain --incremental --root --show-stats --show-name --show-number -n --show-email -e -f -w -M -C --score-debug -s -l -t',
|
|
76
|
+
string: '-L --date --ignore-rev --ignore-revs-file',
|
|
77
|
+
number: '--abbrev',
|
|
78
|
+
doubleDash: false,
|
|
79
|
+
}),
|
|
80
|
+
'git ls-files': table({
|
|
81
|
+
none: '--cached -c --deleted -d --modified -m --others -o --ignored -i --stage -s --killed -k --unmerged -u --directory --no-empty-directory --eol --full-name --debug -z -t -v -f --exclude-standard --error-unmatch --recurse-submodules',
|
|
82
|
+
string: '--exclude -x --exclude-from -X --exclude-per-directory',
|
|
83
|
+
number: '--abbrev',
|
|
84
|
+
}),
|
|
85
|
+
'git config --get': table({
|
|
86
|
+
none: '--local --global --system --worktree --bool --int --bool-or-int --path --expiry-date -z --null --name-only --show-origin --show-scope',
|
|
87
|
+
string: '--default --type',
|
|
88
|
+
}),
|
|
89
|
+
'git remote show': table({
|
|
90
|
+
none: '-n',
|
|
91
|
+
}),
|
|
92
|
+
'git remote': table({
|
|
93
|
+
none: '-v --verbose',
|
|
94
|
+
}),
|
|
95
|
+
'git merge-base': table({
|
|
96
|
+
none: '--is-ancestor --fork-point --octopus --independent --all',
|
|
97
|
+
}),
|
|
98
|
+
'git rev-parse': table({
|
|
99
|
+
none: '--verify --abbrev-ref --symbolic --symbolic-full-name --show-toplevel --show-cdup --show-prefix --git-dir --git-common-dir --absolute-git-dir --show-superproject-working-tree --is-inside-work-tree --is-inside-git-dir --is-bare-repository --is-shallow-repository --is-shallow-update --path-prefix',
|
|
100
|
+
string: '--short',
|
|
101
|
+
}),
|
|
102
|
+
'git rev-list': table({
|
|
103
|
+
none: '--all --branches --tags --remotes --count --reverse --first-parent --ancestry-path --merges --no-merges --no-min-parents --no-max-parents --walk-reflogs --oneline --abbrev-commit --full-history --dense --sparse --source --graph',
|
|
104
|
+
string: '--since --after --until --before --author --committer --grep --pretty --format',
|
|
105
|
+
number: '--max-count -n --min-parents --max-parents --skip --max-age --min-age --abbrev',
|
|
106
|
+
}),
|
|
107
|
+
'git describe': table({
|
|
108
|
+
none: '--tags --long --always --contains --first-match --exact-match --dirty --broken',
|
|
109
|
+
string: '--match --exclude',
|
|
110
|
+
number: '--abbrev --candidates',
|
|
111
|
+
}),
|
|
112
|
+
'git cat-file': table({
|
|
113
|
+
none: '-t -s -p -e --batch-check --allow-undetermined-type',
|
|
114
|
+
}),
|
|
115
|
+
'git for-each-ref': table({
|
|
116
|
+
string: '--format --sort --contains --no-contains --merged --no-merged --points-at',
|
|
117
|
+
number: '--count',
|
|
118
|
+
}),
|
|
119
|
+
'git grep': table({
|
|
120
|
+
none: '-E --extended-regexp -G --basic-regexp -F --fixed-strings -P --perl-regexp -i --ignore-case -v --invert-match -w --word-regexp -n --line-number -c --count -l --files-with-matches -L --files-without-match -h -H --heading --break --full-name --color --no-color -o --only-matching --and --or --not --untracked --no-index --recurse-submodules --cached -q --quiet',
|
|
121
|
+
string: '-e',
|
|
122
|
+
number: '-A --after-context -B --before-context -C --context --max-depth --threads',
|
|
123
|
+
}),
|
|
124
|
+
'git stash show': table({
|
|
125
|
+
none: '--stat --numstat --shortstat --name-only --name-status --color --no-color --patch -p --no-patch --no-ext-diff -s --word-diff',
|
|
126
|
+
string: '--word-diff-regex --diff-filter',
|
|
127
|
+
number: '--abbrev',
|
|
128
|
+
}),
|
|
129
|
+
'git worktree list': table({
|
|
130
|
+
none: '--porcelain -v --verbose',
|
|
131
|
+
string: '--expire',
|
|
132
|
+
}),
|
|
133
|
+
'git tag': table({
|
|
134
|
+
none: '-l --list --column --no-column -i --ignore-case',
|
|
135
|
+
string: '--contains --no-contains --merged --no-merged --sort --format --points-at',
|
|
136
|
+
number: '-n',
|
|
137
|
+
}),
|
|
138
|
+
'git branch': table({
|
|
139
|
+
none: '-l --list -a --all -r --remotes -v -vv --verbose --color --no-color --column --no-column --no-abbrev --merged --no-merged --show-current -i --ignore-case',
|
|
140
|
+
string: '--contains --no-contains --points-at --sort',
|
|
141
|
+
number: '--abbrev',
|
|
142
|
+
}),
|
|
143
|
+
file: table({
|
|
144
|
+
none: '--brief -b --mime -i --mime-type --mime-encoding --apple --check-encoding -c --print0 -0 --help --version -v --no-dereference -h --dereference -L --keep-going -k --list -l --no-buffer -n --preserve-date -p --raw -r -s --special-files',
|
|
145
|
+
string: '--exclude --exclude-quiet -F --separator',
|
|
146
|
+
}),
|
|
147
|
+
sort: table({
|
|
148
|
+
none: '--ignore-leading-blanks -b --dictionary-order -d --ignore-case -f --general-numeric-sort -g --human-numeric-sort -h --ignore-nonprinting -i --month-sort -M --numeric-sort -n --random-sort -R --reverse -r --stable -s --unique -u --version-sort -V --zero-terminated -z --check -c --check-char-order -C --merge -m --help --version',
|
|
149
|
+
string: '--sort --key -k --field-separator -t --buffer-size -S',
|
|
150
|
+
number: '--parallel --batch-size',
|
|
151
|
+
}),
|
|
152
|
+
base64: table({
|
|
153
|
+
none: '-d -D --decode --ignore-garbage -h --help --version',
|
|
154
|
+
string: '-i --input',
|
|
155
|
+
number: '-b --break -w --wrap',
|
|
156
|
+
doubleDash: false,
|
|
157
|
+
}),
|
|
158
|
+
grep: table({
|
|
159
|
+
none: '-F --fixed-strings -G --basic-regexp -E --extended-regexp -P --perl-regexp -i --ignore-case --no-ignore-case -v --invert-match -w --word-regexp -x --line-regexp -c --count -L --files-without-match -l --files-with-matches -o --only-matching -q --quiet --silent -s --no-messages -b --byte-offset -H --with-filename -h --no-filename -n --line-number -T --initial-tab -u --unix-byte-offsets -Z --null -z --null-data --no-group-separator -a --text -I -r --recursive -R --dereference-recursive --line-buffered -U --binary --help -V --version',
|
|
160
|
+
string:
|
|
161
|
+
'-e --regexp -f --file --color --colour --label --group-separator --binary-files -D --devices -d --directories --exclude --exclude-from --exclude-dir --include',
|
|
162
|
+
number: '-m --max-count -A --after-context -B --before-context -C --context',
|
|
163
|
+
}),
|
|
164
|
+
egrep: table({
|
|
165
|
+
none: '-F --fixed-strings -G --basic-regexp -E --extended-regexp -P --perl-regexp -i --ignore-case --no-ignore-case -v --invert-match -w --word-regexp -x --line-regexp -c --count -L --files-without-match -l --files-with-matches -o --only-matching -q --quiet --silent -s --no-messages -b --byte-offset -H --with-filename -h --no-filename -n --line-number -T --initial-tab -u --unix-byte-offsets -Z --null -z --null-data --no-group-separator -a --text -I -r --recursive -R --dereference-recursive --line-buffered -U --binary --help -V --version',
|
|
166
|
+
string:
|
|
167
|
+
'-e --regexp -f --file --color --colour --label --group-separator --binary-files -D --devices -d --directories --exclude --exclude-from --exclude-dir --include',
|
|
168
|
+
number: '-m --max-count -A --after-context -B --before-context -C --context',
|
|
169
|
+
}),
|
|
170
|
+
fgrep: table({
|
|
171
|
+
none: '-F --fixed-strings -G --basic-regexp -E --extended-regexp -P --perl-regexp -i --ignore-case --no-ignore-case -v --invert-match -w --word-regexp -x --line-regexp -c --count -L --files-without-match -l --files-with-matches -o --only-matching -q --quiet --silent -s --no-messages -b --byte-offset -H --with-filename -h --no-filename -n --line-number -T --initial-tab -u --unix-byte-offsets -Z --null -z --null-data --no-group-separator -a --text -I -r --recursive -R --dereference-recursive --line-buffered -U --binary --help -V --version',
|
|
172
|
+
string:
|
|
173
|
+
'-e --regexp -f --file --color --colour --label --group-separator --binary-files -D --devices -d --directories --exclude --exclude-from --exclude-dir --include',
|
|
174
|
+
number: '-m --max-count -A --after-context -B --before-context -C --context',
|
|
175
|
+
}),
|
|
176
|
+
rg: table({
|
|
177
|
+
none: '-i --ignore-case -S --smart-case -F --fixed-strings -w --word-regexp -v --invert-match -c --count -l --files-with-matches --files-without-match -n --line-number -o --only-matching -H -h --heading --no-heading -q --quiet --column --type-list --hidden --no-ignore -u -a --text -L --follow --json --stats --help --version --debug --',
|
|
178
|
+
string: '-e --regexp -f -g --glob -t --type -T --type-not --color',
|
|
179
|
+
number: '-A --after-context -B --before-context -C --context -m --max-count -d --max-depth',
|
|
180
|
+
}),
|
|
181
|
+
sha256sum: table({
|
|
182
|
+
none: '-b --binary -t --text -c --check --ignore-missing --quiet --status --strict -w --warn --tag -z --zero --help --version',
|
|
183
|
+
}),
|
|
184
|
+
sha1sum: table({
|
|
185
|
+
none: '-b --binary -t --text -c --check --ignore-missing --quiet --status --strict -w --warn --tag -z --zero --help --version',
|
|
186
|
+
}),
|
|
187
|
+
md5sum: table({
|
|
188
|
+
none: '-b --binary -t --text -c --check --ignore-missing --quiet --status --strict -w --warn --tag -z --zero --help --version',
|
|
189
|
+
}),
|
|
190
|
+
tree: table({
|
|
191
|
+
none: '-a -d -l -f -x --gitignore --ignore-case --matchdirs --metafirst --prune --info --noreport -q -N -Q -p -u -g -s -h --si --du -D -F --inodes --device -v -t -c -U -r --dirsfirst --filesfirst -i -A -S -n -C -X -J --nolinks --hyperlink --fromfile --fromtabfile --fflinks --help --version',
|
|
192
|
+
string:
|
|
193
|
+
'-P -I --gitfile --infofile --charset --timefmt --sort -H --hintro --houtro -T --scheme --authority',
|
|
194
|
+
number: '-L --filelimit',
|
|
195
|
+
}),
|
|
196
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a command line removes something it must not, decided from its
|
|
3
|
+
* targets.
|
|
4
|
+
*
|
|
5
|
+
* The old floor was one regular expression, `rm\s+-rf\s+\/`. It matched a
|
|
6
|
+
* single spelling: `rm -fr /`, `rm -r -f /`, `rm --recursive --force /`,
|
|
7
|
+
* `rm -rf ~`, `rm -rf $HOME`, `rm -rf "$BUILD/"` (empty variable, so `/`) and
|
|
8
|
+
* `rm -rf ..` all went past it. The flags were never the point. A removal is
|
|
9
|
+
* catastrophic because of WHAT it removes, so this reads the targets of `rm`
|
|
10
|
+
* and `rmdir` whatever the flags, as the reference terminal's dangerous-removal
|
|
11
|
+
* check does (version 2.1.302).
|
|
12
|
+
*
|
|
13
|
+
* A removal is flagged when a target is
|
|
14
|
+
*
|
|
15
|
+
* - the file system root, a top-level directory (`/usr`, `/etc`), or the
|
|
16
|
+
* operator's home directory (`~`, `$HOME`, `${HOME}`);
|
|
17
|
+
* - the working directory or any directory above it (`.`, `..`, `../..`), or
|
|
18
|
+
* the same for an added directory when the target is a glob;
|
|
19
|
+
* - a bare glob that crosses directories (`*`, `/*`, `*\/*`);
|
|
20
|
+
* - a variable followed by a slash (`$DIR/`, `"$DIR/"/*`): when the variable
|
|
21
|
+
* is empty the path is `/`; or a variable followed by a top-level directory
|
|
22
|
+
* name (`$PREFIX/usr`) when the variable may be unset;
|
|
23
|
+
* - the output of a command substitution, for a recursive removal;
|
|
24
|
+
* - a path that cannot be settled before the line runs (a relative glob after
|
|
25
|
+
* a `cd`, `a/../*`, `~user`).
|
|
26
|
+
*
|
|
27
|
+
* The answer is a reason, which the kernel turns into an escalation: asked
|
|
28
|
+
* every time, in every mode, and refused when nobody can be asked. It is not a
|
|
29
|
+
* denial: the reference asks, because there are legitimate removals a person
|
|
30
|
+
* can approve.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
34
|
+
import { homedir } from 'node:os'
|
|
35
|
+
import {
|
|
36
|
+
dirname,
|
|
37
|
+
isAbsolute,
|
|
38
|
+
join,
|
|
39
|
+
normalize,
|
|
40
|
+
basename as pathBasename,
|
|
41
|
+
resolve,
|
|
42
|
+
sep,
|
|
43
|
+
} from 'node:path'
|
|
44
|
+
|
|
45
|
+
import type { ShellWord } from './shell-lexer.js'
|
|
46
|
+
import { type ShellInvocation, type ShellReading, expandsAtRuntime } from './shell-paths.js'
|
|
47
|
+
|
|
48
|
+
/** What the check needs to know about the turn. */
|
|
49
|
+
export interface RemovalContext {
|
|
50
|
+
/** The turn's working directory. */
|
|
51
|
+
readonly cwd: string
|
|
52
|
+
/** Directories added to the session. */
|
|
53
|
+
readonly additionalDirectories?: readonly string[]
|
|
54
|
+
/** The operator's home directory; the host's own when omitted. */
|
|
55
|
+
readonly home?: string
|
|
56
|
+
/** Environment to decide whether a variable may be empty; `process.env` when omitted. */
|
|
57
|
+
readonly env?: Readonly<Record<string, string | undefined>>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Top-level directories a variable-prefixed target may become when the variable is empty. */
|
|
61
|
+
const TOP_LEVEL =
|
|
62
|
+
'bin|boot|dev|etc|home|lib|lib32|lib64|libx32|media|mnt|opt|proc|root|run|sbin|srv|sys|tmp|usr|var|snap|nix|lost\\+found|private|cores|Applications|Library|System|Users|Volumes|Windows|ProgramData|cygdrive'
|
|
63
|
+
|
|
64
|
+
const VAR = '(?:\\{([A-Za-z_][A-Za-z0-9_]*)(?::?-[^}]*)?\\}|([A-Za-z_][A-Za-z0-9_]*))'
|
|
65
|
+
/** `$VAR/` followed by something that cannot name a child: end, a glob, another expansion, a slash. */
|
|
66
|
+
const EMPTY_VARIABLE_ROOT = new RegExp(`^\\$${VAR}/(?:[*?[{]|\\$|/|$)`)
|
|
67
|
+
const EMPTY_VARIABLE_TOP = new RegExp(`^\\$${VAR}/(${TOP_LEVEL})(?:/+\\*+)*/?$`, 'i')
|
|
68
|
+
const BARE_HOME = /^\$(?:\{HOME\}|HOME)(?:\/+\*?)?$/
|
|
69
|
+
|
|
70
|
+
function fold(name: string): string {
|
|
71
|
+
return name.toLowerCase().replace(/ı/g, 'i').replace(/ſ/g, 's')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `realpath` of the deepest existing ancestor, with the rest appended. */
|
|
75
|
+
function canonical(path: string): string {
|
|
76
|
+
let existing = path
|
|
77
|
+
let rest = ''
|
|
78
|
+
for (;;) {
|
|
79
|
+
if (existsSync(existing)) {
|
|
80
|
+
try {
|
|
81
|
+
const real = realpathSync(existing)
|
|
82
|
+
return rest === '' ? real : join(real, rest)
|
|
83
|
+
} catch {
|
|
84
|
+
return path
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const parent = dirname(existing)
|
|
88
|
+
if (parent === existing) return path
|
|
89
|
+
rest = rest === '' ? pathBasename(existing) : join(pathBasename(existing), rest)
|
|
90
|
+
existing = parent
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function slashes(path: string): string {
|
|
95
|
+
return path.replace(/[\\/]+/g, '/')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isWithinOrEqual(parent: string, child: string): boolean {
|
|
99
|
+
const p = fold(slashes(parent)).replace(/\/$/, '')
|
|
100
|
+
const c = fold(slashes(child)).replace(/\/$/, '')
|
|
101
|
+
return c === p || c.startsWith(`${p}/`) || p === ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The file system root, a top-level directory, a drive root, a bare glob, or the home directory. */
|
|
105
|
+
function isCritical(path: string, homes: readonly string[]): boolean {
|
|
106
|
+
const normalized = slashes(path)
|
|
107
|
+
if (normalized === '*' || normalized.endsWith('/*')) return true
|
|
108
|
+
const trimmed = normalized === '/' ? '/' : normalized.replace(/\/$/, '')
|
|
109
|
+
if (trimmed === '/') return true
|
|
110
|
+
if (/^[A-Za-z]:\/?$/.test(trimmed)) return true
|
|
111
|
+
if (homes.some((home) => fold(trimmed) === fold(slashes(home).replace(/\/$/, '')))) return true
|
|
112
|
+
if (dirname(trimmed) === '/') return true
|
|
113
|
+
return /^[A-Za-z]:\/[^/]+$/.test(trimmed)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isRecursive(args: readonly ShellWord[]): boolean {
|
|
117
|
+
const flags: string[] = []
|
|
118
|
+
for (const arg of args) {
|
|
119
|
+
if (arg.value === '--') break
|
|
120
|
+
flags.push(arg.value)
|
|
121
|
+
}
|
|
122
|
+
return flags.some((flag) => /^--r/.test(flag) || /^-[a-zA-Z]*[rR]/.test(flag))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Operands of `rm`/`rmdir`: everything after `--` and everything from the first non-flag on. */
|
|
126
|
+
function targetsOf(args: readonly ShellWord[]): ShellWord[] {
|
|
127
|
+
const out: ShellWord[] = []
|
|
128
|
+
let afterDashes = false
|
|
129
|
+
let started = false
|
|
130
|
+
for (const arg of args) {
|
|
131
|
+
if (afterDashes || started) out.push(arg)
|
|
132
|
+
else if (arg.value === '--') afterDashes = true
|
|
133
|
+
else if (arg.value === '-' || !arg.value.startsWith('-')) {
|
|
134
|
+
out.push(arg)
|
|
135
|
+
started = true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** `~` and `~/…` against `home`. */
|
|
142
|
+
function expandTilde(value: string, home: string): string {
|
|
143
|
+
if (value === '~') return home
|
|
144
|
+
if (value.startsWith('~/')) return join(home, value.slice(2))
|
|
145
|
+
return value
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Why the line removes something it must not, or `undefined`.
|
|
150
|
+
*
|
|
151
|
+
* `reading` is {@link readShellLine}'s result for the same line;
|
|
152
|
+
* `changesDirectory` says a `cd` runs in it, after which a relative target is
|
|
153
|
+
* not the one the working directory names.
|
|
154
|
+
*/
|
|
155
|
+
export function dangerousRemoval(
|
|
156
|
+
reading: ShellReading,
|
|
157
|
+
changesDirectory: boolean,
|
|
158
|
+
context: RemovalContext,
|
|
159
|
+
): string | undefined {
|
|
160
|
+
const removals = reading.invocations.filter((i) => i.program === 'rm' || i.program === 'rmdir')
|
|
161
|
+
if (removals.length === 0) return undefined
|
|
162
|
+
const home = context.home ?? homedir()
|
|
163
|
+
const env = context.env ?? process.env
|
|
164
|
+
const cwd = resolve(context.cwd)
|
|
165
|
+
const homes = [home, canonical(home)]
|
|
166
|
+
const workdirs = [cwd, canonical(cwd)]
|
|
167
|
+
const everyDirectory = [
|
|
168
|
+
...new Set([
|
|
169
|
+
...workdirs,
|
|
170
|
+
...(context.additionalDirectories ?? []).flatMap((d) => [resolve(d), canonical(resolve(d))]),
|
|
171
|
+
]),
|
|
172
|
+
]
|
|
173
|
+
for (const removal of removals) {
|
|
174
|
+
const reason = checkRemoval(removal, {
|
|
175
|
+
cwd,
|
|
176
|
+
home,
|
|
177
|
+
homes,
|
|
178
|
+
workdirs,
|
|
179
|
+
everyDirectory,
|
|
180
|
+
env,
|
|
181
|
+
changesDirectory,
|
|
182
|
+
})
|
|
183
|
+
if (reason !== undefined) return reason
|
|
184
|
+
}
|
|
185
|
+
return undefined
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface Scope {
|
|
189
|
+
readonly cwd: string
|
|
190
|
+
readonly home: string
|
|
191
|
+
readonly homes: readonly string[]
|
|
192
|
+
readonly workdirs: readonly string[]
|
|
193
|
+
readonly everyDirectory: readonly string[]
|
|
194
|
+
readonly env: Readonly<Record<string, string | undefined>>
|
|
195
|
+
readonly changesDirectory: boolean
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function checkRemoval(removal: ShellInvocation, scope: Scope): string | undefined {
|
|
199
|
+
const { program, args } = removal
|
|
200
|
+
const recursive = isRecursive(args)
|
|
201
|
+
for (const target of targetsOf(args)) {
|
|
202
|
+
const spelled = target.value
|
|
203
|
+
if (spelled === '') continue
|
|
204
|
+
|
|
205
|
+
// A backslash alone is the drive root in Git Bash on Windows.
|
|
206
|
+
if (/^\\+$/.test(spelled)) return `${program} ${spelled}: a backslash alone is the drive root`
|
|
207
|
+
|
|
208
|
+
if (BARE_HOME.test(spelled)) return `${program} ${spelled}: that is the home directory`
|
|
209
|
+
|
|
210
|
+
const root = EMPTY_VARIABLE_ROOT.exec(spelled)
|
|
211
|
+
if (root !== null) {
|
|
212
|
+
const name = root[1] ?? root[2]
|
|
213
|
+
return `${program} ${spelled}: when $${name} is empty this removes /`
|
|
214
|
+
}
|
|
215
|
+
const top = EMPTY_VARIABLE_TOP.exec(spelled)
|
|
216
|
+
if (top !== null) {
|
|
217
|
+
const name = top[1] ?? top[2] ?? ''
|
|
218
|
+
const value = scope.env[name]
|
|
219
|
+
if (typeof value !== 'string' || value === '') {
|
|
220
|
+
return `${program} ${spelled}: when $${name} is empty this removes /${top[3]}`
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (target.substitutes && recursive && /^(?:\$\(.*\)|`.*`)[/*.]*$/s.test(spelled)) {
|
|
225
|
+
return `${program} ${spelled}: the target is the output of a command substitution, which cannot be checked before it runs`
|
|
226
|
+
}
|
|
227
|
+
// A substitution that comes out empty leaves what is before it.
|
|
228
|
+
const stripped = target.substitutes
|
|
229
|
+
? spelled.replace(/(.)(?:\$\([^)]*\)|`[^`]*`)(?:(?!\.\.)[/*.])*((?:\/\.\.)*)\/*$/s, '$1$2')
|
|
230
|
+
: spelled
|
|
231
|
+
const reason = checkTarget(program, args, stripped, target, scope)
|
|
232
|
+
if (reason !== undefined) return reason
|
|
233
|
+
}
|
|
234
|
+
return undefined
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function checkTarget(
|
|
238
|
+
program: string,
|
|
239
|
+
args: readonly ShellWord[],
|
|
240
|
+
spelled: string,
|
|
241
|
+
word: ShellWord,
|
|
242
|
+
scope: Scope,
|
|
243
|
+
): string | undefined {
|
|
244
|
+
const expanded = expandTilde(spelled, scope.home)
|
|
245
|
+
const relative = !isAbsolute(expanded)
|
|
246
|
+
if (expandsAtRuntime(word) && /[$`]/.test(expanded)) {
|
|
247
|
+
// A variable the check cannot read: the reference treats it as a relative
|
|
248
|
+
// name, which is also what the shell does when the variable is unset and
|
|
249
|
+
// the rest is a plain name. Nothing more to learn from it.
|
|
250
|
+
return undefined
|
|
251
|
+
}
|
|
252
|
+
const absolute = relative ? resolve(scope.cwd, expanded) : resolve(expanded)
|
|
253
|
+
let settled = absolute
|
|
254
|
+
for (let previous = ''; previous !== settled; ) {
|
|
255
|
+
previous = settled
|
|
256
|
+
const stripped = settled.replace(/([\\/]\*+)+[\\/]*$/, '') || sep
|
|
257
|
+
if (stripped !== settled) settled = /[\\/]/.test(stripped) ? normalize(stripped) : stripped
|
|
258
|
+
}
|
|
259
|
+
// A glob suffix was stripped from the target: the removal reaches what is in it.
|
|
260
|
+
const globbed = settled !== absolute
|
|
261
|
+
const original = expanded
|
|
262
|
+
|
|
263
|
+
if (scope.changesDirectory && globbed && relative && /[\\/]\*$/.test(absolute)) {
|
|
264
|
+
return `${program} ${spelled}: a relative glob after a cd cannot be resolved before the line runs`
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Where the target sits under the working directory, for the glob depth below.
|
|
268
|
+
let below = settled
|
|
269
|
+
if (relative) {
|
|
270
|
+
const prefix = scope.cwd.endsWith(sep) ? scope.cwd : scope.cwd + sep
|
|
271
|
+
if (settled.startsWith(prefix)) below = settled.slice(prefix.length)
|
|
272
|
+
else if (settled === scope.cwd) below = ''
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
globbed &&
|
|
277
|
+
(hasParentAfterName(original) ||
|
|
278
|
+
original.startsWith('~') ||
|
|
279
|
+
/^[\\/]{2}/.test(original) ||
|
|
280
|
+
(relative && /(^|[\\/])\.\.([\\/]|$)/.test(original) && /[\\/]\*$/.test(absolute)) ||
|
|
281
|
+
(program === 'rmdir' &&
|
|
282
|
+
/[\\/]\*$/.test(absolute) &&
|
|
283
|
+
args.some((a) => /^--p/.test(a.value) || /^-[a-z]*p/.test(a.value))) ||
|
|
284
|
+
(relative && /\*[\\/]+$/.test(original) && /[\\/]\*$/.test(absolute)))
|
|
285
|
+
) {
|
|
286
|
+
return `${program} ${spelled}: the target cannot be resolved to a directory before the line runs`
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!globbed || !/[*?[]/.test(below)) {
|
|
290
|
+
const candidates = [settled]
|
|
291
|
+
const real = canonical(settled)
|
|
292
|
+
if (real !== settled) candidates.push(real)
|
|
293
|
+
for (const candidate of candidates) {
|
|
294
|
+
if (isCritical(candidate, scope.homes)) {
|
|
295
|
+
return `${program} ${spelled}: that is the file system root, a top-level directory or the home directory`
|
|
296
|
+
}
|
|
297
|
+
const directories = globbed ? scope.everyDirectory : scope.workdirs
|
|
298
|
+
if (directories.some((directory) => isWithinOrEqual(candidate, directory))) {
|
|
299
|
+
return `${program} ${spelled}: that is the working directory or one of its parents`
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (globbed && /[\\/]\*$/.test(absolute)) {
|
|
305
|
+
const segments = (value: string): number =>
|
|
306
|
+
value.split(/[\\/]+/).filter((s) => s !== '' && s !== '.').length
|
|
307
|
+
const crossed = segments(absolute) - segments(settled)
|
|
308
|
+
const inside = below.split(/[\\/]+/).filter((s) => /[*?[]/.test(s)).length
|
|
309
|
+
if (crossed + inside > 1) {
|
|
310
|
+
return `${program} ${spelled}: the glob crosses directories that cannot be listed before the line runs`
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** `a/../b`: a parent step after a real name, which moves the target somewhere the spelling hides. */
|
|
317
|
+
function hasParentAfterName(path: string): boolean {
|
|
318
|
+
let named = false
|
|
319
|
+
for (const segment of path.split(/[\\/]+/)) {
|
|
320
|
+
if (segment === '' || segment === '.') continue
|
|
321
|
+
if (segment === '..') {
|
|
322
|
+
if (named) return true
|
|
323
|
+
} else named = true
|
|
324
|
+
}
|
|
325
|
+
return false
|
|
326
|
+
}
|