one-code-extension 0.4.1 → 0.5.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/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -11,24 +11,27 @@
|
|
|
11
11
|
* whose repository is decided at runtime (`xargs`/`parallel`/`find -exec`
|
|
12
12
|
* feeding it arguments, a `-C "$DIR"` computed from a variable, a `cd`
|
|
13
13
|
* whose destination cannot be resolved), or that points into the shared
|
|
14
|
-
* checkout or a sibling worktree (`git -C`, `--git-dir`, `--work-tree`,
|
|
15
|
-
*
|
|
14
|
+
* checkout or a sibling worktree (`git -C`, `--git-dir`, `--work-tree`,
|
|
15
|
+
* `-c core.worktree=`, a `GIT_DIR`/`GIT_WORK_TREE`/`GIT_COMMON_DIR`
|
|
16
|
+
* assignment before git or exported earlier in the line, a preceding
|
|
17
|
+
* `cd`), is refused with the fix named. Git against an unrelated
|
|
16
18
|
* repository elsewhere on disk is left alone — the permission gate and
|
|
17
19
|
* auto-mode still apply to it.
|
|
18
20
|
* 2. The stash stack is shared by the main checkout and every worktree of the
|
|
19
21
|
* repository, and parallel sessions can interleave on it. Stash forms that
|
|
20
22
|
* collide with a concurrent session (untagged push, `pop`, `clear`,
|
|
21
|
-
* ref-less `drop`) are refused with the tag + apply-by-SHA recipe.
|
|
23
|
+
* ref-less `drop` and `branch`) are refused with the tag + apply-by-SHA recipe.
|
|
22
24
|
*
|
|
23
|
-
* Steering, not security:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
25
|
+
* Steering, not security: bash (and `monitor`, which runs bash) is inspected
|
|
26
|
+
* here, PowerShell in powershell-guards.ts, and an unparseable command is
|
|
27
|
+
* refused only when it visibly involves git (fail closed on the invariant,
|
|
28
|
+
* fail open on everything else).
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
31
|
import { homedir } from "node:os";
|
|
29
32
|
import { isAbsolute } from "node:path";
|
|
30
|
-
import { isWithin, toAbsoluteBash } from "../auto-mode/paths.ts";
|
|
31
|
-
import { gitSubcommand, leadTokens, parseCommand, resolvePayload, type Token } from "../auto-mode/shell-analysis.ts";
|
|
33
|
+
import { isWithin, resolveForContainment, toAbsoluteBash } from "../auto-mode/paths.ts";
|
|
34
|
+
import { gitSubcommand, INLINE_SCRIPT_SHELLS, isUnknownTilde, leadTokens, LOOPS, movesDirectory, parseCommand, resolvePayload, scopedTracker, type Token } from "../auto-mode/shell-analysis.ts";
|
|
32
35
|
|
|
33
36
|
export interface WorktreeGuardContext {
|
|
34
37
|
command: string;
|
|
@@ -36,15 +39,59 @@ export interface WorktreeGuardContext {
|
|
|
36
39
|
worktreePath: string;
|
|
37
40
|
/** Root of the shared checkout the worktree belongs to. */
|
|
38
41
|
sharedRoot: string;
|
|
42
|
+
/**
|
|
43
|
+
* Every worktree of the repository (`git worktree list`). A linked worktree
|
|
44
|
+
* can live outside the shared root, and git aimed at it is refused too.
|
|
45
|
+
*/
|
|
46
|
+
otherWorktrees?: readonly string[];
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Whether a git target lies in a root, judged where both resolve: git lists
|
|
51
|
+
* worktrees by their real paths (`/private/var/…` on macOS) while the session
|
|
52
|
+
* and the model may spell a directory through a link (`/var/…`), and a link
|
|
53
|
+
* inside one worktree may lead anywhere. Lexical only when resolution fails.
|
|
54
|
+
*/
|
|
55
|
+
export const containsPath = (root: string, target: string): boolean => {
|
|
56
|
+
const resolvedRoot = resolveForContainment(root);
|
|
57
|
+
const resolvedTarget = resolveForContainment(target);
|
|
58
|
+
return resolvedRoot !== undefined && resolvedTarget !== undefined ? isWithin(resolvedRoot, resolvedTarget) : isWithin(root, target);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The roots a git command must not target: the shared checkout and every other worktree. */
|
|
62
|
+
export const sharedRepositoryRoots = ({ sharedRoot, otherWorktrees }: Pick<WorktreeGuardContext, "sharedRoot" | "otherWorktrees">): string[] => [sharedRoot, ...(otherWorktrees ?? [])];
|
|
63
|
+
|
|
41
64
|
/** Expansion syntax the guard cannot resolve statically. */
|
|
42
65
|
const hasExpansion = (value: string) => /[$`]/.test(value);
|
|
43
66
|
|
|
44
|
-
const isolated = (worktreePath: string, problem: string, fix: string): string =>
|
|
67
|
+
export const isolated = (worktreePath: string, problem: string, fix: string): string =>
|
|
45
68
|
`This session is isolated in the worktree ${worktreePath}, but ${problem}. ` +
|
|
46
69
|
`Refusing to run it — a worktree-isolated session's git operations must target its own worktree. ${fix}`;
|
|
47
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Whether a shell command may run git, so its targets need the repository's
|
|
73
|
+
* worktree list: any spelling the parse resolves to git (`g"it"`, `\\git`,
|
|
74
|
+
* `$'\\x67it'`, `env git`), git passed to a runner, git in a script a shell
|
|
75
|
+
* runs, or a line that does not parse. Over-matching costs one `git worktree list`.
|
|
76
|
+
*/
|
|
77
|
+
export function namesGit(command: string): boolean {
|
|
78
|
+
if (mentionsGit(command)) return true;
|
|
79
|
+
const { segments, parseFailed } = parseCommand(command);
|
|
80
|
+
if (parseFailed) return true;
|
|
81
|
+
return segments.some((seg) => {
|
|
82
|
+
const { command: cmd, args } = resolvePayload(leadTokens(seg));
|
|
83
|
+
return cmd === "git" || args.some((arg) => arg.value === "git" || mentionsGit(arg.value));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The worktree list could not be read, so a git command's target cannot be checked: refuse it. */
|
|
88
|
+
export const unlistedWorktreesReason = (worktreePath: string): string =>
|
|
89
|
+
isolated(
|
|
90
|
+
worktreePath,
|
|
91
|
+
"the repository's worktrees could not be listed, so the repository this git command targets cannot be verified",
|
|
92
|
+
`Retry; if it keeps failing, run git from ${worktreePath} without -C, --git-dir or --work-tree.`,
|
|
93
|
+
);
|
|
94
|
+
|
|
48
95
|
function stashMessage(form: string, hazard: string): string {
|
|
49
96
|
return (
|
|
50
97
|
`Blocked: \`${form}\` — the git stash stack is shared between the main checkout and every worktree of this repository, and parallel sessions may push or pop entries concurrently; ${hazard}. ` +
|
|
@@ -54,7 +101,7 @@ function stashMessage(form: string, hazard: string): string {
|
|
|
54
101
|
);
|
|
55
102
|
}
|
|
56
103
|
|
|
57
|
-
function stashReason(rest: Token[]): string | undefined {
|
|
104
|
+
export function stashReason(rest: Pick<Token, "value">[]): string | undefined {
|
|
58
105
|
const words = rest.map((t) => t.value);
|
|
59
106
|
const sub = words.find((w) => !w.startsWith("-"));
|
|
60
107
|
// `-m` may ride in a short-flag bundle (`git stash push -um wip`).
|
|
@@ -80,19 +127,98 @@ function stashReason(rest: Token[]): string | undefined {
|
|
|
80
127
|
return stashMessage("git stash drop", "a ref-less `drop` deletes whatever is currently stash@{0}, which may be another session's entry");
|
|
81
128
|
}
|
|
82
129
|
}
|
|
130
|
+
if (sub === "branch") {
|
|
131
|
+
// `git stash branch <name> [<stash>]`: without the stash ref it pops stash@{0}.
|
|
132
|
+
const operands = words.slice(words.indexOf("branch") + 1).filter((w) => !w.startsWith("-"));
|
|
133
|
+
if (operands.length < 2) {
|
|
134
|
+
return stashMessage("git stash branch", "without a stash ref, `branch` pops whatever is currently stash@{0}, which may be another session's entry");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
83
137
|
return undefined;
|
|
84
138
|
}
|
|
85
139
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
/** Environment variables that point git at a repository, like `--git-dir` and `--work-tree`. */
|
|
141
|
+
const GIT_REPOSITORY_ENV = /^(GIT_DIR|GIT_WORK_TREE|GIT_COMMON_DIR)=/;
|
|
142
|
+
|
|
143
|
+
/** `declare -x` / `typeset -gx`: the builtins that export like `export`. */
|
|
144
|
+
const exportsLikeExport = (cmd: string, args: Token[]) =>
|
|
145
|
+
cmd === "export" || ((cmd === "declare" || cmd === "typeset") && args.some((arg) => /^-[a-zA-Z]*x/.test(arg.value)));
|
|
146
|
+
|
|
147
|
+
export function worktreeBashGuardReason(context: WorktreeGuardContext): string | undefined {
|
|
148
|
+
return guardScript(context.command, context.worktreePath, sharedRepositoryRoots(context), context.worktreePath, 0).reason;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Judge one script that starts in `startDir` (undefined: not statically
|
|
153
|
+
* known). Returns the refusal, if any, and whether the script can change the
|
|
154
|
+
* directory of the shell it runs in (what an `eval` of it would do).
|
|
155
|
+
*/
|
|
156
|
+
/** Builtins that run a script in this shell. */
|
|
157
|
+
const SCRIPT_RUNNERS: ReadonlySet<string> = new Set(["eval", "source", "."]);
|
|
158
|
+
|
|
159
|
+
/** Shell options that take the next word as their value. */
|
|
160
|
+
const SHELL_VALUE_OPTIONS = new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The command lines a shell invocation runs: with `-c` (alone or in a cluster
|
|
164
|
+
* such as `-lc`), the first operand; the words after it are `$0`, `$1`, …
|
|
165
|
+
* (`bash -c 'echo ok' 'git status'` runs no git), unless the script expands
|
|
166
|
+
* them (`bash -c '"$@"' _ git status`), when they are a command line too.
|
|
167
|
+
* Without `-c`, every argument, since which one is a script file is not known.
|
|
168
|
+
*/
|
|
169
|
+
function shellScripts(args: Token[]): string[] {
|
|
170
|
+
let command = false;
|
|
171
|
+
let i = 0;
|
|
172
|
+
for (; i < args.length; i++) {
|
|
173
|
+
const value = args[i].value;
|
|
174
|
+
if (value === "--" || value === "-") {
|
|
175
|
+
i++;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
if (!/^[-+]/.test(value)) break;
|
|
179
|
+
if (SHELL_VALUE_OPTIONS.has(value)) i++;
|
|
180
|
+
else if (/^-[A-Za-z]+$/.test(value) && value.includes("c")) command = true;
|
|
181
|
+
}
|
|
182
|
+
const values = args.map((arg) => arg.value);
|
|
183
|
+
if (!command) return values;
|
|
184
|
+
const [script, ...positional] = values.slice(i);
|
|
185
|
+
if (script === undefined) return [];
|
|
186
|
+
// `$0`…`$9`, `$@`, `$*`, `${1}`, `${@}`: read both from `$0` and from `$1`.
|
|
187
|
+
if (!/\$(?:[0-9@*]|\{[0-9@*])/.test(script) || positional.length === 0) return [script];
|
|
188
|
+
return [script, positional.join(" "), positional.slice(1).join(" ")];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Whether git appears as a word in `text`, read with quotes and backslashes removed (`gi\t`, `g"it"`). */
|
|
192
|
+
const mentionsGit = (text: string): boolean => /\bgit\b/.test(text.replace(/[\\'"]/g, ""));
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A script the guard cannot follow (it failed to parse, or is nested too deep):
|
|
196
|
+
* refused if git appears anywhere in it, and a `cd` in it leaves the directory
|
|
197
|
+
* unknown.
|
|
198
|
+
*/
|
|
199
|
+
function unverifiedScript(command: string, worktreePath: string, mayMove: boolean): { reason?: string; moves?: boolean } {
|
|
200
|
+
if (!mentionsGit(command)) return { moves: mayMove && /\b(?:cd|pushd|popd)\b/.test(command) };
|
|
201
|
+
return {
|
|
202
|
+
reason: isolated(
|
|
91
203
|
worktreePath,
|
|
92
204
|
"this command is too complex to verify that its git operations stay inside the worktree",
|
|
93
205
|
`Break it into plain, separate git commands with literal paths and run them from ${worktreePath}.`,
|
|
94
|
-
)
|
|
95
|
-
}
|
|
206
|
+
),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** How many directories a segment may run in before the guard treats its directory as unknown. */
|
|
211
|
+
const MAX_POSSIBLE_DIRS = 8;
|
|
212
|
+
|
|
213
|
+
function guardScript(
|
|
214
|
+
command: string,
|
|
215
|
+
worktreePath: string,
|
|
216
|
+
sharedRoots: readonly string[],
|
|
217
|
+
startDir: string | undefined,
|
|
218
|
+
depth: number,
|
|
219
|
+
): { reason?: string; moves?: boolean } {
|
|
220
|
+
const { segments, parseFailed } = parseCommand(command);
|
|
221
|
+
if (parseFailed || segments.length === 0) return unverifiedScript(command, worktreePath, parseFailed);
|
|
96
222
|
|
|
97
223
|
// Claude Code's exact refusal for stdin-fed git (one static string for
|
|
98
224
|
// both mechanisms) — keep it byte-identical to the capture.
|
|
@@ -103,38 +229,141 @@ export function worktreeBashGuardReason({ command, worktreePath, sharedRoot }: W
|
|
|
103
229
|
`Run the equivalent from ${worktreePath} without the redirect.`,
|
|
104
230
|
);
|
|
105
231
|
|
|
106
|
-
/** Directory
|
|
107
|
-
let dir: string | undefined =
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
232
|
+
/** Directory the current segment runs in; undefined = not statically known. */
|
|
233
|
+
let dir: string | undefined = startDir;
|
|
234
|
+
/**
|
|
235
|
+
* Repository variables an earlier `export` in this line set, by name:
|
|
236
|
+
* the directory they name, or undefined when it is not statically known.
|
|
237
|
+
* Later git in the line runs with them. Subshell scopes are ignored, which
|
|
238
|
+
* can only refuse more.
|
|
239
|
+
*/
|
|
240
|
+
const exportedGitEnv = new Map<string, string | undefined>();
|
|
241
|
+
const envTarget = (value: string, token: Token | undefined): string | undefined =>
|
|
242
|
+
!value || hasExpansion(value) || token?.dynamic || token?.glob || dir === undefined ? undefined : toAbsoluteBash(dir, value, homedir());
|
|
243
|
+
/**
|
|
244
|
+
* The directories a segment may run in, per subshell scope
|
|
245
|
+
* (`Segment.scopes`): a `cd` inside `( … )`, a substitution or a pipeline
|
|
246
|
+
* member does not leak out. A `cd` that may not run (`false && cd x`, an
|
|
247
|
+
* `if` branch) leaves both the old and the new directory possible, and every
|
|
248
|
+
* git command must be safe in each.
|
|
249
|
+
*/
|
|
250
|
+
const scopeDirs = scopedTracker<(string | undefined)[]>([startDir]);
|
|
251
|
+
|
|
252
|
+
// A git command as the parse sees it, spelled any way (`gi\t`, `env git`,
|
|
253
|
+
// `xargs git`, `parallel git`), or anywhere in the script a shell or `eval`
|
|
254
|
+
// runs (`bash -c '…'`), read as text with quotes and backslashes removed so
|
|
255
|
+
// a script too complex to parse still counts.
|
|
256
|
+
const runsGit = (seg: (typeof segments)[number]) => {
|
|
257
|
+
const { command: cmd, args } = resolvePayload(leadTokens(seg));
|
|
258
|
+
if (cmd === "git" || (cmd === "parallel" && args.some((arg) => arg.value === "git"))) return true;
|
|
259
|
+
if (!INLINE_SCRIPT_SHELLS.has(cmd) && !SCRIPT_RUNNERS.has(cmd)) return false;
|
|
260
|
+
return args.some((arg) => mentionsGit(arg.value));
|
|
261
|
+
};
|
|
262
|
+
// A loop body runs more than once, so a `cd` in it moves every git command
|
|
263
|
+
// in the loop after the first pass; the segments show one pass only.
|
|
264
|
+
// An `eval` there may `cd` too.
|
|
265
|
+
const movesOrEvals = (seg: (typeof segments)[number]) => movesDirectory(seg) || resolvePayload(leadTokens(seg)).command === "eval";
|
|
266
|
+
const movesInLoop = segments.some((seg) => seg.enclosing.some((construct) => LOOPS.has(construct)) && movesOrEvals(seg));
|
|
267
|
+
if (movesInLoop && segments.some(runsGit)) {
|
|
268
|
+
return {
|
|
269
|
+
reason: isolated(
|
|
270
|
+
worktreePath,
|
|
271
|
+
"this command changes directory inside a loop, so the repository its git commands target cannot be verified",
|
|
272
|
+
`Break it into plain, separate git commands with literal paths and run them from ${worktreePath}.`,
|
|
273
|
+
),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// The last member of a pipeline runs in the current shell under `shopt -s
|
|
277
|
+
// lastpipe`, so its `cd` may move every later command.
|
|
278
|
+
// Only a git command after such a `cd` can be moved by it.
|
|
279
|
+
// Inside a substitution too (`$(echo | cd /repo; git status)`), but only for
|
|
280
|
+
// git later in the shell that tail runs in.
|
|
281
|
+
const inShell = (seg: (typeof segments)[number], shell: number[]) => shell.every((scope, depth) => seg.scopes[depth] === scope);
|
|
282
|
+
const pipeMoved = segments.some(
|
|
283
|
+
(cd, index) => cd.pipelineShell && movesDirectory(cd) && segments.slice(index + 1).some((seg) => inShell(seg, cd.pipelineShell!) && runsGit(seg)),
|
|
284
|
+
);
|
|
285
|
+
if (pipeMoved) {
|
|
286
|
+
return {
|
|
287
|
+
reason: isolated(
|
|
288
|
+
worktreePath,
|
|
289
|
+
"this command changes directory in the last command of a pipeline, which can run in the current shell, so the repository its git commands target cannot be verified",
|
|
290
|
+
`Break it into plain, separate git commands with literal paths and run them from ${worktreePath}.`,
|
|
291
|
+
),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
111
294
|
|
|
112
295
|
const checkSegment = (seg: (typeof segments)[number]): string | undefined => {
|
|
113
296
|
const tokens = leadTokens(seg);
|
|
114
297
|
const rawLead = tokens[0]?.value;
|
|
115
298
|
if (!rawLead) return undefined;
|
|
299
|
+
// The command a wrapper runs (`builtin cd`, `command cd`), not the first word.
|
|
300
|
+
const { command: lead, args: leadArgs } = resolvePayload(tokens);
|
|
116
301
|
|
|
117
|
-
if (
|
|
302
|
+
if (lead === "cd") {
|
|
118
303
|
// Skip cd's own options (-P/-L/-e/-@); `--` ends them; a lone `-`
|
|
119
304
|
// means the previous directory, which is not statically known.
|
|
120
|
-
let j =
|
|
121
|
-
while (j <
|
|
122
|
-
if (
|
|
123
|
-
const
|
|
305
|
+
let j = 0;
|
|
306
|
+
while (j < leadArgs.length && leadArgs[j].value.startsWith("-") && !["-", "--"].includes(leadArgs[j].value)) j++;
|
|
307
|
+
if (leadArgs[j]?.value === "--") j++;
|
|
308
|
+
const targetToken = leadArgs[j];
|
|
309
|
+
const target = targetToken?.value;
|
|
124
310
|
if (!target) dir = homedir();
|
|
125
|
-
|
|
311
|
+
// A glob (`cd /r*po`), an expansion or `~user` lands wherever bash expands it.
|
|
312
|
+
else if (hasExpansion(target) || target === "-" || targetToken.glob || targetToken.dynamic || isUnknownTilde(target)) dir = undefined;
|
|
126
313
|
// An absolute (or ~) destination re-anchors the tracked directory
|
|
127
314
|
// even when it was unknown — later git commands become checkable again.
|
|
128
315
|
else if (isAbsolute(target) || target === "~" || target.startsWith("~/")) dir = toAbsoluteBash("/", target, homedir());
|
|
129
316
|
else if (dir !== undefined) dir = toAbsoluteBash(dir, target, homedir());
|
|
130
317
|
return undefined;
|
|
131
318
|
}
|
|
132
|
-
if (
|
|
319
|
+
if (lead === "pushd" || lead === "popd") {
|
|
133
320
|
dir = undefined;
|
|
134
321
|
return undefined;
|
|
135
322
|
}
|
|
136
323
|
|
|
137
324
|
const { command: cmd, args, peeled } = resolvePayload(tokens);
|
|
325
|
+
// Git behind a wrapper the strict reading does not peel (`sudo`, `exec`,
|
|
326
|
+
// `env -C /repo`): the wrapper may have moved it, so its target cannot be
|
|
327
|
+
// read off the command.
|
|
328
|
+
// A shell, `eval` or `source` in command position is judged below instead.
|
|
329
|
+
if (cmd !== "git" && !SCRIPT_RUNNERS.has(cmd) && !INLINE_SCRIPT_SHELLS.has(cmd)) {
|
|
330
|
+
const wide = resolvePayload(tokens, "wide");
|
|
331
|
+
const runsScript = INLINE_SCRIPT_SHELLS.has(wide.command) || SCRIPT_RUNNERS.has(wide.command);
|
|
332
|
+
const shellScript = runsScript && wide.args.some((arg) => mentionsGit(arg.value));
|
|
333
|
+
if (wide.command === "git" || shellScript || wide.scripts.some(mentionsGit)) {
|
|
334
|
+
return isolated(
|
|
335
|
+
worktreePath,
|
|
336
|
+
`this command runs git through ${rawLead}, which can change the repository it targets`,
|
|
337
|
+
`Run git directly, with literal paths inside ${worktreePath}.`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// A script a shell or `eval` runs is judged as commands of its own,
|
|
343
|
+
// starting where this command runs: `bash -c 'cd /repo && git status'`
|
|
344
|
+
// runs git in /repo. A shell's script runs in a child, but `eval` runs
|
|
345
|
+
// in this shell, so a `cd` in it moves the commands after it.
|
|
346
|
+
// Past three levels a script is judged as one the guard cannot follow.
|
|
347
|
+
if (INLINE_SCRIPT_SHELLS.has(cmd) || cmd === "eval") {
|
|
348
|
+
const scripts = cmd === "eval" ? [args.map((arg) => arg.value).join(" ")] : shellScripts(args);
|
|
349
|
+
for (const script of scripts) {
|
|
350
|
+
const nested = depth < 3 ? guardScript(script, worktreePath, sharedRoots, dir, depth + 1) : unverifiedScript(script, worktreePath, true);
|
|
351
|
+
if (nested.reason) return nested.reason;
|
|
352
|
+
if (cmd === "eval" && nested.moves) dir = undefined;
|
|
353
|
+
}
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// `source <(…)`, `. "$f"`: the script is only known at runtime, so git in
|
|
358
|
+
// its source text cannot be checked.
|
|
359
|
+
// It runs in this shell, so a `cd` in it would move later git: the
|
|
360
|
+
// directory is unknown after it. A literal file (`source .venv/bin/activate`)
|
|
361
|
+
// keeps it: its text is not the guard's to read.
|
|
362
|
+
if ((cmd === "source" || cmd === ".") && args.some((arg) => arg.dynamic)) {
|
|
363
|
+
const unverified = unverifiedScript(seg.raw, worktreePath, false);
|
|
364
|
+
if (unverified.reason) return unverified.reason;
|
|
365
|
+
dir = undefined;
|
|
366
|
+
}
|
|
138
367
|
|
|
139
368
|
// Git whose arguments are assembled at runtime — the repository it will
|
|
140
369
|
// target cannot be read off the command.
|
|
@@ -152,6 +381,16 @@ export function worktreeBashGuardReason({ command, worktreePath, sharedRoot }: W
|
|
|
152
381
|
);
|
|
153
382
|
}
|
|
154
383
|
|
|
384
|
+
if (exportsLikeExport(cmd, args)) {
|
|
385
|
+
for (const arg of args) {
|
|
386
|
+
const name = /^(GIT_DIR|GIT_WORK_TREE|GIT_COMMON_DIR)(=|$)/.exec(arg.value);
|
|
387
|
+
if (!name) continue;
|
|
388
|
+
// `export GIT_DIR` (no value) exports whatever it was set to earlier.
|
|
389
|
+
exportedGitEnv.set(name[1], name[2] ? envTarget(arg.value.slice(name[0].length), arg) : undefined);
|
|
390
|
+
}
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
155
394
|
if (cmd !== "git") return undefined;
|
|
156
395
|
|
|
157
396
|
if (dir === undefined) {
|
|
@@ -165,15 +404,47 @@ export function worktreeBashGuardReason({ command, worktreePath, sharedRoot }: W
|
|
|
165
404
|
// Effective repository target: the tracked cwd, adjusted by global flags.
|
|
166
405
|
let effective = dir;
|
|
167
406
|
const extraTargets: string[] = [];
|
|
407
|
+
// `GIT_DIR=… git`, `env GIT_WORK_TREE=… git`, and an earlier `export`:
|
|
408
|
+
// the environment spelling of `--git-dir` / `--work-tree`.
|
|
409
|
+
const envRefusal = (name: string) =>
|
|
410
|
+
isolated(
|
|
411
|
+
worktreePath,
|
|
412
|
+
`\`${name}\` points git at a repository that is decided at runtime, so it cannot be verified`,
|
|
413
|
+
`Drop the ${name} setting and run git from ${worktreePath}.`,
|
|
414
|
+
);
|
|
415
|
+
const prefix = tokens.slice(0, tokens.length - args.length - 1);
|
|
416
|
+
const assigned = new Map(exportedGitEnv);
|
|
417
|
+
for (const token of prefix) {
|
|
418
|
+
if (!GIT_REPOSITORY_ENV.test(token.value)) continue;
|
|
419
|
+
const eq = token.value.indexOf("=");
|
|
420
|
+
assigned.set(token.value.slice(0, eq), envTarget(token.value.slice(eq + 1), token));
|
|
421
|
+
}
|
|
422
|
+
for (const [name, target] of assigned) {
|
|
423
|
+
if (target === undefined) return envRefusal(name);
|
|
424
|
+
extraTargets.push(target);
|
|
425
|
+
}
|
|
168
426
|
let i = 0;
|
|
169
427
|
while (i < args.length) {
|
|
170
428
|
const value = args[i].value;
|
|
171
429
|
if (!value.startsWith("-")) break;
|
|
430
|
+
// `-c core.worktree=<dir>` is `--work-tree` spelled as config;
|
|
431
|
+
// `--config-env` takes the value from a variable at runtime.
|
|
432
|
+
if (value === "-c" && /^core\.worktree=/i.test(args[i + 1]?.value ?? "")) {
|
|
433
|
+
const raw = args[i + 1].value.slice(args[i + 1].value.indexOf("=") + 1);
|
|
434
|
+
if (!raw || hasExpansion(raw)) return envRefusal("core.worktree");
|
|
435
|
+
extraTargets.push(toAbsoluteBash(effective, raw, homedir()));
|
|
436
|
+
i += 2;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if ((value === "--config-env" && /^core\.worktree=/i.test(args[i + 1]?.value ?? "")) || /^--config-env=core\.worktree=/i.test(value)) {
|
|
440
|
+
return envRefusal("core.worktree");
|
|
441
|
+
}
|
|
172
442
|
const pathFlag = value === "-C" || value === "--git-dir" || value === "--work-tree";
|
|
173
443
|
const inlined = value.startsWith("--git-dir=") || value.startsWith("--work-tree=");
|
|
174
444
|
if (pathFlag || inlined) {
|
|
175
|
-
const
|
|
176
|
-
|
|
445
|
+
const pathToken = inlined ? args[i] : args[i + 1];
|
|
446
|
+
const raw = inlined ? value.slice(value.indexOf("=") + 1) : pathToken?.value;
|
|
447
|
+
if (!raw || hasExpansion(raw) || pathToken?.dynamic || pathToken?.glob || isUnknownTilde(raw)) {
|
|
177
448
|
return isolated(
|
|
178
449
|
worktreePath,
|
|
179
450
|
`\`git ${value}\` computes its repository target at runtime, so it cannot be verified`,
|
|
@@ -191,8 +462,8 @@ export function worktreeBashGuardReason({ command, worktreePath, sharedRoot }: W
|
|
|
191
462
|
|
|
192
463
|
const targets = [effective, ...extraTargets];
|
|
193
464
|
for (const target of targets) {
|
|
194
|
-
if (
|
|
195
|
-
if (
|
|
465
|
+
if (containsPath(worktreePath, target)) continue;
|
|
466
|
+
if (sharedRoots.some((root) => containsPath(root, target))) {
|
|
196
467
|
return isolated(
|
|
197
468
|
worktreePath,
|
|
198
469
|
`this git command targets ${target}, which is the shared checkout or another worktree of the same repository`,
|
|
@@ -205,7 +476,7 @@ export function worktreeBashGuardReason({ command, worktreePath, sharedRoot }: W
|
|
|
205
476
|
// The stash stack is per-repository and shared across its worktrees. Any
|
|
206
477
|
// shared-root target that is NOT inside the worktree already returned
|
|
207
478
|
// above, so only worktree-contained targets can reach this check.
|
|
208
|
-
if (targets.some((t) =>
|
|
479
|
+
if (targets.some((t) => containsPath(worktreePath, t))) {
|
|
209
480
|
const { sub, rest } = gitSubcommand(args);
|
|
210
481
|
if (sub === "stash") {
|
|
211
482
|
const reason = stashReason(rest);
|
|
@@ -216,26 +487,17 @@ export function worktreeBashGuardReason({ command, worktreePath, sharedRoot }: W
|
|
|
216
487
|
};
|
|
217
488
|
|
|
218
489
|
for (const seg of segments) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const reason = checkSegment(seg);
|
|
228
|
-
if (reason) return reason;
|
|
229
|
-
|
|
230
|
-
const last = seg.tokens[seg.tokens.length - 1]?.value ?? "";
|
|
231
|
-
const closes = (/\)+$/.exec(last)?.[0] ?? "").length;
|
|
232
|
-
if (closes > 0 && depth > 0) {
|
|
233
|
-
depth = Math.max(0, depth - closes);
|
|
234
|
-
if (depth === 0 && dirBeforeSubshell) {
|
|
235
|
-
dir = dirBeforeSubshell.dir;
|
|
236
|
-
dirBeforeSubshell = undefined;
|
|
237
|
-
}
|
|
490
|
+
const before = scopeDirs.get(seg);
|
|
491
|
+
const after: (string | undefined)[] = [];
|
|
492
|
+
for (const start of before) {
|
|
493
|
+
dir = start;
|
|
494
|
+
const reason = checkSegment(seg);
|
|
495
|
+
if (reason) return { reason };
|
|
496
|
+
after.push(dir);
|
|
238
497
|
}
|
|
498
|
+
const possible = [...new Set(seg.conditional ? [...before, ...after] : after)];
|
|
499
|
+
// Past a handful of possibilities the directory is as good as unknown.
|
|
500
|
+
scopeDirs.set(seg, possible.length > MAX_POSSIBLE_DIRS ? [undefined] : possible);
|
|
239
501
|
}
|
|
240
|
-
return
|
|
502
|
+
return { moves: segments.some(movesDirectory) || segments.some((seg) => resolvePayload(leadTokens(seg)).command === "eval") };
|
|
241
503
|
}
|