@gajae-code/coding-agent 0.12.0 → 0.12.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/CHANGELOG.md +111 -0
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +56 -2
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +16 -2
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +40 -2
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +7 -1
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +9 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +290 -0
- package/src/async/job-manager.ts +99 -39
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +53 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +18 -1
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +491 -27
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
- package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
- package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
- package/src/gjc-runtime/workflow-manifest.ts +21 -70
- package/src/internal-urls/docs-index.generated.ts +16 -16
- package/src/modes/DESIGN.md +104 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -15
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +42 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +193 -34
- package/src/modes/controllers/selector-controller.ts +160 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/runtime-init.ts +3 -0
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +7 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +89 -9
- package/src/session/agent-session.ts +844 -173
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +200 -111
- package/src/session/internal/managed-session-storage.ts +492 -62
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +38 -45
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +280 -45
- package/src/task/output-manager.ts +24 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +34 -8
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +86 -55
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +6 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -60,6 +60,35 @@ const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s
|
|
|
60
60
|
/** Literal `sh|bash|zsh -c '<script>'` payloads whose nested script must also be scanned. */
|
|
61
61
|
const BASH_NESTED_SHELL_RE =
|
|
62
62
|
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(?:ba|z|da)?sh\s+(?:-[A-Za-z]+\s+)*-[A-Za-z]*c\s+(?:(')([^']*)'|(")([^"]*)")/g;
|
|
63
|
+
/** Heredoc opener (`<<`/`<<-` plus an optionally quoted delimiter); `<<<` here-strings excluded. */
|
|
64
|
+
const BASH_HEREDOC_OPEN_RE = /(?<!<)<<(?!<)(-?)\s*(?:'([^'\s]+)'|"([^"\s]+)"|(\\)?([A-Za-z_][\w.-]*))/g;
|
|
65
|
+
/** Consumers whose stdin is inert data: their heredoc bodies are safe to mask. Anything else stays live. */
|
|
66
|
+
const HEREDOC_DATA_CONSUMERS = new Set([
|
|
67
|
+
"cat",
|
|
68
|
+
"tee",
|
|
69
|
+
"head",
|
|
70
|
+
"tail",
|
|
71
|
+
"wc",
|
|
72
|
+
"sort",
|
|
73
|
+
"uniq",
|
|
74
|
+
"tr",
|
|
75
|
+
"cut",
|
|
76
|
+
"grep",
|
|
77
|
+
"column",
|
|
78
|
+
"nl",
|
|
79
|
+
"fold",
|
|
80
|
+
"fmt",
|
|
81
|
+
"base64",
|
|
82
|
+
"md5sum",
|
|
83
|
+
"sha1sum",
|
|
84
|
+
"sha256sum",
|
|
85
|
+
"sha512sum",
|
|
86
|
+
"shasum",
|
|
87
|
+
"jq",
|
|
88
|
+
"yq",
|
|
89
|
+
]);
|
|
90
|
+
/** Stdin appliers whose heredoc body mutates files in ways no body scan can model. */
|
|
91
|
+
const HEREDOC_MUTATING_CONSUMERS = new Set(["patch", "ed", "ex", "sqlite3"]);
|
|
63
92
|
|
|
64
93
|
type ToolWithEditMode = AgentTool & {
|
|
65
94
|
mode?: unknown;
|
|
@@ -418,6 +447,457 @@ function isDeviceSinkPath(value: string): boolean {
|
|
|
418
447
|
return DEVICE_SINK_PATHS.has(cleanShellWord(value));
|
|
419
448
|
}
|
|
420
449
|
|
|
450
|
+
interface HeredocMaskResult {
|
|
451
|
+
masked: string;
|
|
452
|
+
/** An unquoted-delimiter heredoc body carried `$(…)`/backtick expansion — live code the scanner cannot model. */
|
|
453
|
+
opaqueExpansion: boolean;
|
|
454
|
+
/** The heredoc feeds a stdin applier (patch/ed/ex/sqlite3) whose body mutates files no scan can attribute. */
|
|
455
|
+
mutatingConsumer: boolean;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Quote/substitution state carried ACROSS physical lines so multiline constructs cannot fake heredoc openers. */
|
|
459
|
+
interface ShellQuoteState {
|
|
460
|
+
inSingle: boolean;
|
|
461
|
+
inDouble: boolean;
|
|
462
|
+
/** The open single quote is ANSI-C (`$'…'`): backslash escapes inside it decode at runtime. */
|
|
463
|
+
inAnsiC: boolean;
|
|
464
|
+
/**
|
|
465
|
+
* Paren stack inside substitutions: `true` for a `$(`/`<(`/`>(` opener
|
|
466
|
+
* (its `)` is a word character), `false` for a plain nested paren (its `)`
|
|
467
|
+
* is an operator, so a following `#` starts a comment).
|
|
468
|
+
*/
|
|
469
|
+
parenStack: boolean[];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Mask one physical line for syntax analysis, carrying `state` across lines:
|
|
474
|
+
* quoted spans and backslash-escaped characters become spaces (they are data,
|
|
475
|
+
* never syntax), so a `<<` inside `"…"`/`'…'` — even when the quote opened on a
|
|
476
|
+
* PREVIOUS line — or an escaped separator like `\|` is never misread. An
|
|
477
|
+
* unquoted `#` at a word boundary starts a comment: the rest of the line is
|
|
478
|
+
* blanked without touching quote state, so a stray quote inside a comment
|
|
479
|
+
* cannot poison later lines. A `)` closing a `$(`/`<(`/`>(` substitution is
|
|
480
|
+
* part of the current word (`x=$(true)#literal`), not a comment boundary.
|
|
481
|
+
* `masked` is the same length as the input so match offsets stay aligned.
|
|
482
|
+
* `dequoted` applies bash-like QUOTE REMOVAL instead — quote/backslash marks
|
|
483
|
+
* drop, quoted word characters stay, quoted metacharacters neutralize to `_` —
|
|
484
|
+
* so obfuscated spellings (`e\val`, `'ev'al`) reassemble into their real
|
|
485
|
+
* command word without quoted data ever forming fake command boundaries. A
|
|
486
|
+
* leading `$` immediately before `'…'` (ANSI-C) or `"…"` (locale-translated)
|
|
487
|
+
* is stripped during quote removal too, so `$'eval'`/`$"eval"` dequote to
|
|
488
|
+
* plain `eval`, matching real bash quote-removal semantics for both forms.
|
|
489
|
+
* `continued` reports an unescaped trailing backslash outside quotes (logical
|
|
490
|
+
* line continuation).
|
|
491
|
+
*/
|
|
492
|
+
function maskLineForSyntax(
|
|
493
|
+
line: string,
|
|
494
|
+
state: ShellQuoteState,
|
|
495
|
+
): { masked: string; dequoted: string; continued: boolean; ansiCEscape: boolean } {
|
|
496
|
+
let masked = "";
|
|
497
|
+
let dequoted = "";
|
|
498
|
+
let escaped = false;
|
|
499
|
+
let previous = "";
|
|
500
|
+
let ansiCEscape = false;
|
|
501
|
+
// Quoted data keeps word characters (so `'ev'al` reassembles to `eval`) but
|
|
502
|
+
// neutralizes everything else to `_` so it can never form a command boundary.
|
|
503
|
+
const dequoteData = (character: string): string => (/[\w./-]/.test(character) ? character : "_");
|
|
504
|
+
for (const character of line) {
|
|
505
|
+
if (state.inSingle) {
|
|
506
|
+
if (character === "'") {
|
|
507
|
+
state.inSingle = false;
|
|
508
|
+
state.inAnsiC = false;
|
|
509
|
+
} else {
|
|
510
|
+
// ANSI-C escapes (`$'\145val'`) decode at runtime into characters the
|
|
511
|
+
// dequoted view cannot reproduce — flag them so the caller fails closed.
|
|
512
|
+
if (state.inAnsiC && character === "\\") ansiCEscape = true;
|
|
513
|
+
masked += " ";
|
|
514
|
+
dequoted += dequoteData(character);
|
|
515
|
+
previous = character;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
masked += "'";
|
|
519
|
+
previous = character;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (escaped) {
|
|
523
|
+
escaped = false;
|
|
524
|
+
masked += " ";
|
|
525
|
+
dequoted += dequoteData(character);
|
|
526
|
+
previous = character;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (character === "\\") {
|
|
530
|
+
escaped = true;
|
|
531
|
+
masked += " ";
|
|
532
|
+
previous = character;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (state.inDouble) {
|
|
536
|
+
if (character === '"') state.inDouble = false;
|
|
537
|
+
else {
|
|
538
|
+
masked += " ";
|
|
539
|
+
dequoted += dequoteData(character);
|
|
540
|
+
previous = character;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
masked += '"';
|
|
544
|
+
previous = character;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (
|
|
548
|
+
character === "(" &&
|
|
549
|
+
(previous === "$" || previous === "<" || previous === ">" || state.parenStack.length > 0)
|
|
550
|
+
) {
|
|
551
|
+
// Substitution opener, or any paren nested INSIDE one (e.g. `$((1))`,
|
|
552
|
+
// `$(f (x))`): both must be balanced before the substitution closes.
|
|
553
|
+
state.parenStack.push(previous === "$" || previous === "<" || previous === ">");
|
|
554
|
+
masked += character;
|
|
555
|
+
dequoted += character;
|
|
556
|
+
previous = character;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (character === ")" && state.parenStack.length > 0) {
|
|
560
|
+
const wasSubstitutionOpener = state.parenStack.pop() === true;
|
|
561
|
+
masked += character;
|
|
562
|
+
dequoted += character;
|
|
563
|
+
// A substitution close binds to the surrounding word (`x=$(true)#lit`);
|
|
564
|
+
// a nested subshell close is an operator, so `#` after it comments.
|
|
565
|
+
previous = wasSubstitutionOpener ? "x" : ")";
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (
|
|
569
|
+
character === "#" &&
|
|
570
|
+
(previous === "" || previous === " " || previous === "\t" || ";|&()".includes(previous))
|
|
571
|
+
) {
|
|
572
|
+
// Comment: blank the remainder without evaluating quotes inside it.
|
|
573
|
+
masked += " ".repeat(line.length - masked.length);
|
|
574
|
+
return { masked, dequoted, continued: false, ansiCEscape };
|
|
575
|
+
}
|
|
576
|
+
if (character === "'") {
|
|
577
|
+
state.inSingle = true;
|
|
578
|
+
masked += "'";
|
|
579
|
+
// ANSI-C quoting `$'…'`: bash strips the `$` during quote removal, and
|
|
580
|
+
// escapes inside decode at runtime.
|
|
581
|
+
if (dequoted.endsWith("$")) {
|
|
582
|
+
dequoted = dequoted.slice(0, -1);
|
|
583
|
+
state.inAnsiC = true;
|
|
584
|
+
}
|
|
585
|
+
previous = character;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (character === '"') {
|
|
589
|
+
state.inDouble = true;
|
|
590
|
+
masked += '"';
|
|
591
|
+
// `$"…"` locale-translated strings undergo the same quote removal as
|
|
592
|
+
// ANSI-C `$'…'`: bash drops the `$`, so `$"eval"` dequotes to `eval`.
|
|
593
|
+
if (dequoted.endsWith("$")) dequoted = dequoted.slice(0, -1);
|
|
594
|
+
previous = character;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
masked += character;
|
|
598
|
+
dequoted += character;
|
|
599
|
+
previous = character;
|
|
600
|
+
}
|
|
601
|
+
return { masked, dequoted, continued: escaped, ansiCEscape };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const DATA_CONSUMER_NAMES = new Set(HEREDOC_DATA_CONSUMERS);
|
|
605
|
+
/**
|
|
606
|
+
* Command-position words that force the fail-closed pass. `eval`/`source`/`.`
|
|
607
|
+
* can install a shadow from data the syntax view has blanked (quoted strings,
|
|
608
|
+
* /dev/stdin). `case` introduces pattern `)` terminators that desync the
|
|
609
|
+
* substitution-depth scanner (`$(case x in x) …;; esac)`), making comment
|
|
610
|
+
* detection unreliable — masking is disabled rather than guessing.
|
|
611
|
+
*/
|
|
612
|
+
const SHELL_EVALUATED_COMMANDS = new Set(["eval", "source", ".", "case"]);
|
|
613
|
+
|
|
614
|
+
/** Wrappers whose next word is still the effective command (`builtin eval …`, `command cat …`). */
|
|
615
|
+
const SHELL_COMMAND_WRAPPER_WORDS = new Set(["sudo", "builtin", "command", "exec", "nohup", "nice"]);
|
|
616
|
+
|
|
617
|
+
/** Reserved words that introduce a nested command position (`if eval …`, `while eval …`). */
|
|
618
|
+
const SHELL_RESERVED_PREFIX_WORDS = new Set([
|
|
619
|
+
"if",
|
|
620
|
+
"then",
|
|
621
|
+
"else",
|
|
622
|
+
"elif",
|
|
623
|
+
"while",
|
|
624
|
+
"until",
|
|
625
|
+
"do",
|
|
626
|
+
"time",
|
|
627
|
+
"coproc",
|
|
628
|
+
"{",
|
|
629
|
+
"!",
|
|
630
|
+
]);
|
|
631
|
+
|
|
632
|
+
/** A redirection token (`</dev/null`, `2>err`, `>out`) — not a command word. */
|
|
633
|
+
const SHELL_REDIRECTION_TOKEN_RE = /^\d*[<>]/;
|
|
634
|
+
|
|
635
|
+
/** Command-position words of one command-list segment: the first real word plus words after reserved prefixes. */
|
|
636
|
+
function commandPositionWords(segment: string): string[] {
|
|
637
|
+
const words: string[] = [];
|
|
638
|
+
let expectCommand = true;
|
|
639
|
+
let afterWrapper = false;
|
|
640
|
+
let afterBareRedirection = false;
|
|
641
|
+
for (const word of segment.trim().split(/\s+/)) {
|
|
642
|
+
if (!word) continue;
|
|
643
|
+
// A bare redirection operator (`<`, `2>`) consumes the NEXT word as its target.
|
|
644
|
+
if (afterBareRedirection) {
|
|
645
|
+
afterBareRedirection = false;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (expectCommand) {
|
|
649
|
+
if (/^\w+=/.test(word)) continue;
|
|
650
|
+
// Leading redirections precede the command word (`</dev/null eval …`).
|
|
651
|
+
// A bare operator (`<`, `2>`) additionally consumes its separated target.
|
|
652
|
+
if (SHELL_REDIRECTION_TOKEN_RE.test(word)) {
|
|
653
|
+
if (/^\d*[<>]+$/.test(word)) afterBareRedirection = true;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (SHELL_COMMAND_WRAPPER_WORDS.has(word.toLowerCase())) {
|
|
657
|
+
// The wrapper's effective command follows, possibly after options/`--`.
|
|
658
|
+
afterWrapper = true;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (afterWrapper && (word === "--" || word.startsWith("-"))) {
|
|
662
|
+
// `command -v`/`-V` only DESCRIBES its operand — the rest is data.
|
|
663
|
+
if (/^-[a-zA-Z]*[vV]/.test(word)) return words;
|
|
664
|
+
// Other wrapper options (`command -p`, `builtin --`) precede the real command.
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
afterWrapper = false;
|
|
668
|
+
if (SHELL_RESERVED_PREFIX_WORDS.has(word.toLowerCase())) {
|
|
669
|
+
// Reserved word keeps the NEXT word in command position.
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
words.push(word.split("/").pop()?.toLowerCase() ?? "");
|
|
673
|
+
expectCommand = false;
|
|
674
|
+
}
|
|
675
|
+
// Argument position: only real list operators (handled by the segment
|
|
676
|
+
// split) restore command position — reserved-word TEXT here is data.
|
|
677
|
+
}
|
|
678
|
+
return words;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Command-list segments of a dequoted view (split at `;`, `|`, `&`, `(`, backtick, newline). */
|
|
682
|
+
function commandListSegments(dequotedView: string): string[] {
|
|
683
|
+
return dequotedView.split(/[;|&(`\n]/);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* True when any command-position word of the dequoted syntax view is
|
|
688
|
+
* eval/source/`.` — including nested positions after reserved words
|
|
689
|
+
* (`if eval …`). `echo eval` and `find .` never match because their
|
|
690
|
+
* tokens sit in argument position.
|
|
691
|
+
*/
|
|
692
|
+
function hasShellEvaluatedCommand(dequotedView: string): boolean {
|
|
693
|
+
return commandListSegments(dequotedView).some(segment =>
|
|
694
|
+
commandPositionWords(segment).some(word => SHELL_EVALUATED_COMMANDS.has(word)),
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* True when live syntax actually DECLARES a shadow of an allowlisted
|
|
700
|
+
* data-consumer name: `name()` at a command-list start, `function name`,
|
|
701
|
+
* or `alias name=…` — each in command position, so `echo function cat`
|
|
702
|
+
* (argument position) never matches.
|
|
703
|
+
*/
|
|
704
|
+
function hasDataConsumerShadow(dequotedView: string): boolean {
|
|
705
|
+
for (const segment of commandListSegments(dequotedView)) {
|
|
706
|
+
const words = commandPositionWords(segment);
|
|
707
|
+
for (let index = 0; index < words.length; index++) {
|
|
708
|
+
const word = words[index] ?? "";
|
|
709
|
+
if (word === "function") {
|
|
710
|
+
// commandPositionWords stops after the first non-reserved word, so
|
|
711
|
+
// re-inspect the raw segment for the name following `function`.
|
|
712
|
+
const rawWords = segment.trim().split(/\s+/).filter(Boolean);
|
|
713
|
+
const at = rawWords.findIndex(raw => raw.toLowerCase() === "function");
|
|
714
|
+
const name = rawWords[at + 1]?.split("/").pop()?.toLowerCase() ?? "";
|
|
715
|
+
if (DATA_CONSUMER_NAMES.has(name)) return true;
|
|
716
|
+
}
|
|
717
|
+
if (word === "alias") {
|
|
718
|
+
const rawWords = segment.trim().split(/\s+/).filter(Boolean);
|
|
719
|
+
if (rawWords.some(raw => DATA_CONSUMER_NAMES.has(raw.split("=")[0]?.toLowerCase() ?? ""))) return true;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// `name()` / `name ()` in command position — allow reserved-word prefixes
|
|
724
|
+
// (`then cat() { … }`) before the name (POSIX function definition).
|
|
725
|
+
const nameGroup = [...DATA_CONSUMER_NAMES].join("|");
|
|
726
|
+
const reservedGroup = [...SHELL_RESERVED_PREFIX_WORDS].filter(word => /^\w+$/.test(word)).join("|");
|
|
727
|
+
return new RegExp(
|
|
728
|
+
`(?:^|[;|&{(\`\\n])\\s*(?:(?:${reservedGroup}|\\{|!)\\s+)*(?:${nameGroup})\\s*\\(\\s*\\)`,
|
|
729
|
+
"i",
|
|
730
|
+
).test(dequotedView);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** First command word of a pipeline-segment slice of a quote-masked opener line. */
|
|
734
|
+
function firstCommandWord(segment: string): string {
|
|
735
|
+
return commandPositionWords(segment)[0] ?? "";
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Classify the command consuming the heredoc at offset `at` of the comment-cut,
|
|
740
|
+
* quote-masked opener line — including every DOWNSTREAM pipe stage, because
|
|
741
|
+
* `cat <<'EOF' | bash` hands the body to the interpreter even though `cat` is
|
|
742
|
+
* inert. Only a pipeline whose every stage is an explicitly inert data consumer
|
|
743
|
+
* qualifies for body masking; any stdin applier stage fails closed; anything
|
|
744
|
+
* else (interpreters, awk, unknown binaries) keeps the body live.
|
|
745
|
+
*/
|
|
746
|
+
function heredocConsumerKind(syntaxLine: string, at: number): "data" | "mutating" | "other" {
|
|
747
|
+
let start = 0;
|
|
748
|
+
for (let index = at - 1; index >= 0; index--) {
|
|
749
|
+
const character = syntaxLine[index] ?? "";
|
|
750
|
+
if (";|&(`".includes(character) || character === "\n") {
|
|
751
|
+
start = index + 1;
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// The heredoc's own simple command plus every downstream `|` stage until the
|
|
756
|
+
// command list ends (`;`, `&&`, `||`, `&`, backtick, or subshell close).
|
|
757
|
+
const segments: string[] = [syntaxLine.slice(start, at)];
|
|
758
|
+
let cursor = at;
|
|
759
|
+
while (cursor < syntaxLine.length) {
|
|
760
|
+
const character = syntaxLine[cursor] ?? "";
|
|
761
|
+
if (character === ";" || character === "&" || character === "`" || character === ")") break;
|
|
762
|
+
if (character === "|") {
|
|
763
|
+
if (syntaxLine[cursor + 1] === "|") break;
|
|
764
|
+
let stageEnd = cursor + 1;
|
|
765
|
+
while (stageEnd < syntaxLine.length && !";|&`)".includes(syntaxLine[stageEnd] ?? "")) stageEnd++;
|
|
766
|
+
segments.push(syntaxLine.slice(cursor + 1, stageEnd));
|
|
767
|
+
cursor = stageEnd;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
cursor++;
|
|
771
|
+
}
|
|
772
|
+
let kind: "data" | "mutating" | "other" = "data";
|
|
773
|
+
for (const segment of segments) {
|
|
774
|
+
const base = firstCommandWord(segment);
|
|
775
|
+
if (HEREDOC_MUTATING_CONSUMERS.has(base)) return "mutating";
|
|
776
|
+
if (!HEREDOC_DATA_CONSUMERS.has(base)) kind = "other";
|
|
777
|
+
}
|
|
778
|
+
return kind;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Blank out heredoc BODY lines so document payloads (markdown specs, plans,
|
|
783
|
+
* fixtures) piped to an explicitly inert data consumer (`cat <<'EOF' >
|
|
784
|
+
* /tmp/spec.md`) are not misread as shell commands: body text like `a > b` or
|
|
785
|
+
* stray apostrophes must not register redirection targets or unbalance the
|
|
786
|
+
* quote masker. Bodies stay UNMASKED (scanned as today) for every other
|
|
787
|
+
* consumer — interpreters, awk, unknown binaries — because there the body may
|
|
788
|
+
* be live code. Stdin appliers (patch/ed/ex/sqlite3) flag `mutatingConsumer`
|
|
789
|
+
* and the caller fails closed. Unquoted-delimiter bodies still expand
|
|
790
|
+
* `$(…)`/backticks in real shells, so masked ones flag `opaqueExpansion`. A
|
|
791
|
+
* `<<` inside a comment or a quoted span (including a quote opened on a
|
|
792
|
+
* previous line) is not an opener; a line-continued opener, a shadowed
|
|
793
|
+
* data-consumer name (`cat() { … }`), and an unterminated heredoc all keep
|
|
794
|
+
* their bodies live so the scanner stays fail-closed rather than blind.
|
|
795
|
+
*/
|
|
796
|
+
function maskHeredocBodies(command: string): HeredocMaskResult {
|
|
797
|
+
if (!command.includes("<<")) return { masked: command, opaqueExpansion: false, mutatingConsumer: false };
|
|
798
|
+
const first = maskHeredocBodiesPass(command, false);
|
|
799
|
+
// Fail closed on live-syntax constructs that can install a shadow the syntax
|
|
800
|
+
// view cannot see: a function/alias definition of an allowlisted consumer
|
|
801
|
+
// name, or a command-position eval/source/`.` whose argument is blanked
|
|
802
|
+
// quoted data. Both run against the DEQUOTED view so quoting tricks
|
|
803
|
+
// (`e\val`, `'ev'al`) cannot hide the word, while heredoc bodies and
|
|
804
|
+
// comments stay excluded — a spec that merely DOCUMENTS `cat() { … }` or
|
|
805
|
+
// mentions "eval" in prose stays inert.
|
|
806
|
+
if (first.ansiCEscape || hasDataConsumerShadow(first.dequotedView) || hasShellEvaluatedCommand(first.dequotedView)) {
|
|
807
|
+
return maskHeredocBodiesPass(command, true).result;
|
|
808
|
+
}
|
|
809
|
+
return first.result;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function maskHeredocBodiesPass(
|
|
813
|
+
command: string,
|
|
814
|
+
shadowed: boolean,
|
|
815
|
+
): { result: HeredocMaskResult; dequotedView: string; ansiCEscape: boolean } {
|
|
816
|
+
const lines = command.split("\n");
|
|
817
|
+
const out: string[] = [];
|
|
818
|
+
const dequotedLines: string[] = [];
|
|
819
|
+
const state: ShellQuoteState = { inSingle: false, inDouble: false, inAnsiC: false, parenStack: [] };
|
|
820
|
+
let previousContinued = false;
|
|
821
|
+
let opaqueExpansion = false;
|
|
822
|
+
let mutatingConsumer = false;
|
|
823
|
+
let sawAnsiCEscape = false;
|
|
824
|
+
for (let index = 0; index < lines.length; index++) {
|
|
825
|
+
const line = lines[index] ?? "";
|
|
826
|
+
out.push(line);
|
|
827
|
+
// Cross-line quote masking decides whether a `<<` is real syntax or inert
|
|
828
|
+
// data — even when the enclosing quote opened on a previous line.
|
|
829
|
+
const { masked, dequoted, continued, ansiCEscape } = maskLineForSyntax(line, state);
|
|
830
|
+
if (ansiCEscape) sawAnsiCEscape = true;
|
|
831
|
+
const syntax = masked;
|
|
832
|
+
const isContinuationLine = previousContinued;
|
|
833
|
+
// Bash removes a backslash-newline pair entirely: fold a continuation into
|
|
834
|
+
// the previous dequoted line so split command words (`e\` + `val`)
|
|
835
|
+
// reassemble for command/shadow detection.
|
|
836
|
+
if (isContinuationLine && dequotedLines.length > 0) {
|
|
837
|
+
let previousDequoted = dequotedLines[dequotedLines.length - 1] ?? "";
|
|
838
|
+
// A continuation can also split ANSI-C/locale quoting (`$\` + `'eval'` or
|
|
839
|
+
// `$\` + `"eval"`): the rejoined `$'`/`$"` still drops its `$` during
|
|
840
|
+
// quote removal.
|
|
841
|
+
if (previousDequoted.endsWith("$") && (line.startsWith("'") || line.startsWith('"'))) {
|
|
842
|
+
previousDequoted = previousDequoted.slice(0, -1);
|
|
843
|
+
// The masker already opened this quote as ordinary (it ran before the
|
|
844
|
+
// fold), so re-check the rejoined ANSI-C span for runtime-decoding
|
|
845
|
+
// escapes (`$\` + `'\145val'`) it could not have flagged.
|
|
846
|
+
if (line.startsWith("'") && /^'[^']*\\/.test(line)) sawAnsiCEscape = true;
|
|
847
|
+
}
|
|
848
|
+
dequotedLines[dequotedLines.length - 1] = previousDequoted + dequoted;
|
|
849
|
+
} else {
|
|
850
|
+
dequotedLines.push(dequoted);
|
|
851
|
+
}
|
|
852
|
+
previousContinued = continued;
|
|
853
|
+
for (const match of line.matchAll(BASH_HEREDOC_OPEN_RE)) {
|
|
854
|
+
const at = match.index ?? 0;
|
|
855
|
+
if (syntax.slice(at, at + 2) !== "<<") continue;
|
|
856
|
+
const delimiter = match[2] ?? match[3] ?? match[5] ?? "";
|
|
857
|
+
if (!delimiter) continue;
|
|
858
|
+
let kind = heredocConsumerKind(syntax, at);
|
|
859
|
+
// A continued/continuation opener line hides part of the pipeline
|
|
860
|
+
// (`cat <<'EOF' \` / `| bash`); a shadowed consumer name is untrustworthy.
|
|
861
|
+
// Both downgrade masking eligibility, never block eligibility.
|
|
862
|
+
if (kind === "data" && (shadowed || continued || isContinuationLine)) kind = "other";
|
|
863
|
+
if (kind === "mutating") mutatingConsumer = true;
|
|
864
|
+
const quoted = match[2] !== undefined || match[3] !== undefined || match[4] !== undefined;
|
|
865
|
+
const stripTabs = match[1] === "-";
|
|
866
|
+
let end = -1;
|
|
867
|
+
for (let scan = index + 1; scan < lines.length; scan++) {
|
|
868
|
+
const candidate = stripTabs ? (lines[scan] ?? "").replace(/^\t+/, "") : (lines[scan] ?? "");
|
|
869
|
+
if (candidate === delimiter) {
|
|
870
|
+
end = scan;
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (end === -1) {
|
|
875
|
+
return {
|
|
876
|
+
result: { masked: command, opaqueExpansion, mutatingConsumer },
|
|
877
|
+
dequotedView: dequotedLines.join("\n"),
|
|
878
|
+
ansiCEscape: sawAnsiCEscape,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const maskBody = kind === "data" || kind === "mutating";
|
|
882
|
+
for (let body = index + 1; body < end; body++) {
|
|
883
|
+
const bodyLine = lines[body] ?? "";
|
|
884
|
+
if (maskBody && !quoted && /\$\(|`/.test(bodyLine)) opaqueExpansion = true;
|
|
885
|
+
out.push(maskBody ? "" : bodyLine);
|
|
886
|
+
}
|
|
887
|
+
out.push(lines[end] ?? "");
|
|
888
|
+
index = end;
|
|
889
|
+
// Heredoc bodies are data: they never affect the shell quote state or
|
|
890
|
+
// the syntax view used for shadow detection.
|
|
891
|
+
previousContinued = false;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
result: { masked: out.join("\n"), opaqueExpansion, mutatingConsumer },
|
|
896
|
+
dequotedView: dequotedLines.join("\n"),
|
|
897
|
+
ansiCEscape: sawAnsiCEscape,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
421
901
|
function extractBashTargets(args: unknown, depth = 0): ExtractedTargets {
|
|
422
902
|
const record = getRecord(args);
|
|
423
903
|
const command = safeString(record?.command);
|
|
@@ -440,10 +920,21 @@ function extractBashTargets(args: unknown, depth = 0): ExtractedTargets {
|
|
|
440
920
|
if (nested.unknown) targets.unknown = true;
|
|
441
921
|
if (nested.explicitMutation) targets.explicitMutation = true;
|
|
442
922
|
}
|
|
923
|
+
// Heredoc bodies fed to data consumers are inert document payloads; mask them
|
|
924
|
+
// so spec/plan text cannot fake redirections. Script-consumer bodies survive
|
|
925
|
+
// the mask and are still scanned as live code below.
|
|
926
|
+
const heredoc = maskHeredocBodies(command);
|
|
927
|
+
if (heredoc.opaqueExpansion || heredoc.mutatingConsumer) {
|
|
928
|
+
targets.explicitMutation = true;
|
|
929
|
+
targets.unknown = true;
|
|
930
|
+
}
|
|
443
931
|
// Nested scripts were read from the raw text above; every scanner below works
|
|
444
932
|
// on the masked view so quoted argument data cannot look like a redirection.
|
|
445
|
-
const scanned = maskSingleQuotedSpans(
|
|
446
|
-
if (
|
|
933
|
+
const scanned = maskSingleQuotedSpans(heredoc.masked);
|
|
934
|
+
if (
|
|
935
|
+
BASH_OPAQUE_INTERPRETER_WRITE_RE.test(heredoc.masked) ||
|
|
936
|
+
BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE.test(heredoc.masked)
|
|
937
|
+
) {
|
|
447
938
|
targets.explicitMutation = true;
|
|
448
939
|
targets.unknown = true;
|
|
449
940
|
}
|
|
@@ -502,6 +993,9 @@ function extractBashTargets(args: unknown, depth = 0): ExtractedTargets {
|
|
|
502
993
|
for (const part of targetParts) {
|
|
503
994
|
const cleaned = cleanShellWord(part);
|
|
504
995
|
if (!cleaned || cleaned.startsWith("-")) continue;
|
|
996
|
+
// Redirection/heredoc operator words (`>/dev/null`, `<<'DOC'`, `2>&1`) are
|
|
997
|
+
// not argument paths; their targets are captured by the redirect scanners.
|
|
998
|
+
if (/^\d*[<>]/.test(cleaned)) continue;
|
|
505
999
|
addPath(targets, cleaned);
|
|
506
1000
|
}
|
|
507
1001
|
}
|