@dsh-cove/dsh-web-all 0.0.0-stage → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.i18n.yaml +6 -0
- package/README.md +201 -2
- package/README.zh.md +202 -0
- package/cordis.patch.yml +59 -0
- package/lib/client.js +25413 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +20024 -0
- package/lib/invariant.js +6 -0
- package/lib/types/client/compat.d.ts +29 -0
- package/lib/types/client/compat.d.ts.map +1 -0
- package/lib/types/client/compat.js +69 -0
- package/lib/types/client/conversation/git-branch.d.ts +13 -0
- package/lib/types/client/conversation/git-branch.d.ts.map +1 -0
- package/lib/types/client/conversation/git-branch.js +121 -0
- package/lib/types/client/conversation/git-graph/api.d.ts +42 -0
- package/lib/types/client/conversation/git-graph/api.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/api.js +72 -0
- package/lib/types/client/conversation/git-graph/branch-chip.d.ts +47 -0
- package/lib/types/client/conversation/git-graph/branch-chip.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/branch-chip.js +268 -0
- package/lib/types/client/conversation/git-graph/branch-popover.d.ts +32 -0
- package/lib/types/client/conversation/git-graph/branch-popover.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/branch-popover.js +108 -0
- package/lib/types/client/conversation/git-graph/chip.d.ts +24 -0
- package/lib/types/client/conversation/git-graph/chip.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/chip.js +15 -0
- package/lib/types/client/conversation/git-graph/create-branch-dialog.d.ts +30 -0
- package/lib/types/client/conversation/git-graph/create-branch-dialog.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/create-branch-dialog.js +52 -0
- package/lib/types/client/conversation/git-graph/error-copy.d.ts +17 -0
- package/lib/types/client/conversation/git-graph/error-copy.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/error-copy.js +44 -0
- package/lib/types/client/conversation/git-graph/graph-dialog.d.ts +21 -0
- package/lib/types/client/conversation/git-graph/graph-dialog.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/graph-dialog.js +112 -0
- package/lib/types/client/conversation/git-graph/hero-seat.d.ts +53 -0
- package/lib/types/client/conversation/git-graph/hero-seat.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/hero-seat.js +54 -0
- package/lib/types/client/conversation/git-graph/locales.d.ts +88 -0
- package/lib/types/client/conversation/git-graph/locales.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/locales.js +85 -0
- package/lib/types/client/conversation/git-graph/sse-leader.d.ts +37 -0
- package/lib/types/client/conversation/git-graph/sse-leader.d.ts.map +1 -0
- package/lib/types/client/conversation/git-graph/sse-leader.js +101 -0
- package/lib/types/client/conversation/index.d.ts +43 -0
- package/lib/types/client/conversation/index.d.ts.map +1 -0
- package/lib/types/client/conversation/index.js +39 -0
- package/lib/types/client/conversation/selector-context.d.ts +12 -0
- package/lib/types/client/conversation/selector-context.d.ts.map +1 -0
- package/lib/types/client/conversation/selector-context.js +11 -0
- package/lib/types/client/conversation/session-lane-chip.d.ts +72 -0
- package/lib/types/client/conversation/session-lane-chip.d.ts.map +1 -0
- package/lib/types/client/conversation/session-lane-chip.js +163 -0
- package/lib/types/client/conversation/session-lane-locales.d.ts +22 -0
- package/lib/types/client/conversation/session-lane-locales.d.ts.map +1 -0
- package/lib/types/client/conversation/session-lane-locales.js +29 -0
- package/lib/types/client/conversation/subagent-model-dom.d.ts +16 -0
- package/lib/types/client/conversation/subagent-model-dom.d.ts.map +1 -0
- package/lib/types/client/conversation/subagent-model-dom.js +138 -0
- package/lib/types/client/conversation/subagent-model.d.ts +17 -0
- package/lib/types/client/conversation/subagent-model.d.ts.map +1 -0
- package/lib/types/client/conversation/subagent-model.js +156 -0
- package/lib/types/client/conversation/worktree/api.d.ts +97 -0
- package/lib/types/client/conversation/worktree/api.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/api.js +239 -0
- package/lib/types/client/conversation/worktree/dialogs/DeleteWorktreeDialog.d.ts +35 -0
- package/lib/types/client/conversation/worktree/dialogs/DeleteWorktreeDialog.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/dialogs/DeleteWorktreeDialog.js +55 -0
- package/lib/types/client/conversation/worktree/dialogs/PrCreateDialog.d.ts +27 -0
- package/lib/types/client/conversation/worktree/dialogs/PrCreateDialog.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/dialogs/PrCreateDialog.js +49 -0
- package/lib/types/client/conversation/worktree/lane-binding.d.ts +46 -0
- package/lib/types/client/conversation/worktree/lane-binding.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/lane-binding.js +60 -0
- package/lib/types/client/conversation/worktree/lane-menu.d.ts +51 -0
- package/lib/types/client/conversation/worktree/lane-menu.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/lane-menu.js +140 -0
- package/lib/types/client/conversation/worktree/lane-open.d.ts +126 -0
- package/lib/types/client/conversation/worktree/lane-open.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/lane-open.js +140 -0
- package/lib/types/client/conversation/worktree/lane-workspace-sweeper.d.ts +79 -0
- package/lib/types/client/conversation/worktree/lane-workspace-sweeper.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/lane-workspace-sweeper.js +116 -0
- package/lib/types/client/conversation/worktree/lifecycle.d.ts +29 -0
- package/lib/types/client/conversation/worktree/lifecycle.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/lifecycle.js +67 -0
- package/lib/types/client/conversation/worktree/locales.d.ts +58 -0
- package/lib/types/client/conversation/worktree/locales.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/locales.js +86 -0
- package/lib/types/client/conversation/worktree/semantic.d.ts +11 -0
- package/lib/types/client/conversation/worktree/semantic.d.ts.map +1 -0
- package/lib/types/client/conversation/worktree/semantic.js +10 -0
- package/lib/types/client/index.d.ts +14 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +45 -0
- package/lib/types/client/lark-inbox/i18n.d.ts +63 -0
- package/lib/types/client/lark-inbox/i18n.d.ts.map +1 -0
- package/lib/types/client/lark-inbox/i18n.js +119 -0
- package/lib/types/client/lark-inbox/wizard.d.ts +19 -0
- package/lib/types/client/lark-inbox/wizard.d.ts.map +1 -0
- package/lib/types/client/lark-inbox/wizard.js +158 -0
- package/lib/types/client/notify/i18n.d.ts +47 -0
- package/lib/types/client/notify/i18n.d.ts.map +1 -0
- package/lib/types/client/notify/i18n.js +87 -0
- package/lib/types/client/settings/family-scope.d.ts +23 -0
- package/lib/types/client/settings/family-scope.d.ts.map +1 -0
- package/lib/types/client/settings/family-scope.js +30 -0
- package/lib/types/client/settings/lark-inbox.d.ts +73 -0
- package/lib/types/client/settings/lark-inbox.d.ts.map +1 -0
- package/lib/types/client/settings/lark-inbox.js +144 -0
- package/lib/types/client/settings/notify.d.ts +66 -0
- package/lib/types/client/settings/notify.d.ts.map +1 -0
- package/lib/types/client/settings/notify.js +139 -0
- package/lib/types/client/settings/primitives-0.2.d.ts +137 -0
- package/lib/types/client/settings/primitives-0.2.d.ts.map +1 -0
- package/lib/types/client/settings/primitives-0.2.js +8 -0
- package/lib/types/client/sidebar/RenderBoundary.d.ts +17 -0
- package/lib/types/client/sidebar/RenderBoundary.d.ts.map +1 -0
- package/lib/types/client/sidebar/RenderBoundary.js +18 -0
- package/lib/types/client/sidebar/agent-browser.d.ts +9 -0
- package/lib/types/client/sidebar/agent-browser.d.ts.map +1 -0
- package/lib/types/client/sidebar/agent-browser.js +102 -0
- package/lib/types/client/sidebar/diff-view.d.ts +58 -0
- package/lib/types/client/sidebar/diff-view.d.ts.map +1 -0
- package/lib/types/client/sidebar/diff-view.js +223 -0
- package/lib/types/client/sidebar/git-workbench.d.ts +41 -0
- package/lib/types/client/sidebar/git-workbench.d.ts.map +1 -0
- package/lib/types/client/sidebar/git-workbench.js +480 -0
- package/lib/types/client/sidebar/icons.d.ts +9 -0
- package/lib/types/client/sidebar/icons.d.ts.map +1 -0
- package/lib/types/client/sidebar/icons.js +6 -0
- package/lib/types/client/sidebar/index.d.ts +19 -0
- package/lib/types/client/sidebar/index.d.ts.map +1 -0
- package/lib/types/client/sidebar/index.js +12 -0
- package/lib/types/client/sidebar/native-panels.d.ts +19 -0
- package/lib/types/client/sidebar/native-panels.d.ts.map +1 -0
- package/lib/types/client/sidebar/native-panels.js +103 -0
- package/lib/types/client/sidebar/page-shell.d.ts +52 -0
- package/lib/types/client/sidebar/page-shell.d.ts.map +1 -0
- package/lib/types/client/sidebar/page-shell.js +18 -0
- package/lib/types/client/sidebar/panels/memoir/api.d.ts +258 -0
- package/lib/types/client/sidebar/panels/memoir/api.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/memoir/api.js +135 -0
- package/lib/types/client/sidebar/panels/memoir/controller.d.ts +22 -0
- package/lib/types/client/sidebar/panels/memoir/controller.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/memoir/controller.js +38 -0
- package/lib/types/client/sidebar/panels/memoir/cwd.d.ts +80 -0
- package/lib/types/client/sidebar/panels/memoir/cwd.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/memoir/cwd.js +96 -0
- package/lib/types/client/sidebar/panels/memoir/i18n.d.ts +21 -0
- package/lib/types/client/sidebar/panels/memoir/i18n.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/memoir/i18n.js +288 -0
- package/lib/types/client/sidebar/panels/memoir/panel-activation.d.ts +11 -0
- package/lib/types/client/sidebar/panels/memoir/panel-activation.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/memoir/panel-activation.js +37 -0
- package/lib/types/client/sidebar/panels/memoir/panel.d.ts +26 -0
- package/lib/types/client/sidebar/panels/memoir/panel.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/memoir/panel.js +423 -0
- package/lib/types/client/sidebar/panels/memoir/styles.d.ts +22 -0
- package/lib/types/client/sidebar/panels/memoir/styles.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/memoir/styles.js +774 -0
- package/lib/types/client/sidebar/panels/memoir/types.d.ts +7 -0
- package/lib/types/client/sidebar/panels/memoir/types.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/memoir/types.js +5 -0
- package/lib/types/client/sidebar/panels/ssh/ClusterTab.d.ts +8 -0
- package/lib/types/client/sidebar/panels/ssh/ClusterTab.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/ClusterTab.js +53 -0
- package/lib/types/client/sidebar/panels/ssh/HostFormDialog.d.ts +13 -0
- package/lib/types/client/sidebar/panels/ssh/HostFormDialog.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/HostFormDialog.js +110 -0
- package/lib/types/client/sidebar/panels/ssh/HostsTab.d.ts +26 -0
- package/lib/types/client/sidebar/panels/ssh/HostsTab.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/HostsTab.js +175 -0
- package/lib/types/client/sidebar/panels/ssh/SshPanel.d.ts +14 -0
- package/lib/types/client/sidebar/panels/ssh/SshPanel.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/SshPanel.js +34 -0
- package/lib/types/client/sidebar/panels/ssh/TerminalTab.d.ts +18 -0
- package/lib/types/client/sidebar/panels/ssh/TerminalTab.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/TerminalTab.js +207 -0
- package/lib/types/client/sidebar/panels/ssh/TransferTab.d.ts +8 -0
- package/lib/types/client/sidebar/panels/ssh/TransferTab.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/TransferTab.js +168 -0
- package/lib/types/client/sidebar/panels/ssh/TunnelsTab.d.ts +18 -0
- package/lib/types/client/sidebar/panels/ssh/TunnelsTab.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/TunnelsTab.js +161 -0
- package/lib/types/client/sidebar/panels/ssh/api.d.ts +80 -0
- package/lib/types/client/sidebar/panels/ssh/api.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/api.js +333 -0
- package/lib/types/client/sidebar/panels/ssh/controller.d.ts +23 -0
- package/lib/types/client/sidebar/panels/ssh/controller.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/controller.js +41 -0
- package/lib/types/client/sidebar/panels/ssh/helpers.d.ts +44 -0
- package/lib/types/client/sidebar/panels/ssh/helpers.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/helpers.js +53 -0
- package/lib/types/client/sidebar/panels/ssh/locales.d.ts +155 -0
- package/lib/types/client/sidebar/panels/ssh/locales.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/locales.js +312 -0
- package/lib/types/client/sidebar/panels/ssh/xterm.css.d.ts +3 -0
- package/lib/types/client/sidebar/panels/ssh/xterm.css.d.ts.map +1 -0
- package/lib/types/client/sidebar/panels/ssh/xterm.css.js +2 -0
- package/lib/types/client/sidebar/right-pane-i18n.d.ts +121 -0
- package/lib/types/client/sidebar/right-pane-i18n.d.ts.map +1 -0
- package/lib/types/client/sidebar/right-pane-i18n.js +239 -0
- package/lib/types/client/sidebar/right-pane.d.ts +27 -0
- package/lib/types/client/sidebar/right-pane.d.ts.map +1 -0
- package/lib/types/client/sidebar/right-pane.js +112 -0
- package/lib/types/client/sidebar/scope.d.ts +17 -0
- package/lib/types/client/sidebar/scope.d.ts.map +1 -0
- package/lib/types/client/sidebar/scope.js +41 -0
- package/lib/types/client/trust.d.ts +78 -0
- package/lib/types/client/trust.d.ts.map +1 -0
- package/lib/types/client/trust.js +130 -0
- package/lib/types/client/ui-icons.d.ts +19 -0
- package/lib/types/client/ui-icons.d.ts.map +1 -0
- package/lib/types/client/ui-icons.js +33 -0
- package/lib/types/core/git-graph/commands.d.ts +36 -0
- package/lib/types/core/git-graph/commands.d.ts.map +1 -0
- package/lib/types/core/git-graph/commands.js +122 -0
- package/lib/types/core/git-graph/protocol.d.ts +104 -0
- package/lib/types/core/git-graph/protocol.d.ts.map +1 -0
- package/lib/types/core/git-graph/protocol.js +142 -0
- package/lib/types/core/index.d.ts +3 -0
- package/lib/types/core/index.d.ts.map +1 -0
- package/lib/types/core/index.js +2 -0
- package/lib/types/core/lark-inbox/gating.d.ts +21 -0
- package/lib/types/core/lark-inbox/gating.d.ts.map +1 -0
- package/lib/types/core/lark-inbox/gating.js +17 -0
- package/lib/types/core/lark-inbox/queue.d.ts +27 -0
- package/lib/types/core/lark-inbox/queue.d.ts.map +1 -0
- package/lib/types/core/lark-inbox/queue.js +78 -0
- package/lib/types/core/memoir/protocol.d.ts +42 -0
- package/lib/types/core/memoir/protocol.d.ts.map +1 -0
- package/lib/types/core/memoir/protocol.js +8 -0
- package/lib/types/core/no-browser-auth/contract.d.ts +22 -0
- package/lib/types/core/no-browser-auth/contract.d.ts.map +1 -0
- package/lib/types/core/no-browser-auth/contract.js +9 -0
- package/lib/types/core/notify/contract.d.ts +133 -0
- package/lib/types/core/notify/contract.d.ts.map +1 -0
- package/lib/types/core/notify/contract.js +186 -0
- package/lib/types/core/notify/feed.d.ts +116 -0
- package/lib/types/core/notify/feed.d.ts.map +1 -0
- package/lib/types/core/notify/feed.js +214 -0
- package/lib/types/core/playwright-sessions/contracts.d.ts +98 -0
- package/lib/types/core/playwright-sessions/contracts.d.ts.map +1 -0
- package/lib/types/core/playwright-sessions/contracts.js +68 -0
- package/lib/types/core/playwright-sessions/control.d.ts +19 -0
- package/lib/types/core/playwright-sessions/control.d.ts.map +1 -0
- package/lib/types/core/playwright-sessions/control.js +9 -0
- package/lib/types/core/playwright-sessions/index.d.ts +4 -0
- package/lib/types/core/playwright-sessions/index.d.ts.map +1 -0
- package/lib/types/core/playwright-sessions/index.js +3 -0
- package/lib/types/core/playwright-sessions/names.d.ts +4 -0
- package/lib/types/core/playwright-sessions/names.d.ts.map +1 -0
- package/lib/types/core/playwright-sessions/names.js +84 -0
- package/lib/types/core/playwright-sessions/schema.d.ts +4 -0
- package/lib/types/core/playwright-sessions/schema.d.ts.map +1 -0
- package/lib/types/core/playwright-sessions/schema.js +90 -0
- package/lib/types/core/playwright-sessions/wire.d.ts +78 -0
- package/lib/types/core/playwright-sessions/wire.d.ts.map +1 -0
- package/lib/types/core/playwright-sessions/wire.js +103 -0
- package/lib/types/core/remote-access/paths.d.ts +19 -0
- package/lib/types/core/remote-access/paths.d.ts.map +1 -0
- package/lib/types/core/remote-access/paths.js +18 -0
- package/lib/types/core/remote-web-ui/security.d.ts +35 -0
- package/lib/types/core/remote-web-ui/security.d.ts.map +1 -0
- package/lib/types/core/remote-web-ui/security.js +146 -0
- package/lib/types/core/right-pane/protocol.d.ts +25 -0
- package/lib/types/core/right-pane/protocol.d.ts.map +1 -0
- package/lib/types/core/right-pane/protocol.js +91 -0
- package/lib/types/core/schedule-mode/index.d.ts +7 -0
- package/lib/types/core/schedule-mode/index.d.ts.map +1 -0
- package/lib/types/core/schedule-mode/index.js +6 -0
- package/lib/types/core/schedule-mode/native.d.ts +35 -0
- package/lib/types/core/schedule-mode/native.d.ts.map +1 -0
- package/lib/types/core/schedule-mode/native.js +82 -0
- package/lib/types/core/schedule-mode/types.d.ts +177 -0
- package/lib/types/core/schedule-mode/types.d.ts.map +1 -0
- package/lib/types/core/schedule-mode/types.js +65 -0
- package/lib/types/core/schedule-mode/view.d.ts +62 -0
- package/lib/types/core/schedule-mode/view.d.ts.map +1 -0
- package/lib/types/core/schedule-mode/view.js +4 -0
- package/lib/types/core/ssh/protocol.d.ts +215 -0
- package/lib/types/core/ssh/protocol.d.ts.map +1 -0
- package/lib/types/core/ssh/protocol.js +23 -0
- package/lib/types/core/subagent-model/badges.d.ts +73 -0
- package/lib/types/core/subagent-model/badges.d.ts.map +1 -0
- package/lib/types/core/subagent-model/badges.js +105 -0
- package/lib/types/core/subagent-model/model-resolver.d.ts +25 -0
- package/lib/types/core/subagent-model/model-resolver.d.ts.map +1 -0
- package/lib/types/core/subagent-model/model-resolver.js +60 -0
- package/lib/types/core/worktree/invariant.d.ts +62 -0
- package/lib/types/core/worktree/invariant.d.ts.map +1 -0
- package/lib/types/core/worktree/invariant.js +72 -0
- package/lib/types/core/worktree/manifest.d.ts +71 -0
- package/lib/types/core/worktree/manifest.d.ts.map +1 -0
- package/lib/types/core/worktree/manifest.js +98 -0
- package/lib/types/core/worktree/paths.d.ts +60 -0
- package/lib/types/core/worktree/paths.d.ts.map +1 -0
- package/lib/types/core/worktree/paths.js +118 -0
- package/lib/types/core/worktree/pr-create.d.ts +67 -0
- package/lib/types/core/worktree/pr-create.d.ts.map +1 -0
- package/lib/types/core/worktree/pr-create.js +118 -0
- package/lib/types/core/worktree/protocol.d.ts +7 -0
- package/lib/types/core/worktree/protocol.d.ts.map +1 -0
- package/lib/types/core/worktree/protocol.js +6 -0
- package/lib/types/core/worktree/types.d.ts +292 -0
- package/lib/types/core/worktree/types.d.ts.map +1 -0
- package/lib/types/core/worktree/types.js +14 -0
- package/lib/types/core/worktree/workspace-model.d.ts +56 -0
- package/lib/types/core/worktree/workspace-model.d.ts.map +1 -0
- package/lib/types/core/worktree/workspace-model.js +14 -0
- package/lib/types/host/services/access-trust/index.d.ts +54 -0
- package/lib/types/host/services/access-trust/index.d.ts.map +1 -0
- package/lib/types/host/services/access-trust/index.js +82 -0
- package/lib/types/host/services/access-trust/trust.d.ts +70 -0
- package/lib/types/host/services/access-trust/trust.d.ts.map +1 -0
- package/lib/types/host/services/access-trust/trust.js +187 -0
- package/lib/types/host/services/codex-bridge/bridge.d.ts +106 -0
- package/lib/types/host/services/codex-bridge/bridge.d.ts.map +1 -0
- package/lib/types/host/services/codex-bridge/bridge.js +321 -0
- package/lib/types/host/services/codex-bridge/codex-auth.d.ts +108 -0
- package/lib/types/host/services/codex-bridge/codex-auth.d.ts.map +1 -0
- package/lib/types/host/services/codex-bridge/codex-auth.js +241 -0
- package/lib/types/host/services/codex-bridge/dsh-integration.d.ts +58 -0
- package/lib/types/host/services/codex-bridge/dsh-integration.d.ts.map +1 -0
- package/lib/types/host/services/codex-bridge/dsh-integration.js +120 -0
- package/lib/types/host/services/codex-bridge/http.d.ts +43 -0
- package/lib/types/host/services/codex-bridge/http.d.ts.map +1 -0
- package/lib/types/host/services/codex-bridge/http.js +92 -0
- package/lib/types/host/services/codex-bridge/index.d.ts +63 -0
- package/lib/types/host/services/codex-bridge/index.d.ts.map +1 -0
- package/lib/types/host/services/codex-bridge/index.js +111 -0
- package/lib/types/host/services/codex-bridge/loopback.d.ts +25 -0
- package/lib/types/host/services/codex-bridge/loopback.d.ts.map +1 -0
- package/lib/types/host/services/codex-bridge/loopback.js +68 -0
- package/lib/types/host/services/codex-bridge/routes.d.ts +31 -0
- package/lib/types/host/services/codex-bridge/routes.d.ts.map +1 -0
- package/lib/types/host/services/codex-bridge/routes.js +126 -0
- package/lib/types/host/services/family-config.d.ts +71 -0
- package/lib/types/host/services/family-config.d.ts.map +1 -0
- package/lib/types/host/services/family-config.js +166 -0
- package/lib/types/host/services/family-namespaces.d.ts +24 -0
- package/lib/types/host/services/family-namespaces.d.ts.map +1 -0
- package/lib/types/host/services/family-namespaces.js +30 -0
- package/lib/types/host/services/git-graph/index.d.ts +6 -0
- package/lib/types/host/services/git-graph/index.d.ts.map +1 -0
- package/lib/types/host/services/git-graph/index.js +7 -0
- package/lib/types/host/services/git-graph/routes.d.ts +4 -0
- package/lib/types/host/services/git-graph/routes.d.ts.map +1 -0
- package/lib/types/host/services/git-graph/routes.js +241 -0
- package/lib/types/host/services/git-graph/service.d.ts +52 -0
- package/lib/types/host/services/git-graph/service.d.ts.map +1 -0
- package/lib/types/host/services/git-graph/service.js +301 -0
- package/lib/types/host/services/index.d.ts +23 -0
- package/lib/types/host/services/index.d.ts.map +1 -0
- package/lib/types/host/services/index.js +80 -0
- package/lib/types/host/services/lark-inbox/config.d.ts +24 -0
- package/lib/types/host/services/lark-inbox/config.d.ts.map +1 -0
- package/lib/types/host/services/lark-inbox/config.js +48 -0
- package/lib/types/host/services/lark-inbox/gateway.d.ts +53 -0
- package/lib/types/host/services/lark-inbox/gateway.d.ts.map +1 -0
- package/lib/types/host/services/lark-inbox/gateway.js +171 -0
- package/lib/types/host/services/lark-inbox/host-routes.d.ts +22 -0
- package/lib/types/host/services/lark-inbox/host-routes.d.ts.map +1 -0
- package/lib/types/host/services/lark-inbox/host-routes.js +146 -0
- package/lib/types/host/services/lark-inbox/inbox-store.d.ts +39 -0
- package/lib/types/host/services/lark-inbox/inbox-store.d.ts.map +1 -0
- package/lib/types/host/services/lark-inbox/inbox-store.js +124 -0
- package/lib/types/host/services/lark-inbox/index.d.ts +26 -0
- package/lib/types/host/services/lark-inbox/index.d.ts.map +1 -0
- package/lib/types/host/services/lark-inbox/index.js +135 -0
- package/lib/types/host/services/lark-inbox/tools.d.ts +7 -0
- package/lib/types/host/services/lark-inbox/tools.d.ts.map +1 -0
- package/lib/types/host/services/lark-inbox/tools.js +119 -0
- package/lib/types/host/services/lark-inbox/wizard.d.ts +62 -0
- package/lib/types/host/services/lark-inbox/wizard.d.ts.map +1 -0
- package/lib/types/host/services/lark-inbox/wizard.js +211 -0
- package/lib/types/host/services/lifecycle.d.ts +29 -0
- package/lib/types/host/services/lifecycle.d.ts.map +1 -0
- package/lib/types/host/services/lifecycle.js +93 -0
- package/lib/types/host/services/memoir/index.d.ts +40 -0
- package/lib/types/host/services/memoir/index.d.ts.map +1 -0
- package/lib/types/host/services/memoir/index.js +52 -0
- package/lib/types/host/services/memoir/routes.d.ts +30 -0
- package/lib/types/host/services/memoir/routes.d.ts.map +1 -0
- package/lib/types/host/services/memoir/routes.js +364 -0
- package/lib/types/host/services/memoir/settings.d.ts +27 -0
- package/lib/types/host/services/memoir/settings.d.ts.map +1 -0
- package/lib/types/host/services/memoir/settings.js +120 -0
- package/lib/types/host/services/memoir/store.d.ts +35 -0
- package/lib/types/host/services/memoir/store.d.ts.map +1 -0
- package/lib/types/host/services/memoir/store.js +124 -0
- package/lib/types/host/services/memoir/tools.d.ts +8 -0
- package/lib/types/host/services/memoir/tools.d.ts.map +1 -0
- package/lib/types/host/services/memoir/tools.js +154 -0
- package/lib/types/host/services/no-browser-auth/index.d.ts +25 -0
- package/lib/types/host/services/no-browser-auth/index.d.ts.map +1 -0
- package/lib/types/host/services/no-browser-auth/index.js +42 -0
- package/lib/types/host/services/notify/config.d.ts +105 -0
- package/lib/types/host/services/notify/config.d.ts.map +1 -0
- package/lib/types/host/services/notify/config.js +123 -0
- package/lib/types/host/services/notify/feishu-sender.d.ts +120 -0
- package/lib/types/host/services/notify/feishu-sender.d.ts.map +1 -0
- package/lib/types/host/services/notify/feishu-sender.js +186 -0
- package/lib/types/host/services/notify/host-routes.d.ts +69 -0
- package/lib/types/host/services/notify/host-routes.d.ts.map +1 -0
- package/lib/types/host/services/notify/host-routes.js +112 -0
- package/lib/types/host/services/notify/index.d.ts +80 -0
- package/lib/types/host/services/notify/index.d.ts.map +1 -0
- package/lib/types/host/services/notify/index.js +263 -0
- package/lib/types/host/services/playwright-sessions/child.d.ts +76 -0
- package/lib/types/host/services/playwright-sessions/child.d.ts.map +1 -0
- package/lib/types/host/services/playwright-sessions/child.js +197 -0
- package/lib/types/host/services/playwright-sessions/controller.d.ts +25 -0
- package/lib/types/host/services/playwright-sessions/controller.d.ts.map +1 -0
- package/lib/types/host/services/playwright-sessions/controller.js +104 -0
- package/lib/types/host/services/playwright-sessions/index.d.ts +6 -0
- package/lib/types/host/services/playwright-sessions/index.d.ts.map +1 -0
- package/lib/types/host/services/playwright-sessions/index.js +161 -0
- package/lib/types/host/services/playwright-sessions/live-view.d.ts +32 -0
- package/lib/types/host/services/playwright-sessions/live-view.d.ts.map +1 -0
- package/lib/types/host/services/playwright-sessions/live-view.js +188 -0
- package/lib/types/host/services/playwright-sessions/manager.d.ts +87 -0
- package/lib/types/host/services/playwright-sessions/manager.d.ts.map +1 -0
- package/lib/types/host/services/playwright-sessions/manager.js +203 -0
- package/lib/types/host/services/playwright-sessions/profile-gc.d.ts +39 -0
- package/lib/types/host/services/playwright-sessions/profile-gc.d.ts.map +1 -0
- package/lib/types/host/services/playwright-sessions/profile-gc.js +72 -0
- package/lib/types/host/services/playwright-sessions/routes.d.ts +18 -0
- package/lib/types/host/services/playwright-sessions/routes.d.ts.map +1 -0
- package/lib/types/host/services/playwright-sessions/routes.js +87 -0
- package/lib/types/host/services/remote-access/boot-scripts.d.ts +55 -0
- package/lib/types/host/services/remote-access/boot-scripts.d.ts.map +1 -0
- package/lib/types/host/services/remote-access/boot-scripts.js +109 -0
- package/lib/types/host/services/remote-access/index.d.ts +28 -0
- package/lib/types/host/services/remote-access/index.d.ts.map +1 -0
- package/lib/types/host/services/remote-access/index.js +47 -0
- package/lib/types/host/services/remote-access/routes.d.ts +42 -0
- package/lib/types/host/services/remote-access/routes.d.ts.map +1 -0
- package/lib/types/host/services/remote-access/routes.js +119 -0
- package/lib/types/host/services/remote-web-ui/index.d.ts +21 -0
- package/lib/types/host/services/remote-web-ui/index.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/index.js +325 -0
- package/lib/types/host/services/remote-web-ui/server-mode/accounts.d.ts +115 -0
- package/lib/types/host/services/remote-web-ui/server-mode/accounts.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/accounts.js +396 -0
- package/lib/types/host/services/remote-web-ui/server-mode/cookie.d.ts +28 -0
- package/lib/types/host/services/remote-web-ui/server-mode/cookie.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/cookie.js +48 -0
- package/lib/types/host/services/remote-web-ui/server-mode/dsh-home.d.ts +17 -0
- package/lib/types/host/services/remote-web-ui/server-mode/dsh-home.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/dsh-home.js +40 -0
- package/lib/types/host/services/remote-web-ui/server-mode/external-auth.d.ts +73 -0
- package/lib/types/host/services/remote-web-ui/server-mode/external-auth.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/external-auth.js +242 -0
- package/lib/types/host/services/remote-web-ui/server-mode/fence.d.ts +23 -0
- package/lib/types/host/services/remote-web-ui/server-mode/fence.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/fence.js +24 -0
- package/lib/types/host/services/remote-web-ui/server-mode/gateway-routes.d.ts +89 -0
- package/lib/types/host/services/remote-web-ui/server-mode/gateway-routes.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/gateway-routes.js +768 -0
- package/lib/types/host/services/remote-web-ui/server-mode/gateway.d.ts +99 -0
- package/lib/types/host/services/remote-web-ui/server-mode/gateway.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/gateway.js +679 -0
- package/lib/types/host/services/remote-web-ui/server-mode/http.d.ts +43 -0
- package/lib/types/host/services/remote-web-ui/server-mode/http.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/http.js +92 -0
- package/lib/types/host/services/remote-web-ui/server-mode/integrated-auth.d.ts +42 -0
- package/lib/types/host/services/remote-web-ui/server-mode/integrated-auth.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/integrated-auth.js +73 -0
- package/lib/types/host/services/remote-web-ui/server-mode/lan.d.ts +13 -0
- package/lib/types/host/services/remote-web-ui/server-mode/lan.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/lan.js +17 -0
- package/lib/types/host/services/remote-web-ui/server-mode/loopback.d.ts +25 -0
- package/lib/types/host/services/remote-web-ui/server-mode/loopback.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/loopback.js +68 -0
- package/lib/types/host/services/remote-web-ui/server-mode/posture.d.ts +75 -0
- package/lib/types/host/services/remote-web-ui/server-mode/posture.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/posture.js +116 -0
- package/lib/types/host/services/remote-web-ui/server-mode/roles.d.ts +44 -0
- package/lib/types/host/services/remote-web-ui/server-mode/roles.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/roles.js +47 -0
- package/lib/types/host/services/remote-web-ui/server-mode/server-boot.d.ts +21 -0
- package/lib/types/host/services/remote-web-ui/server-mode/server-boot.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/server-boot.js +37 -0
- package/lib/types/host/services/remote-web-ui/server-mode/sessions.d.ts +111 -0
- package/lib/types/host/services/remote-web-ui/server-mode/sessions.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server-mode/sessions.js +298 -0
- package/lib/types/host/services/remote-web-ui/server.d.ts +82 -0
- package/lib/types/host/services/remote-web-ui/server.d.ts.map +1 -0
- package/lib/types/host/services/remote-web-ui/server.js +492 -0
- package/lib/types/host/services/resources.d.ts +9 -0
- package/lib/types/host/services/resources.d.ts.map +1 -0
- package/lib/types/host/services/resources.js +98 -0
- package/lib/types/host/services/right-pane/browser.d.ts +42 -0
- package/lib/types/host/services/right-pane/browser.d.ts.map +1 -0
- package/lib/types/host/services/right-pane/browser.js +106 -0
- package/lib/types/host/services/right-pane/index.d.ts +22 -0
- package/lib/types/host/services/right-pane/index.d.ts.map +1 -0
- package/lib/types/host/services/right-pane/index.js +114 -0
- package/lib/types/host/services/right-pane/wire.d.ts +6 -0
- package/lib/types/host/services/right-pane/wire.d.ts.map +1 -0
- package/lib/types/host/services/right-pane/wire.js +9 -0
- package/lib/types/host/services/schedule-mode/config.d.ts +11 -0
- package/lib/types/host/services/schedule-mode/config.d.ts.map +1 -0
- package/lib/types/host/services/schedule-mode/config.js +15 -0
- package/lib/types/host/services/schedule-mode/engine.d.ts +150 -0
- package/lib/types/host/services/schedule-mode/engine.d.ts.map +1 -0
- package/lib/types/host/services/schedule-mode/engine.js +672 -0
- package/lib/types/host/services/schedule-mode/index.d.ts +33 -0
- package/lib/types/host/services/schedule-mode/index.d.ts.map +1 -0
- package/lib/types/host/services/schedule-mode/index.js +175 -0
- package/lib/types/host/services/schedule-mode/routes.d.ts +31 -0
- package/lib/types/host/services/schedule-mode/routes.d.ts.map +1 -0
- package/lib/types/host/services/schedule-mode/routes.js +109 -0
- package/lib/types/host/services/schedule-mode/session-create.d.ts +27 -0
- package/lib/types/host/services/schedule-mode/session-create.d.ts.map +1 -0
- package/lib/types/host/services/schedule-mode/session-create.js +21 -0
- package/lib/types/host/services/schedule-mode/store.d.ts +36 -0
- package/lib/types/host/services/schedule-mode/store.d.ts.map +1 -0
- package/lib/types/host/services/schedule-mode/store.js +309 -0
- package/lib/types/host/services/schedule-mode/tools.d.ts +13 -0
- package/lib/types/host/services/schedule-mode/tools.d.ts.map +1 -0
- package/lib/types/host/services/schedule-mode/tools.js +132 -0
- package/lib/types/host/services/ssh/engine.d.ts +162 -0
- package/lib/types/host/services/ssh/engine.d.ts.map +1 -0
- package/lib/types/host/services/ssh/engine.js +699 -0
- package/lib/types/host/services/ssh/host-keys.d.ts +10 -0
- package/lib/types/host/services/ssh/host-keys.d.ts.map +1 -0
- package/lib/types/host/services/ssh/host-keys.js +108 -0
- package/lib/types/host/services/ssh/index.d.ts +38 -0
- package/lib/types/host/services/ssh/index.d.ts.map +1 -0
- package/lib/types/host/services/ssh/index.js +47 -0
- package/lib/types/host/services/ssh/routes.d.ts +29 -0
- package/lib/types/host/services/ssh/routes.d.ts.map +1 -0
- package/lib/types/host/services/ssh/routes.js +465 -0
- package/lib/types/host/services/ssh/store.d.ts +40 -0
- package/lib/types/host/services/ssh/store.d.ts.map +1 -0
- package/lib/types/host/services/ssh/store.js +330 -0
- package/lib/types/host/services/ssh/terminal-ws.d.ts +9 -0
- package/lib/types/host/services/ssh/terminal-ws.d.ts.map +1 -0
- package/lib/types/host/services/ssh/terminal-ws.js +142 -0
- package/lib/types/host/services/ssh/tools.d.ts +11 -0
- package/lib/types/host/services/ssh/tools.d.ts.map +1 -0
- package/lib/types/host/services/ssh/tools.js +270 -0
- package/lib/types/host/services/web-render/contracts.d.ts +93 -0
- package/lib/types/host/services/web-render/contracts.d.ts.map +1 -0
- package/lib/types/host/services/web-render/contracts.js +60 -0
- package/lib/types/host/services/web-render/index.d.ts +58 -0
- package/lib/types/host/services/web-render/index.d.ts.map +1 -0
- package/lib/types/host/services/web-render/index.js +90 -0
- package/lib/types/host/services/web-render/provider.d.ts +37 -0
- package/lib/types/host/services/web-render/provider.d.ts.map +1 -0
- package/lib/types/host/services/web-render/provider.js +274 -0
- package/lib/types/host/services/web-search-anthropic/contracts.d.ts +117 -0
- package/lib/types/host/services/web-search-anthropic/contracts.d.ts.map +1 -0
- package/lib/types/host/services/web-search-anthropic/contracts.js +90 -0
- package/lib/types/host/services/web-search-anthropic/index.d.ts +53 -0
- package/lib/types/host/services/web-search-anthropic/index.d.ts.map +1 -0
- package/lib/types/host/services/web-search-anthropic/index.js +123 -0
- package/lib/types/host/services/web-search-anthropic/provider.d.ts +101 -0
- package/lib/types/host/services/web-search-anthropic/provider.d.ts.map +1 -0
- package/lib/types/host/services/web-search-anthropic/provider.js +237 -0
- package/lib/types/host/services/web-search-kaon/contracts.d.ts +114 -0
- package/lib/types/host/services/web-search-kaon/contracts.d.ts.map +1 -0
- package/lib/types/host/services/web-search-kaon/contracts.js +73 -0
- package/lib/types/host/services/web-search-kaon/index.d.ts +50 -0
- package/lib/types/host/services/web-search-kaon/index.d.ts.map +1 -0
- package/lib/types/host/services/web-search-kaon/index.js +114 -0
- package/lib/types/host/services/web-search-kaon/provider.d.ts +111 -0
- package/lib/types/host/services/web-search-kaon/provider.d.ts.map +1 -0
- package/lib/types/host/services/web-search-kaon/provider.js +244 -0
- package/lib/types/host/services/worktree/git-runner.d.ts +82 -0
- package/lib/types/host/services/worktree/git-runner.d.ts.map +1 -0
- package/lib/types/host/services/worktree/git-runner.js +81 -0
- package/lib/types/host/services/worktree/git.d.ts +94 -0
- package/lib/types/host/services/worktree/git.d.ts.map +1 -0
- package/lib/types/host/services/worktree/git.js +153 -0
- package/lib/types/host/services/worktree/http.d.ts +12 -0
- package/lib/types/host/services/worktree/http.d.ts.map +1 -0
- package/lib/types/host/services/worktree/http.js +34 -0
- package/lib/types/host/services/worktree/index.d.ts +38 -0
- package/lib/types/host/services/worktree/index.d.ts.map +1 -0
- package/lib/types/host/services/worktree/index.js +78 -0
- package/lib/types/host/services/worktree/lane-tool.d.ts +22 -0
- package/lib/types/host/services/worktree/lane-tool.d.ts.map +1 -0
- package/lib/types/host/services/worktree/lane-tool.js +92 -0
- package/lib/types/host/services/worktree/manager.d.ts +410 -0
- package/lib/types/host/services/worktree/manager.d.ts.map +1 -0
- package/lib/types/host/services/worktree/manager.js +1485 -0
- package/lib/types/host/services/worktree/model-override.d.ts +20 -0
- package/lib/types/host/services/worktree/model-override.d.ts.map +1 -0
- package/lib/types/host/services/worktree/model-override.js +35 -0
- package/lib/types/host/services/worktree/routes.d.ts +50 -0
- package/lib/types/host/services/worktree/routes.d.ts.map +1 -0
- package/lib/types/host/services/worktree/routes.js +695 -0
- package/lib/types/host/services/worktree/run-command.d.ts +26 -0
- package/lib/types/host/services/worktree/run-command.d.ts.map +1 -0
- package/lib/types/host/services/worktree/run-command.js +36 -0
- package/lib/types/host/services/worktree/session-lane/manager.d.ts +109 -0
- package/lib/types/host/services/worktree/session-lane/manager.d.ts.map +1 -0
- package/lib/types/host/services/worktree/session-lane/manager.js +172 -0
- package/lib/types/host/services/worktree/workspace-model-routes.d.ts +21 -0
- package/lib/types/host/services/worktree/workspace-model-routes.d.ts.map +1 -0
- package/lib/types/host/services/worktree/workspace-model-routes.js +198 -0
- package/lib/types/host/services/worktree/workspace-model.d.ts +48 -0
- package/lib/types/host/services/worktree/workspace-model.d.ts.map +1 -0
- package/lib/types/host/services/worktree/workspace-model.js +113 -0
- package/lib/types/index.d.ts +47 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +52 -0
- package/lib/types/invariant.d.ts +4 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +3 -0
- package/package.json +153 -3
- package/presets/code-vibe/THIRD_PARTY_NOTICES.md +55 -0
- package/presets/code-vibe/skills/UPSTREAM.md +104 -0
- package/presets/code-vibe/skills/agent-experience/SKILL.md +15 -0
- package/presets/code-vibe/skills/agent-operation/SKILL.md +43 -0
- package/presets/code-vibe/skills/code-review/SKILL.md +45 -0
- package/presets/code-vibe/skills/dsh-ci-test-reliability/SKILL.md +133 -0
- package/presets/code-vibe/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md +60 -0
- package/presets/code-vibe/skills/dsh-find-simplifications/SKILL.md +69 -0
- package/presets/code-vibe/skills/dsh-find-simplifications/references/historical-patterns.md +45 -0
- package/presets/code-vibe/skills/dsh-merging-stacked-prs/SKILL.md +129 -0
- package/presets/code-vibe/skills/dsh-prose-standard/SKILL.md +83 -0
- package/presets/code-vibe/skills/dsh-prose-standard/references/examples.md +169 -0
- package/presets/code-vibe/skills/dsh-speed-up-perf/SKILL.md +94 -0
- package/presets/code-vibe/skills/dsh-trim-cot-leakage/SKILL.md +47 -0
- package/presets/code-vibe/skills/dsh-trim-cot-leakage/references/examples.md +275 -0
- package/presets/code-vibe/skills/dsh-trim-cot-leakage/references/recall-batteries.md +52 -0
- package/presets/code-vibe/skills/frontend-design/SKILL.md +60 -0
- package/presets/code-vibe/skills/frontend-design/references/critique-format.md +35 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/LOAD-MAP.md +81 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/README.md +53 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/anchor.md +51 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/audit/ai-tells.md +59 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/audit/web-interface.md +14 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/change-control.md +62 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/copy.md +65 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/craft-knobs.md +61 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/debug-playbook.md +37 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/exemplars/README.md +64 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/plan-architecture.checklist.json +13 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/product-judgment.md +113 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/system-architecture.md +108 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/tokens/README.md +26 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/tokens/color-roles.md +49 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/tokens/motion.md +51 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/tokens/scale.md +89 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/ui-page-gate.md +61 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/ui-surface.checklist.json +49 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/ux.md +76 -0
- package/presets/code-vibe/skills/frontend-design/references/design-standards/visual.md +52 -0
- package/presets/code-vibe/skills/frontend-design/references/perceptible-path.md +27 -0
- package/presets/code-vibe/skills/frontend-design/references/product-package.md +80 -0
- package/presets/code-vibe/skills/frontend-design/references/scheme-antipatterns.md +21 -0
- package/presets/code-vibe/skills/quality-review/SKILL.md +57 -0
- package/presets/code-vibe/skills/quality-review/references/bounded-verify.md +37 -0
- package/presets/code-vibe/skills/quality-review/references/browser-verify.md +81 -0
- package/presets/code-vibe/skills/quality-review/references/evidence-contract.md +194 -0
- package/presets/code-vibe/skills/quality-review/references/falsify-checklist.md +44 -0
- package/presets/code-vibe/skills/quality-review/references/human-acceptance-pack.md +68 -0
- package/presets/code-vibe/skills/quality-review/references/product-regression.md +51 -0
- package/presets/code-vibe/skills/quality-review/references/ux-standards.md +37 -0
- package/presets/code-vibe/skills/quality-review/references/ux-walkthrough.md +49 -0
- package/presets/code-vibe/skills/quality-review/references/validation-report.md +22 -0
- package/presets/code-vibe/skills/quality-review/references/verification-loop.md +95 -0
- package/presets/code-vibe/skills/quality-review/references/version-acceptance-matrix.md +131 -0
- package/presets/code-vibe/skills/record-browser-gif/SKILL.md +174 -0
- package/presets/code-vibe/skills/record-browser-gif/scripts/encode_gif.py +337 -0
- package/presets/code-vibe/skills/record-browser-gif/scripts/test_encode_gif.py +103 -0
- package/presets/code-vibe/skills/wish-programming/SKILL.md +150 -0
- package/presets/code-vibe/skills/wish-programming/references/roles/arch-reviewer.md +26 -0
- package/presets/code-vibe/skills/wish-programming/references/roles/design-reviewer.md +32 -0
- package/presets/code-vibe/skills/wish-programming/references/roles/pm-reviewer.md +26 -0
- package/presets/code-vibe/skills/wish-programming/references/roles/qa-reviewer.md +16 -0
- package/presets/code-vibe/skills/wish-programming/references/roles/security-reviewer.md +16 -0
- package/presets/code-vibe/skills/wish-programming/references/roles/sre-reviewer.md +16 -0
- package/presets/code-vibe/skills/wish-programming/references/roles/ux-adversary.md +16 -0
- package/presets/code-vibe/skills/wish-programming/references/templates/arch.md.tpl +51 -0
- package/presets/code-vibe/skills/wish-programming/references/templates/brief.md.tpl +36 -0
- package/presets/code-vibe/skills/wish-programming/references/templates/confirmation-pack.md +54 -0
- package/presets/code-vibe/skills/wish-programming/references/templates/design-spec.md.tpl +49 -0
- package/presets/code-vibe/skills/wish-programming/references/templates/module-readme.md +37 -0
- package/presets/code-vibe/skills/wish-programming/references/templates/product.md.tpl +74 -0
- package/presets/code-vibe/skills/wish-programming/references/templates/requirements.md.tpl +31 -0
- package/presets/code-vibe/skills/wish-programming/references/workflow-template.md +122 -0
- package/presets/code-vibe/skills/wish-programming/scripts/MANIFEST.sha256 +11 -0
- package/presets/code-vibe/skills/wish-programming/scripts/check_spec.py +416 -0
- package/presets/code-vibe/skills/wish-programming/scripts/check_whitelist.py +193 -0
- package/presets/code-vibe/skills/wish-programming/scripts/coverage_check.py +199 -0
- package/presets/code-vibe/skills/wish-programming/scripts/gate-lock.sh +46 -0
- package/presets/code-vibe/skills/wish-programming/scripts/journey.sh +281 -0
- package/presets/code-vibe/skills/wish-programming/scripts/project.sh +152 -0
- package/presets/code-vibe/skills/wish-programming/scripts/replay.sh +65 -0
- package/presets/code-vibe/skills/wish-programming/scripts/scan_drift.py +79 -0
- package/presets/code-vibe/skills/wish-programming/scripts/verify.sh +169 -0
- package/presets/code-vibe/skills/wish-programming/scripts/verify_preset_budgets.py +218 -0
- package/presets/code-vibe/skills/wish-programming/scripts/wish.sh +150 -0
- package/presets/code-vibe.patch.yml +325 -0
- package/presets/hiopen.patch.yml +109 -0
- package/src/client/compat.ts +86 -0
- package/src/client/conversation/conversation.module.css +264 -0
- package/src/client/conversation/git-branch.tsx +152 -0
- package/src/client/conversation/git-graph/LICENSE.git-graph +202 -0
- package/src/client/conversation/git-graph/api.ts +85 -0
- package/src/client/conversation/git-graph/branch-chip.tsx +354 -0
- package/src/client/conversation/git-graph/branch-popover.tsx +185 -0
- package/src/client/conversation/git-graph/chip.tsx +47 -0
- package/src/client/conversation/git-graph/create-branch-dialog.tsx +108 -0
- package/src/client/conversation/git-graph/error-copy.ts +50 -0
- package/src/client/conversation/git-graph/git-graph.module.css +792 -0
- package/src/client/conversation/git-graph/graph-dialog.tsx +196 -0
- package/src/client/conversation/git-graph/hero-seat.ts +76 -0
- package/src/client/conversation/git-graph/locales.ts +90 -0
- package/src/client/conversation/git-graph/sse-leader.ts +124 -0
- package/src/client/conversation/index.ts +82 -0
- package/src/client/conversation/selector-context.tsx +24 -0
- package/src/client/conversation/session-lane-chip.tsx +262 -0
- package/src/client/conversation/session-lane-locales.ts +34 -0
- package/src/client/conversation/subagent-model-dom.ts +157 -0
- package/src/client/conversation/subagent-model.ts +198 -0
- package/src/client/conversation/worktree/api.ts +283 -0
- package/src/client/conversation/worktree/dialogs/DeleteWorktreeDialog.tsx +111 -0
- package/src/client/conversation/worktree/dialogs/PrCreateDialog.tsx +114 -0
- package/src/client/conversation/worktree/lane-binding.ts +77 -0
- package/src/client/conversation/worktree/lane-menu.tsx +247 -0
- package/src/client/conversation/worktree/lane-open.ts +204 -0
- package/src/client/conversation/worktree/lane-workspace-sweeper.ts +136 -0
- package/src/client/conversation/worktree/lifecycle.ts +75 -0
- package/src/client/conversation/worktree/locales.ts +97 -0
- package/src/client/conversation/worktree/semantic.ts +10 -0
- package/src/client/conversation/worktree/worktree.module.css +70 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +58 -0
- package/src/client/lark-inbox/i18n.ts +125 -0
- package/src/client/lark-inbox/lark-wizard.module.css +88 -0
- package/src/client/lark-inbox/wizard.tsx +211 -0
- package/src/client/notify/i18n.ts +93 -0
- package/src/client/settings/family-scope.ts +45 -0
- package/src/client/settings/lark-inbox.tsx +319 -0
- package/src/client/settings/notify.tsx +288 -0
- package/src/client/settings/primitives-0.2.ts +148 -0
- package/src/client/settings/settings-shell.module.css +56 -0
- package/src/client/sidebar/RenderBoundary.tsx +17 -0
- package/src/client/sidebar/agent-browser.tsx +114 -0
- package/src/client/sidebar/diff-view.tsx +314 -0
- package/src/client/sidebar/git-workbench.tsx +700 -0
- package/src/client/sidebar/icons.tsx +23 -0
- package/src/client/sidebar/index.ts +28 -0
- package/src/client/sidebar/native-panels.tsx +149 -0
- package/src/client/sidebar/page-shell.module.css +96 -0
- package/src/client/sidebar/page-shell.tsx +86 -0
- package/src/client/sidebar/panels/memoir/api.ts +300 -0
- package/src/client/sidebar/panels/memoir/controller.ts +47 -0
- package/src/client/sidebar/panels/memoir/cwd.ts +140 -0
- package/src/client/sidebar/panels/memoir/i18n.ts +300 -0
- package/src/client/sidebar/panels/memoir/panel-activation.ts +42 -0
- package/src/client/sidebar/panels/memoir/panel.tsx +869 -0
- package/src/client/sidebar/panels/memoir/styles.ts +776 -0
- package/src/client/sidebar/panels/memoir/types.ts +7 -0
- package/src/client/sidebar/panels/ssh/ClusterTab.tsx +123 -0
- package/src/client/sidebar/panels/ssh/HostFormDialog.tsx +243 -0
- package/src/client/sidebar/panels/ssh/HostsTab.tsx +290 -0
- package/src/client/sidebar/panels/ssh/SshPanel.tsx +76 -0
- package/src/client/sidebar/panels/ssh/TerminalTab.tsx +290 -0
- package/src/client/sidebar/panels/ssh/TransferTab.tsx +232 -0
- package/src/client/sidebar/panels/ssh/TunnelsTab.tsx +204 -0
- package/src/client/sidebar/panels/ssh/api.ts +388 -0
- package/src/client/sidebar/panels/ssh/controller.ts +48 -0
- package/src/client/sidebar/panels/ssh/helpers.ts +70 -0
- package/src/client/sidebar/panels/ssh/locales.ts +318 -0
- package/src/client/sidebar/panels/ssh/panel.module.css +801 -0
- package/src/client/sidebar/panels/ssh/xterm.css.ts +2 -0
- package/src/client/sidebar/right-pane-i18n.ts +243 -0
- package/src/client/sidebar/right-pane.tsx +156 -0
- package/src/client/sidebar/scope.ts +54 -0
- package/src/client/sidebar/sidebar-header.module.css +72 -0
- package/src/client/sidebar/sidebar.module.css +619 -0
- package/src/client/trust.ts +187 -0
- package/src/client/ui-icons.ts +35 -0
- package/src/core/git-graph/commands.ts +107 -0
- package/src/core/git-graph/protocol.ts +238 -0
- package/src/core/index.ts +2 -0
- package/src/core/lark-inbox/gating.ts +35 -0
- package/src/core/lark-inbox/queue.ts +100 -0
- package/src/core/memoir/protocol.ts +46 -0
- package/src/core/no-browser-auth/contract.ts +25 -0
- package/src/core/notify/contract.ts +236 -0
- package/src/core/notify/feed.ts +235 -0
- package/src/core/playwright-sessions/contracts.ts +143 -0
- package/src/core/playwright-sessions/control.ts +33 -0
- package/src/core/playwright-sessions/index.ts +3 -0
- package/src/core/playwright-sessions/names.ts +83 -0
- package/src/core/playwright-sessions/schema.ts +102 -0
- package/src/core/playwright-sessions/wire.ts +142 -0
- package/src/core/remote-access/paths.ts +23 -0
- package/src/core/remote-web-ui/security.ts +171 -0
- package/src/core/right-pane/protocol.ts +94 -0
- package/src/core/schedule-mode/index.ts +6 -0
- package/src/core/schedule-mode/native.ts +105 -0
- package/src/core/schedule-mode/types.ts +182 -0
- package/src/core/schedule-mode/view.ts +66 -0
- package/src/core/ssh/protocol.ts +212 -0
- package/src/core/subagent-model/badges.ts +161 -0
- package/src/core/worktree/invariant.ts +101 -0
- package/src/core/worktree/manifest.ts +135 -0
- package/src/core/worktree/paths.ts +123 -0
- package/src/core/worktree/pr-create.ts +157 -0
- package/src/core/worktree/protocol.ts +7 -0
- package/src/core/worktree/types.ts +317 -0
- package/src/core/worktree/workspace-model.ts +61 -0
- package/src/host/services/access-trust/index.ts +127 -0
- package/src/host/services/access-trust/trust.ts +189 -0
- package/src/host/services/codex-bridge/bridge.ts +392 -0
- package/src/host/services/codex-bridge/codex-auth.ts +352 -0
- package/src/host/services/codex-bridge/dsh-integration.ts +169 -0
- package/src/host/services/codex-bridge/http.ts +105 -0
- package/src/host/services/codex-bridge/index.ts +154 -0
- package/src/host/services/codex-bridge/loopback.ts +63 -0
- package/src/host/services/codex-bridge/routes.ts +143 -0
- package/src/host/services/family-config.ts +224 -0
- package/src/host/services/family-namespaces.ts +39 -0
- package/src/host/services/git-graph/index.ts +11 -0
- package/src/host/services/git-graph/routes.ts +212 -0
- package/src/host/services/git-graph/service.ts +305 -0
- package/src/host/services/index.ts +94 -0
- package/src/host/services/lark-inbox/config.ts +66 -0
- package/src/host/services/lark-inbox/gateway.ts +184 -0
- package/src/host/services/lark-inbox/host-routes.ts +113 -0
- package/src/host/services/lark-inbox/inbox-store.ts +160 -0
- package/src/host/services/lark-inbox/index.ts +141 -0
- package/src/host/services/lark-inbox/tools.ts +124 -0
- package/src/host/services/lark-inbox/wizard.ts +206 -0
- package/src/host/services/lifecycle.ts +107 -0
- package/src/host/services/memoir/index.ts +80 -0
- package/src/host/services/memoir/routes.ts +346 -0
- package/src/host/services/memoir/settings.ts +122 -0
- package/src/host/services/memoir/store.ts +132 -0
- package/src/host/services/memoir/tools.ts +158 -0
- package/src/host/services/no-browser-auth/index.ts +70 -0
- package/src/host/services/notify/config.ts +178 -0
- package/src/host/services/notify/feishu-sender.ts +220 -0
- package/src/host/services/notify/host-routes.ts +133 -0
- package/src/host/services/notify/index.ts +305 -0
- package/src/host/services/playwright-sessions/child.ts +251 -0
- package/src/host/services/playwright-sessions/controller.ts +102 -0
- package/src/host/services/playwright-sessions/index.ts +146 -0
- package/src/host/services/playwright-sessions/live-view.ts +225 -0
- package/src/host/services/playwright-sessions/manager.ts +239 -0
- package/src/host/services/playwright-sessions/profile-gc.ts +97 -0
- package/src/host/services/playwright-sessions/routes.ts +90 -0
- package/src/host/services/remote-access/boot-scripts.ts +118 -0
- package/src/host/services/remote-access/index.ts +62 -0
- package/src/host/services/remote-access/routes.ts +137 -0
- package/src/host/services/remote-web-ui/index.ts +344 -0
- package/src/host/services/remote-web-ui/server-mode/accounts.ts +423 -0
- package/src/host/services/remote-web-ui/server-mode/cookie.ts +57 -0
- package/src/host/services/remote-web-ui/server-mode/dsh-home.ts +42 -0
- package/src/host/services/remote-web-ui/server-mode/external-auth.ts +296 -0
- package/src/host/services/remote-web-ui/server-mode/fence.ts +27 -0
- package/src/host/services/remote-web-ui/server-mode/gateway-routes.ts +797 -0
- package/src/host/services/remote-web-ui/server-mode/gateway.ts +684 -0
- package/src/host/services/remote-web-ui/server-mode/http.ts +105 -0
- package/src/host/services/remote-web-ui/server-mode/integrated-auth.ts +118 -0
- package/src/host/services/remote-web-ui/server-mode/lan.ts +19 -0
- package/src/host/services/remote-web-ui/server-mode/loopback.ts +63 -0
- package/src/host/services/remote-web-ui/server-mode/posture.ts +154 -0
- package/src/host/services/remote-web-ui/server-mode/roles.ts +51 -0
- package/src/host/services/remote-web-ui/server-mode/server-boot.ts +38 -0
- package/src/host/services/remote-web-ui/server-mode/sessions.ts +347 -0
- package/src/host/services/remote-web-ui/server.ts +591 -0
- package/src/host/services/resources.ts +78 -0
- package/src/host/services/right-pane/browser.ts +106 -0
- package/src/host/services/right-pane/index.ts +117 -0
- package/src/host/services/right-pane/wire.ts +10 -0
- package/src/host/services/schedule-mode/config.ts +26 -0
- package/src/host/services/schedule-mode/engine.ts +689 -0
- package/src/host/services/schedule-mode/index.ts +214 -0
- package/src/host/services/schedule-mode/routes.ts +130 -0
- package/src/host/services/schedule-mode/session-create.ts +36 -0
- package/src/host/services/schedule-mode/store.ts +304 -0
- package/src/host/services/schedule-mode/tools.ts +154 -0
- package/src/host/services/ssh/engine.ts +675 -0
- package/src/host/services/ssh/host-keys.ts +99 -0
- package/src/host/services/ssh/index.ts +84 -0
- package/src/host/services/ssh/routes.ts +442 -0
- package/src/host/services/ssh/store.ts +310 -0
- package/src/host/services/ssh/terminal-ws.ts +157 -0
- package/src/host/services/ssh/tools.ts +279 -0
- package/src/host/services/web-render/contracts.ts +138 -0
- package/src/host/services/web-render/index.ts +112 -0
- package/src/host/services/web-render/provider.ts +282 -0
- package/src/host/services/web-search-anthropic/contracts.ts +163 -0
- package/src/host/services/web-search-anthropic/index.ts +152 -0
- package/src/host/services/web-search-anthropic/provider.ts +325 -0
- package/src/host/services/worktree/git-runner.ts +124 -0
- package/src/host/services/worktree/git.ts +183 -0
- package/src/host/services/worktree/http.ts +36 -0
- package/src/host/services/worktree/index.ts +92 -0
- package/src/host/services/worktree/lane-tool.ts +100 -0
- package/src/host/services/worktree/manager.ts +1553 -0
- package/src/host/services/worktree/model-override.ts +43 -0
- package/src/host/services/worktree/routes.ts +570 -0
- package/src/host/services/worktree/run-command.ts +66 -0
- package/src/host/services/worktree/session-lane/manager.ts +228 -0
- package/src/host/services/worktree/workspace-model-routes.ts +156 -0
- package/src/host/services/worktree/workspace-model.ts +132 -0
- package/src/index.ts +84 -0
- package/src/invariant.ts +4 -0
|
@@ -0,0 +1,1553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host worktree manager: owns the per-repo worktree list, the on-disk
|
|
3
|
+
* manifest under `<repo>/.dsh/workspaces/`, and the workspace-registry
|
|
4
|
+
* bookkeeping. All mutations go through this class so the HTTP layer only
|
|
5
|
+
* knows how to translate envelopes and the browser half only ever sees the
|
|
6
|
+
* stable `WorktreeProject` / `WorktreeView` shapes from `core/types.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Persistence model
|
|
9
|
+
* - Each repo has its own manifest at `<repo>/.dsh/workspaces/manifest.json`
|
|
10
|
+
* - We never write the manifest from the browser; the host owns the file
|
|
11
|
+
* - The manifest records every worktree we ever created; a missing
|
|
12
|
+
* directory is the "orphan" state, not a hard error
|
|
13
|
+
*
|
|
14
|
+
* Concurrency
|
|
15
|
+
* - Per-repo lock prevents two `POST /worktree/create` calls from racing
|
|
16
|
+
* on the same repo (one would otherwise see the other's worktree
|
|
17
|
+
* directory and 409)
|
|
18
|
+
* - The lock is in-process only; out-of-process git contention is the
|
|
19
|
+
* user's responsibility (we do not lock across processes)
|
|
20
|
+
*
|
|
21
|
+
* Data flow
|
|
22
|
+
* - The host returns a worktree-only index (`WorktreeIndex`) on
|
|
23
|
+
* `GET /worktree/tree`. Session data lives in the official client
|
|
24
|
+
* service `ctx.sessions.list` and is never carried through the HTTP
|
|
25
|
+
* layer; the browser merges both sides.
|
|
26
|
+
* @module dsh-client-ui-worktree/host/manager
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
30
|
+
import { mkdir, readFile } from 'node:fs/promises'
|
|
31
|
+
import { dirname, resolve, sep } from 'node:path'
|
|
32
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
|
|
33
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
34
|
+
import type {} from '@deepseek-ai/dsh-subprocess'
|
|
35
|
+
import type {} from '@deepseek-ai/dsh-workspace'
|
|
36
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
37
|
+
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
|
38
|
+
import { subprocessRunner as sharedSubprocessRunner, type GitRunner, type GitRunResult } from './git-runner.ts'
|
|
39
|
+
import { WorktreeError, defaultMessage, isValidBranchName } from '../../../core/worktree/invariant.ts'
|
|
40
|
+
import {
|
|
41
|
+
type CleanupWorktreeInput, type CreateLaneInput, type CreateWorktreeInput, type InitWorktreeInput,
|
|
42
|
+
type LaneHandle, type LaneLookupInput, type LaneView,
|
|
43
|
+
type PrCreateInput, type PrCreateResult, type RemoveWorktreeInput,
|
|
44
|
+
type StatusInput, type WorktreeIndex, type WorktreeProject, type WorktreeStatusIndex,
|
|
45
|
+
type WorktreeStatusView, type WorktreeView,
|
|
46
|
+
} from '../../../core/worktree/types.ts'
|
|
47
|
+
import {
|
|
48
|
+
emptyManifest, parseManifest, type ParsedWorktreeManifest, type WorktreeManifestEntry, manifestPath,
|
|
49
|
+
} from '../../../core/worktree/manifest.ts'
|
|
50
|
+
import { WORKSPACE_DIR, githubCompareUrl, githubOwnerRepo, isInsidePath, samePath, worktreeDir, worktreeRoot } from '../../../core/worktree/paths.ts'
|
|
51
|
+
import {
|
|
52
|
+
branchDeleteArgv, BRANCH_LIST_ARGV, checkIgnoreArgv, FETCH_PRUNE_ARGV, gitSpawnArgv, lsRemoteBranchArgv,
|
|
53
|
+
mergeBaseIsAncestorArgv, parseBranchList, pushUpstreamArgv, REMOTE_HEAD_ARGV, REMOTE_LIST_ARGV,
|
|
54
|
+
remoteGetUrlArgv, verifyRefArgv, verifyRemoteRefArgv, worktreeAddArgv, worktreeListPorcelain,
|
|
55
|
+
worktreeRemoveArgv, WORKTREE_PRUNE_ARGV,
|
|
56
|
+
} from './git.ts'
|
|
57
|
+
|
|
58
|
+
/** Deadline for a git command that talks to the network (fetch / ls-remote / ancestor probe). */
|
|
59
|
+
const GIT_NETWORK_TIMEOUT_MS = 15_000
|
|
60
|
+
/** Deadline for a push; a large branch on a slow link still has to finish. */
|
|
61
|
+
const GIT_PUSH_TIMEOUT_MS = 180_000
|
|
62
|
+
/**
|
|
63
|
+
* How long a status probe may be reused. The panel asks for status whenever it
|
|
64
|
+
* opens and after every mutation, and one probe costs a `git fetch` plus an
|
|
65
|
+
* `ls-remote`/ancestor check per lane. So the answer is cached, every write this
|
|
66
|
+
* manager performs invalidates it, and the TTL only bounds what a user changes
|
|
67
|
+
* outside the plugin (a commit, or a push from a terminal).
|
|
68
|
+
*/
|
|
69
|
+
const STATUS_CACHE_TTL_MS = 60_000
|
|
70
|
+
/**
|
|
71
|
+
* How long the archive-driven lane retirement waits for the durable archive write
|
|
72
|
+
* to land, and how many times it re-checks. The `workspace/session-activity`
|
|
73
|
+
* waterfall runs BEFORE that write and cannot see its outcome.
|
|
74
|
+
*/
|
|
75
|
+
const LANE_RETIRE_POLL_MS = 200
|
|
76
|
+
const LANE_RETIRE_ATTEMPTS = 5
|
|
77
|
+
|
|
78
|
+
/** Optional knobs for the worktree manager (mainly test seams). */
|
|
79
|
+
export interface WorktreeManagerOptions {
|
|
80
|
+
/** GitHub token env-var reference (credential-ref) for PR creation via the REST API. */
|
|
81
|
+
tokenEnv?: string
|
|
82
|
+
/** Runner override for tests; defaults to the shared subprocess runner. */
|
|
83
|
+
runner?: GitRunner
|
|
84
|
+
/** Deadlines (ms) for the network-touching git calls; a test seam narrows them. */
|
|
85
|
+
timeouts?: { networkMs?: number; pushMs?: number }
|
|
86
|
+
/** Status cache lifetime in ms (0 disables the cache). */
|
|
87
|
+
statusTtlMs?: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Structural workspace shape used by cleanup (registry returns a richer entity). */
|
|
91
|
+
interface WorkspaceLike {
|
|
92
|
+
id: string
|
|
93
|
+
path: string
|
|
94
|
+
sessionIds?: readonly string[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Public surface used by `routes.ts`. */
|
|
98
|
+
export class WorktreeManager {
|
|
99
|
+
private readonly runner: GitRunner
|
|
100
|
+
private readonly locks = new Map<string, Promise<void>>()
|
|
101
|
+
/** Last status answer per repo root, invalidated by every manifest write. */
|
|
102
|
+
private readonly statusCache = new Map<string, { index: WorktreeStatusIndex; at: number }>()
|
|
103
|
+
|
|
104
|
+
/** Required services: the subprocess seam and the workspace registry. */
|
|
105
|
+
constructor(private readonly ctx: Context, private readonly options: WorktreeManagerOptions = {}) {
|
|
106
|
+
this.runner = options.runner ?? sharedSubprocessRunner(ctx, { spawnArgv: (argv) => gitSpawnArgv(process.platform, argv) })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Status cache lifetime for this manager (0 disables it). */
|
|
110
|
+
private get statusTtlMs(): number {
|
|
111
|
+
const configured = this.options.statusTtlMs
|
|
112
|
+
return typeof configured === 'number' && configured >= 0 ? configured : STATUS_CACHE_TTL_MS
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Network deadline for this manager (the options seam may narrow it). */
|
|
116
|
+
private get networkTimeoutMs(): number {
|
|
117
|
+
const configured = this.options.timeouts?.networkMs
|
|
118
|
+
return typeof configured === 'number' && configured > 0 ? configured : GIT_NETWORK_TIMEOUT_MS
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Push deadline for this manager (the options seam may narrow it). */
|
|
122
|
+
private get pushTimeoutMs(): number {
|
|
123
|
+
const configured = this.options.timeouts?.pushMs
|
|
124
|
+
return typeof configured === 'number' && configured > 0 ? configured : GIT_PUSH_TIMEOUT_MS
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Return the host-side worktree index: every registered workspace that
|
|
129
|
+
* lives under a git repo, plus the main worktree branch + dirty count
|
|
130
|
+
* and every linked worktree under `<repo>/.dsh/workspaces/`.
|
|
131
|
+
*/
|
|
132
|
+
async getIndex(): Promise<WorktreeIndex> {
|
|
133
|
+
const registry = this.ctx.workspaceRegistry
|
|
134
|
+
const workspaces = registry.list()
|
|
135
|
+
const projects: WorktreeProject[] = []
|
|
136
|
+
const seen = new Set<string>()
|
|
137
|
+
for (const ws of workspaces) {
|
|
138
|
+
const root = await this.repoRoot(ws.path)
|
|
139
|
+
if (root === null || seen.has(root)) continue
|
|
140
|
+
seen.add(root)
|
|
141
|
+
const project = await this.buildProject(root)
|
|
142
|
+
if (project === null) continue
|
|
143
|
+
projects.push(project)
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
projects: projects.sort((a, b) => a.name.localeCompare(b.name)),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a linked worktree that cuts a NEW branch (`input.branch`) at
|
|
152
|
+
* `input.base`, and register it as a DSH workspace. Registration is
|
|
153
|
+
* best-effort here (a failure only surfaces as `workspaceId: null`);
|
|
154
|
+
* `createForSession` is the variant that requires it.
|
|
155
|
+
*/
|
|
156
|
+
async create(input: CreateWorktreeInput): Promise<WorktreeView> {
|
|
157
|
+
if (!isValidBranchName(input.branch)) {
|
|
158
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { branch: input.branch })
|
|
159
|
+
}
|
|
160
|
+
const root = await this.repoRoot(input.repoPath)
|
|
161
|
+
if (root === null) {
|
|
162
|
+
throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
|
|
163
|
+
}
|
|
164
|
+
return this.withLock(root, async () => (await this.createLocked(root, input)).view)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Ensure the worktree behind one branch exists, and that its directory is a
|
|
169
|
+
* DSH workspace.
|
|
170
|
+
*
|
|
171
|
+
* This is the single creation path for a session-bound lane, and it merges
|
|
172
|
+
* what used to be two separate actions:
|
|
173
|
+
* - the branch already exists locally -> ATTACH it (git checks that
|
|
174
|
+
* branch out in its own directory; no new branch is invented);
|
|
175
|
+
* - the branch does not exist yet -> CUT it at `base` with `-b`.
|
|
176
|
+
* Either way the caller gets a directory that is that branch's checkout.
|
|
177
|
+
*
|
|
178
|
+
* Idempotent: an existing lane directory is returned as-is, so "switch to
|
|
179
|
+
* this branch" is safe to repeat and two sessions can share a branch.
|
|
180
|
+
*
|
|
181
|
+
* The workspace registration is not decoration and not ownership: the
|
|
182
|
+
* platform accounts a session to a workspace only while its cwd equals that
|
|
183
|
+
* workspace's path (`WorkspaceEntity.sessionIds`), and a blank conversation
|
|
184
|
+
* that no workspace accounts for renders an inert composer. Teardown
|
|
185
|
+
* unregisters the workspace with the directory, so the sidebar only carries
|
|
186
|
+
* lanes that are still alive.
|
|
187
|
+
*/
|
|
188
|
+
async ensureLane(input: CreateLaneInput): Promise<LaneHandle> {
|
|
189
|
+
if (!isValidBranchName(input.branch)) {
|
|
190
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { branch: input.branch })
|
|
191
|
+
}
|
|
192
|
+
const root = await this.repoRoot(input.repoPath)
|
|
193
|
+
if (root === null) {
|
|
194
|
+
throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
|
|
195
|
+
}
|
|
196
|
+
const owner = root
|
|
197
|
+
return this.withLock(owner, async () => {
|
|
198
|
+
await this.ensureIgnored(owner)
|
|
199
|
+
await this.ensureWorkspaceDir(owner)
|
|
200
|
+
const dir = worktreeDir(owner, input.branch)
|
|
201
|
+
const existing = await this.worktreeBranchAt(owner, dir)
|
|
202
|
+
if (existing !== undefined) {
|
|
203
|
+
// The lane is already on disk: make sure it is reachable as a workspace
|
|
204
|
+
// too, because that is what lets a blank conversation move onto it.
|
|
205
|
+
const workspaceId = await this.ensureLaneWorkspace(owner, input.branch, dir)
|
|
206
|
+
return { ...existing, created: false, workspaceId }
|
|
207
|
+
}
|
|
208
|
+
const attach = await this.branchExists(owner, input.branch)
|
|
209
|
+
const argv = attach
|
|
210
|
+
// Existing local branch: check THAT branch out in the new directory.
|
|
211
|
+
? worktreeAddArgv(dir, input.branch, false)
|
|
212
|
+
// New branch: `-b` is what makes the lane a usable work line. Without
|
|
213
|
+
// it git checks out the base commit-ish itself, which for anything
|
|
214
|
+
// that is not a branch name leaves a DETACHED HEAD — unpushable
|
|
215
|
+
// (`push -u origin <branch>` needs a local branch) and unmatched by
|
|
216
|
+
// `git worktree list --porcelain`.
|
|
217
|
+
: worktreeAddArgv(dir, input.base ?? 'HEAD', false, input.branch)
|
|
218
|
+
const result = await this.runner.run(argv, owner)
|
|
219
|
+
if (result.exitCode !== 0) {
|
|
220
|
+
throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
|
|
221
|
+
argv: [...argv], stderr: result.stderr,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
const head = (await this.runner.run(['rev-parse', 'HEAD'], dir)).stdout.trim()
|
|
225
|
+
await this.appendManifestEntry(owner, {
|
|
226
|
+
name: input.branch,
|
|
227
|
+
path: dir,
|
|
228
|
+
baseCommit: head,
|
|
229
|
+
createdAt: new Date().toISOString(),
|
|
230
|
+
detached: false,
|
|
231
|
+
})
|
|
232
|
+
return {
|
|
233
|
+
name: input.branch,
|
|
234
|
+
branch: input.branch,
|
|
235
|
+
path: dir,
|
|
236
|
+
head: head === '' ? null : await this.headShort(dir),
|
|
237
|
+
created: true,
|
|
238
|
+
// The lane's own DSH workspace. The platform accounts a session to a
|
|
239
|
+
// workspace only while its cwd equals that workspace's path, and a blank
|
|
240
|
+
// conversation no workspace accounts for renders its composer inert, so
|
|
241
|
+
// the lane directory has to BE a workspace for the conversation in it to
|
|
242
|
+
// be usable at all. Teardown removes it again.
|
|
243
|
+
workspaceId: await this.ensureLaneWorkspace(owner, input.branch, dir),
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The lane's DSH workspace, registered once and reused afterwards.
|
|
250
|
+
*
|
|
251
|
+
* Required, not a taste: `WorkspaceEntity.sessionIds` only accounts sessions
|
|
252
|
+
* whose cwd equals the workspace path, `attachSession` refuses any other cwd,
|
|
253
|
+
* and a blank conversation that no workspace accounts for shows an inert
|
|
254
|
+
* composer ("choose a workspace to start"). Registration failure is not fatal
|
|
255
|
+
* here — the worktree is real, so the caller reports the lane without a
|
|
256
|
+
* workspace and the browser declines to claim the conversation moved.
|
|
257
|
+
*/
|
|
258
|
+
private async ensureLaneWorkspace(root: string, branch: string, dir: string): Promise<WorkspaceId | null> {
|
|
259
|
+
const registered = await this.workspaceForPath(dir)
|
|
260
|
+
if (registered !== null) return registered.id as WorkspaceId
|
|
261
|
+
try {
|
|
262
|
+
const created = await this.ctx.workspaceRegistry.create(dir, `${basename(root)} / ${branch}`)
|
|
263
|
+
await this.updateManifestEntry(root, branch, { workspaceId: created.id as WorkspaceId })
|
|
264
|
+
return created.id as WorkspaceId
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.warn('[dsh-worktree] lane workspace registration failed:', error)
|
|
267
|
+
return null
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The lane already present at `dir`, when git reports one there. The
|
|
272
|
+
* workspace half is resolved by the caller (see ensureLane). */
|
|
273
|
+
private async worktreeBranchAt(root: string, dir: string): Promise<Omit<LaneHandle, 'created' | 'workspaceId'> | undefined> {
|
|
274
|
+
if (!existsSync(dir)) return undefined
|
|
275
|
+
const list = await this.runner.run(worktreeListPorcelain(), root)
|
|
276
|
+
if (list.exitCode !== 0) return undefined
|
|
277
|
+
const hit = parsePorcelainWorktrees(list.stdout).find((candidate) => resolve(candidate.path) === resolve(dir))
|
|
278
|
+
if (hit === undefined) return undefined
|
|
279
|
+
const branch = hit.branch ?? deriveNameFromPath(hit.path)
|
|
280
|
+
return {
|
|
281
|
+
name: branch,
|
|
282
|
+
branch,
|
|
283
|
+
path: hit.path,
|
|
284
|
+
head: await this.headShort(hit.path),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Shared create body; the caller holds the per-repo lock. */
|
|
289
|
+
private async createLocked(
|
|
290
|
+
root: string,
|
|
291
|
+
input: CreateWorktreeInput,
|
|
292
|
+
options: { requireWorkspace?: boolean } = {},
|
|
293
|
+
): Promise<{ view: WorktreeView; workspaceId: WorkspaceId | null }> {
|
|
294
|
+
await this.ensureIgnored(root)
|
|
295
|
+
await this.ensureWorkspaceDir(root)
|
|
296
|
+
const dir = worktreeDir(root, input.branch)
|
|
297
|
+
if (existsSync(dir)) {
|
|
298
|
+
throw new WorktreeError('worktree-exists', defaultMessage('worktree-exists'), { dir })
|
|
299
|
+
}
|
|
300
|
+
if (await this.branchExists(root, input.branch)) {
|
|
301
|
+
throw new WorktreeError('worktree-exists', `A local branch named "${input.branch}" already exists; choose another branch name or delete that branch first.`, { branch: input.branch })
|
|
302
|
+
}
|
|
303
|
+
const base = input.base ?? 'HEAD'
|
|
304
|
+
// `-b` is what makes the lane a usable work line. Without it git checks
|
|
305
|
+
// out the base commit-ish itself, which for anything that is not a
|
|
306
|
+
// branch name leaves a DETACHED HEAD: `git push -u origin <branch>`
|
|
307
|
+
// then fails (no local branch) and `git worktree list --porcelain`
|
|
308
|
+
// reports `detached`, so the lane never matches its own name.
|
|
309
|
+
const argv = worktreeAddArgv(dir, base, false, input.branch)
|
|
310
|
+
const result = await this.runner.run(argv, root)
|
|
311
|
+
if (result.exitCode !== 0) {
|
|
312
|
+
throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
|
|
313
|
+
argv: [...argv], stderr: result.stderr,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
const head = (await this.runner.run(['rev-parse', 'HEAD'], dir)).stdout.trim()
|
|
317
|
+
const entry: WorktreeManifestEntry = {
|
|
318
|
+
name: input.branch,
|
|
319
|
+
path: dir,
|
|
320
|
+
baseCommit: head,
|
|
321
|
+
createdAt: new Date().toISOString(),
|
|
322
|
+
detached: false,
|
|
323
|
+
}
|
|
324
|
+
await this.appendManifestEntry(root, entry)
|
|
325
|
+
let workspaceId: WorkspaceId | null = null
|
|
326
|
+
try {
|
|
327
|
+
const created = await this.ctx.workspaceRegistry.create(dir, `${basename(root)} / ${input.branch}`)
|
|
328
|
+
workspaceId = created.id
|
|
329
|
+
await this.updateManifestEntry(root, input.branch, { workspaceId })
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (options.requireWorkspace === true) {
|
|
332
|
+
await this.rollbackLocked(root, input.branch)
|
|
333
|
+
throw new WorktreeError(
|
|
334
|
+
'git-failed',
|
|
335
|
+
`workspace registration failed for lane "${input.branch}": ${String(error)}`,
|
|
336
|
+
{ dir },
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
console.warn('[dsh-worktree] workspace registration failed:', error)
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
view: {
|
|
343
|
+
name: input.branch,
|
|
344
|
+
path: dir,
|
|
345
|
+
branch: input.branch,
|
|
346
|
+
head: head === '' ? null : await this.headShort(dir),
|
|
347
|
+
exists: true,
|
|
348
|
+
workspaceId,
|
|
349
|
+
sessionId: null,
|
|
350
|
+
},
|
|
351
|
+
workspaceId,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The manifest record for one lane: its directory when the repository's
|
|
357
|
+
* manifest records that branch, null otherwise.
|
|
358
|
+
*
|
|
359
|
+
* The bind path reads this instead of trusting `laneForPath`, which
|
|
360
|
+
* synthesizes a lane view for a directory the manifest does not know about —
|
|
361
|
+
* so "the lane exists on disk" and "the host recorded this lane" are two
|
|
362
|
+
* different answers, and only the second may authorize a binding.
|
|
363
|
+
*/
|
|
364
|
+
async laneEntry(repoPath: string, name: string): Promise<{ root: string; path: string } | null> {
|
|
365
|
+
if (!isValidBranchName(name)) {
|
|
366
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name })
|
|
367
|
+
}
|
|
368
|
+
const root = await this.repoRoot(repoPath)
|
|
369
|
+
if (root === null) return null
|
|
370
|
+
const manifest = await this.readManifest(root)
|
|
371
|
+
const entry = manifest.worktrees.find((candidate) => candidate.name === name)
|
|
372
|
+
return entry === undefined ? null : { root, path: entry.path }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Record the session bound to a lane. Called right after the session exists,
|
|
377
|
+
* so the header chip and the teardown guards can resolve `sessionId -> lane`
|
|
378
|
+
* without re-deriving it from the filesystem.
|
|
379
|
+
* @param repoPath - repository root.
|
|
380
|
+
* @param name - worktree (branch) name.
|
|
381
|
+
* @param sessionId - the session to record.
|
|
382
|
+
* @param permissionPresetApplied - whether the lane's access preset landed.
|
|
383
|
+
*/
|
|
384
|
+
async bindSession(
|
|
385
|
+
repoPath: string,
|
|
386
|
+
name: string,
|
|
387
|
+
sessionId: SessionId,
|
|
388
|
+
permissionPresetApplied: boolean,
|
|
389
|
+
): Promise<void> {
|
|
390
|
+
if (!isValidBranchName(name)) {
|
|
391
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name })
|
|
392
|
+
}
|
|
393
|
+
const root = await this.repoRoot(repoPath)
|
|
394
|
+
if (root === null) {
|
|
395
|
+
throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: repoPath })
|
|
396
|
+
}
|
|
397
|
+
await this.withLock(root, async () => {
|
|
398
|
+
await this.updateManifestEntry(root, name, { sessionId, permissionPresetApplied })
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Undo a lane creation. Used when the lane exists but its session could
|
|
404
|
+
* not be created, so a failed create never leaves half a lane behind:
|
|
405
|
+
* worktree directory, local branch, admin records, manifest entry and the
|
|
406
|
+
* registered workspace all go away.
|
|
407
|
+
*/
|
|
408
|
+
async rollbackCreate(input: { repoPath: string; name: string }): Promise<void> {
|
|
409
|
+
if (!isValidBranchName(input.name)) {
|
|
410
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name: input.name })
|
|
411
|
+
}
|
|
412
|
+
const root = await this.repoRoot(input.repoPath)
|
|
413
|
+
if (root === null) {
|
|
414
|
+
throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
|
|
415
|
+
}
|
|
416
|
+
await this.withLock(root, async () => {
|
|
417
|
+
// Rollback exists for a lane whose session never came up. A lane that has
|
|
418
|
+
// a live session under it is not that case: deleting the directory and the
|
|
419
|
+
// branch out from under a running conversation is exactly what remove and
|
|
420
|
+
// cleanup refuse to do, so rollback must not be the way around them.
|
|
421
|
+
const dir = worktreeDir(root, input.name)
|
|
422
|
+
const workspace = await this.workspaceForPath(dir)
|
|
423
|
+
const busySession = await this.liveSessionOnLane(root, dir, workspace)
|
|
424
|
+
if (busySession !== undefined) {
|
|
425
|
+
throw new WorktreeError('worktree-busy', defaultMessage('worktree-busy'), { sessionId: busySession })
|
|
426
|
+
}
|
|
427
|
+
await this.rollbackLocked(root, input.name)
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Resolve the lane a path (or a session's cwd) belongs to. Returns null
|
|
433
|
+
* for the main checkout and for any path outside `<repo>/.dsh/workspaces/`.
|
|
434
|
+
*
|
|
435
|
+
* Deliberately network-free: this feeds the conversation header chip, so
|
|
436
|
+
* it must stay cheap. `merged` is therefore only the locally knowable
|
|
437
|
+
* half (branch pushed AND its tip is an ancestor of the base); the
|
|
438
|
+
* "remote branch deleted on merge" signal needs a `fetch --prune` and
|
|
439
|
+
* stays with the panel's `POST /worktree/status` probe.
|
|
440
|
+
*/
|
|
441
|
+
async laneForPath(input: LaneLookupInput): Promise<LaneView | null> {
|
|
442
|
+
// 1. Cwd-based: the session is already inside a lane directory.
|
|
443
|
+
const target = input.path ?? (input.sessionId === undefined ? undefined : this.sessionCwd(input.sessionId))
|
|
444
|
+
if (target !== undefined && target !== '') {
|
|
445
|
+
const root = await this.repoRoot(target)
|
|
446
|
+
if (root !== null) {
|
|
447
|
+
const laneRoot = resolve(worktreeRoot(root))
|
|
448
|
+
const normalized = resolve(target)
|
|
449
|
+
if (normalized !== laneRoot && normalized.startsWith(laneRoot + sep)) {
|
|
450
|
+
const fromPath = await this.laneForDirectory(root, normalized)
|
|
451
|
+
if (fromPath !== null) return fromPath
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// 2. Binding-based: the conversation carries a lane recorded on the
|
|
456
|
+
// manifest even though its cwd is still the main checkout (an
|
|
457
|
+
// existing conversation cannot move). Without this a mid-conversation
|
|
458
|
+
// branch switch would be invisible to the header chip and the lane tool.
|
|
459
|
+
if (input.sessionId !== undefined) {
|
|
460
|
+
const cwd = this.sessionCwd(input.sessionId)
|
|
461
|
+
if (cwd !== undefined) {
|
|
462
|
+
const root = await this.repoRoot(cwd)
|
|
463
|
+
if (root !== null) {
|
|
464
|
+
const entry = await this.manifestEntryForSession(root, input.sessionId)
|
|
465
|
+
if (entry !== undefined) return this.laneFromEntry(root, entry)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return null
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** One lane record for a directory git reports as a linked worktree. */
|
|
473
|
+
private async laneForDirectory(root: string, dir: string): Promise<LaneView | null> {
|
|
474
|
+
const listResult = await this.runner.run(worktreeListPorcelain(), root)
|
|
475
|
+
if (listResult.exitCode !== 0) return null
|
|
476
|
+
const wt = parsePorcelainWorktrees(listResult.stdout).find((candidate) => resolve(candidate.path) === dir)
|
|
477
|
+
if (wt === undefined) return null
|
|
478
|
+
const manifest = await this.readManifest(root)
|
|
479
|
+
const name = wt.branch ?? deriveNameFromPath(wt.path)
|
|
480
|
+
const entry = manifest.worktrees.find((e) => e.path === wt.path || e.name === name)
|
|
481
|
+
return this.laneFromEntry(root, entry ?? { name, path: wt.path } as WorktreeManifestEntry, wt.branch, dir)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** One lane record from a manifest entry, with the git state filled in. */
|
|
485
|
+
private async laneFromEntry(
|
|
486
|
+
root: string,
|
|
487
|
+
entry: WorktreeManifestEntry,
|
|
488
|
+
branchHint?: string | null,
|
|
489
|
+
pathHint?: string,
|
|
490
|
+
): Promise<LaneView> {
|
|
491
|
+
const path = pathHint ?? entry.path
|
|
492
|
+
const name = entry.name
|
|
493
|
+
const branch = branchHint ?? name
|
|
494
|
+
const base = await this.remoteDefaultBranch(root) ?? 'main'
|
|
495
|
+
// The lane view answers the same question the cleanup guard does, through
|
|
496
|
+
// the same rule. Deriving "merged" here from `pushed` alone made the header
|
|
497
|
+
// chip say "not pushed" for every branch GitHub deleted on merge — while
|
|
498
|
+
// cleanup, which uses worktreeStatus, considered it merged and tore it down.
|
|
499
|
+
const status = await this.worktreeStatus(root, name, base, entry)
|
|
500
|
+
return {
|
|
501
|
+
name,
|
|
502
|
+
branch,
|
|
503
|
+
path,
|
|
504
|
+
repoPath: root,
|
|
505
|
+
exists: existsSync(path),
|
|
506
|
+
workspaceId: (entry.workspaceId as WorkspaceId | undefined) ?? null,
|
|
507
|
+
sessionId: (entry.sessionId as SessionId | undefined) ?? null,
|
|
508
|
+
permissionPresetApplied: entry.permissionPresetApplied ?? null,
|
|
509
|
+
dirty: await this.dirtyCount(path),
|
|
510
|
+
pushed: status.pushed,
|
|
511
|
+
remoteBranchGone: status.remoteBranchGone,
|
|
512
|
+
merged: status.merged,
|
|
513
|
+
prUrl: entry.prUrl ?? null,
|
|
514
|
+
prNumber: entry.prNumber ?? null,
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** The manifest entry a session is bound to, within one repo. */
|
|
519
|
+
private async manifestEntryForSession(root: string, sessionId: string): Promise<WorktreeManifestEntry | undefined> {
|
|
520
|
+
const manifest = await this.readManifest(root)
|
|
521
|
+
return manifest.worktrees.find((entry) => entry.sessionId === sessionId)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Archive the session a lane was bound to, when one is recorded. Once the
|
|
526
|
+
* lane directory is gone the conversation would otherwise keep pointing at
|
|
527
|
+
* a deleted path; archiving hides the record instead of letting it dangle.
|
|
528
|
+
* Best-effort — the workspace registry may be unavailable.
|
|
529
|
+
*
|
|
530
|
+
* DSH 0.2 refuses to archive a session that reports active work (a running
|
|
531
|
+
* turn, a job, a subagent, an active schedule) unless `stopActivity` is
|
|
532
|
+
* requested; the lane's own session is exactly the one this teardown is
|
|
533
|
+
* allowed to take down, so the archive must write first and then ask the
|
|
534
|
+
* activity owners to stop.
|
|
535
|
+
*/
|
|
536
|
+
private async archiveBoundSession(root: string, name: string): Promise<void> {
|
|
537
|
+
const manifest = await this.readManifest(root)
|
|
538
|
+
const sessionId = manifest.worktrees.find((entry) => entry.name === name)?.sessionId
|
|
539
|
+
if (sessionId === undefined) return
|
|
540
|
+
try {
|
|
541
|
+
await this.ctx.workspaceRegistry.archiveSession(sessionId as SessionId, { stopActivity: true })
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.warn(`[dsh-worktree] archive session ${sessionId} failed:`, error)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Refuse to create a lane when the repository does not ignore the lane
|
|
549
|
+
* root. Without the ignore rule the lane directory shows up as untracked
|
|
550
|
+
* in the main checkout, and a `git add -A` there would sweep an entire
|
|
551
|
+
* lane into the trunk — exactly the trunk pollution that lanes exist to
|
|
552
|
+
* prevent.
|
|
553
|
+
*/
|
|
554
|
+
private async ensureIgnored(root: string): Promise<void> {
|
|
555
|
+
// Probe a child path so a directory rule (`/.dsh/workspaces/`) matches.
|
|
556
|
+
const probe = `${worktreeRoot(root)}/probe`
|
|
557
|
+
const result = await this.runner.run(checkIgnoreArgv(probe), root)
|
|
558
|
+
if (result.exitCode !== 0) {
|
|
559
|
+
throw new WorktreeError(
|
|
560
|
+
'worktree-not-ignored',
|
|
561
|
+
`${defaultMessage('worktree-not-ignored')} Add "${WORKSPACE_DIR}/" to ${root}/.gitignore.`,
|
|
562
|
+
{
|
|
563
|
+
dir: worktreeRoot(root),
|
|
564
|
+
hint: `add "${WORKSPACE_DIR}/" to ${root}/.gitignore`,
|
|
565
|
+
},
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Whether a local branch of that name already exists. */
|
|
571
|
+
private async branchExists(root: string, branch: string): Promise<boolean> {
|
|
572
|
+
const result = await this.runner.run(verifyRefArgv(branch), root)
|
|
573
|
+
return result.exitCode === 0
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** The cwd recorded on a live session header, when the host can see it. */
|
|
577
|
+
private sessionCwd(sessionId: string): string | undefined {
|
|
578
|
+
const sessions = optionalService<{ get?: (id: string) => { header?: { cwd?: string } } | undefined }>(this.ctx, 'sessions')
|
|
579
|
+
const cwd = sessions?.get?.(sessionId)?.header?.cwd
|
|
580
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : undefined
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Patch one manifest entry in place; a missing entry is a no-op. */
|
|
584
|
+
private async updateManifestEntry(
|
|
585
|
+
repoPath: string,
|
|
586
|
+
name: string,
|
|
587
|
+
patch: Partial<WorktreeManifestEntry>,
|
|
588
|
+
): Promise<void> {
|
|
589
|
+
const manifest = await this.readManifest(repoPath)
|
|
590
|
+
let touched = false
|
|
591
|
+
manifest.worktrees = manifest.worktrees.map((entry) => {
|
|
592
|
+
if (entry.name !== name) return entry
|
|
593
|
+
touched = true
|
|
594
|
+
return { ...entry, ...patch }
|
|
595
|
+
})
|
|
596
|
+
if (touched) await this.writeManifest(repoPath, manifest)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Best-effort teardown of a lane that never became usable. */
|
|
600
|
+
private async rollbackLocked(root: string, name: string): Promise<void> {
|
|
601
|
+
const dir = worktreeDir(root, name)
|
|
602
|
+
if (existsSync(dir)) {
|
|
603
|
+
await this.runner.run(worktreeRemoveArgv(dir, true), root)
|
|
604
|
+
}
|
|
605
|
+
if (await this.branchExists(root, name)) {
|
|
606
|
+
await this.runner.run(branchDeleteArgv(name, true), root)
|
|
607
|
+
}
|
|
608
|
+
await this.runner.run(WORKTREE_PRUNE_ARGV, root)
|
|
609
|
+
await this.removeManifestEntry(root, name)
|
|
610
|
+
const workspace = await this.workspaceForPath(dir)
|
|
611
|
+
if (workspace !== null) {
|
|
612
|
+
try {
|
|
613
|
+
await this.ctx.workspaceRegistry.delete(workspace.id as WorkspaceId)
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.warn('[dsh-worktree] rollback workspace unregister failed:', error)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Remove a worktree without silently discarding user state. A live session
|
|
622
|
+
* always blocks removal; a dirty checkout requires the caller's explicit
|
|
623
|
+
* force confirmation. Cleanup remains the preferred merged-aware flow.
|
|
624
|
+
*/
|
|
625
|
+
async remove(input: RemoveWorktreeInput): Promise<{ name: string; path: string }> {
|
|
626
|
+
if (!isValidBranchName(input.name)) {
|
|
627
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name: input.name })
|
|
628
|
+
}
|
|
629
|
+
const root = await this.repoRoot(input.repoPath)
|
|
630
|
+
if (root === null) {
|
|
631
|
+
throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
|
|
632
|
+
}
|
|
633
|
+
return this.withLock(root, async () => {
|
|
634
|
+
const dir = worktreeDir(root, input.name)
|
|
635
|
+
if (!existsSync(dir)) {
|
|
636
|
+
throw new WorktreeError('worktree-not-found', defaultMessage('worktree-not-found'), { dir })
|
|
637
|
+
}
|
|
638
|
+
const workspace = await this.workspaceForPath(dir)
|
|
639
|
+
const busySession = await this.liveSessionOnLane(root, dir, workspace, input.sessionId)
|
|
640
|
+
if (busySession !== undefined) {
|
|
641
|
+
throw new WorktreeError('worktree-busy', defaultMessage('worktree-busy'), { sessionId: busySession })
|
|
642
|
+
}
|
|
643
|
+
const dirty = await this.runner.run(['status', '--porcelain'], dir)
|
|
644
|
+
if (dirty.exitCode !== 0) {
|
|
645
|
+
throw new WorktreeError('git-failed', defaultMessage('git-failed'), { stderr: dirty.stderr })
|
|
646
|
+
}
|
|
647
|
+
if (dirty.stdout.trim() !== '' && input.force !== true) {
|
|
648
|
+
throw new WorktreeError('worktree-busy', 'The worktree has uncommitted changes; confirm force removal to discard them.', { dir, dirty: true })
|
|
649
|
+
}
|
|
650
|
+
const argv = worktreeRemoveArgv(dir, input.force === true)
|
|
651
|
+
const result = await this.runner.run(argv, root)
|
|
652
|
+
if (result.exitCode !== 0) {
|
|
653
|
+
throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
|
|
654
|
+
argv: [...argv], stderr: result.stderr,
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
await this.runner.run(WORKTREE_PRUNE_ARGV, root)
|
|
658
|
+
await this.archiveBoundSession(root, input.name)
|
|
659
|
+
await this.removeManifestEntry(root, input.name)
|
|
660
|
+
// Unregister the workspace resolved BEFORE the directory went away: a
|
|
661
|
+
// lookup by a deleted path finds nothing, and the sidebar would keep a
|
|
662
|
+
// stale row for a directory that is gone (and offer to start a session in
|
|
663
|
+
// it). Cleanup already resolved it up front for the same reason.
|
|
664
|
+
if (workspace !== null) {
|
|
665
|
+
try {
|
|
666
|
+
await this.ctx.workspaceRegistry.delete(workspace.id as WorkspaceId)
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.warn('[dsh-worktree] workspace unregister failed:', error)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return { name: input.name, path: dir }
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* `git init -b <initialBranch>` on a directory that is not yet a git
|
|
677
|
+
* repository; idempotent (a no-op if the directory is already a repo).
|
|
678
|
+
* The host-side caller is the browser dialog that asks "convert this
|
|
679
|
+
* workspace into a git project so I can use worktrees on it".
|
|
680
|
+
*/
|
|
681
|
+
async init(input: InitWorktreeInput): Promise<{ repoPath: string; initialBranch: string }> {
|
|
682
|
+
const path = resolve(input.repoPath)
|
|
683
|
+
if (!existsSync(path)) {
|
|
684
|
+
throw new WorktreeError('workspace-unknown', defaultMessage('workspace-unknown'), { path })
|
|
685
|
+
}
|
|
686
|
+
const existing = await this.repoRoot(path)
|
|
687
|
+
if (existing !== null) {
|
|
688
|
+
return { repoPath: existing, initialBranch: input.initialBranch ?? 'main' }
|
|
689
|
+
}
|
|
690
|
+
// Converting a directory into a repository mutates that directory, so it is
|
|
691
|
+
// limited to a workspace the user actually registered with the host. Without
|
|
692
|
+
// this the route is a `git init` anywhere on the machine.
|
|
693
|
+
const workspace = await this.workspaceForPath(path)
|
|
694
|
+
if (workspace === null || !samePath(workspace.path, path)) {
|
|
695
|
+
throw new WorktreeError('workspace-unknown', defaultMessage('workspace-unknown'), { path })
|
|
696
|
+
}
|
|
697
|
+
const branch = input.initialBranch ?? 'main'
|
|
698
|
+
if (!isValidBranchName(branch)) {
|
|
699
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { branch })
|
|
700
|
+
}
|
|
701
|
+
const argv = gitSpawnArgv(process.platform, ['init', '-b', branch])
|
|
702
|
+
const result = await this.runner.run(argv, path)
|
|
703
|
+
if (result.exitCode !== 0) {
|
|
704
|
+
throw new WorktreeError('git-failed', defaultMessage('git-failed'), { stderr: result.stderr })
|
|
705
|
+
}
|
|
706
|
+
await this.ensureWorkspaceDir(path)
|
|
707
|
+
return { repoPath: path, initialBranch: branch }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ─── PR lifecycle: create / status / cleanup ─────────────────────────────
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Push the worktree branch and open (or create) a pull request. Always
|
|
714
|
+
* pushes to `origin`; a PR page is then produced either through the
|
|
715
|
+
* GitHub compare URL (default, no credentials) or through the GitHub
|
|
716
|
+
* REST API when a `tokenEnv` credential is configured. The resulting
|
|
717
|
+
* URL and metadata are recorded in the manifest so the panel can resume
|
|
718
|
+
* the PR later.
|
|
719
|
+
*/
|
|
720
|
+
async prCreate(input: PrCreateInput): Promise<PrCreateResult> {
|
|
721
|
+
if (!isValidBranchName(input.name)) {
|
|
722
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name: input.name })
|
|
723
|
+
}
|
|
724
|
+
const root = await this.repoRoot(input.repoPath)
|
|
725
|
+
if (root === null) {
|
|
726
|
+
throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
|
|
727
|
+
}
|
|
728
|
+
const dir = worktreeDir(root, input.name)
|
|
729
|
+
if (!existsSync(dir)) {
|
|
730
|
+
throw new WorktreeError('worktree-not-found', defaultMessage('worktree-not-found'), { dir })
|
|
731
|
+
}
|
|
732
|
+
const remoteUrl = await this.originRemoteUrl(root)
|
|
733
|
+
if (remoteUrl === null) {
|
|
734
|
+
throw new WorktreeError('no-remote', defaultMessage('no-remote'), { repoPath: root })
|
|
735
|
+
}
|
|
736
|
+
return this.withLock(root, async () => {
|
|
737
|
+
// Push the branch with upstream tracking, from the worktree itself.
|
|
738
|
+
const push = await this.runBounded(pushUpstreamArgv(input.name), dir, this.pushTimeoutMs)
|
|
739
|
+
if (push.exitCode !== 0) {
|
|
740
|
+
throw new WorktreeError('push-failed', defaultMessage('push-failed'), {
|
|
741
|
+
branch: input.name, stderr: push.stderr,
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
const manifest = await this.readManifest(root)
|
|
745
|
+
const baseRef = input.baseRef ?? manifest.baseBranch ?? await this.remoteDefaultBranch(root) ?? 'main'
|
|
746
|
+
const title = input.title?.trim() !== '' && input.title !== undefined ? input.title!.trim() : input.name
|
|
747
|
+
// Preferred: REST API PR creation when a token is configured.
|
|
748
|
+
const viaApi = await this.createPullRequestViaApi(root, remoteUrl, input.name, baseRef, title)
|
|
749
|
+
let prUrl: string | null
|
|
750
|
+
let prNumber: number | undefined
|
|
751
|
+
let method: PrCreateResult['method']
|
|
752
|
+
if (viaApi !== null) {
|
|
753
|
+
prUrl = viaApi.htmlUrl
|
|
754
|
+
prNumber = viaApi.number
|
|
755
|
+
method = 'api'
|
|
756
|
+
} else {
|
|
757
|
+
prUrl = githubCompareUrl(remoteUrl, baseRef, input.name)
|
|
758
|
+
if (prUrl === null) {
|
|
759
|
+
throw new WorktreeError('unsupported-remote', defaultMessage('unsupported-remote'), { remoteUrl })
|
|
760
|
+
}
|
|
761
|
+
method = 'url'
|
|
762
|
+
}
|
|
763
|
+
const entry = manifest.worktrees.find((e) => e.name === input.name)
|
|
764
|
+
if (entry === undefined) {
|
|
765
|
+
throw new WorktreeError('worktree-not-found', defaultMessage('worktree-not-found'), { name: input.name })
|
|
766
|
+
}
|
|
767
|
+
entry.prUrl = prUrl
|
|
768
|
+
if (prNumber !== undefined) entry.prNumber = prNumber
|
|
769
|
+
entry.remoteBranch = 'origin'
|
|
770
|
+
entry.baseRef = baseRef
|
|
771
|
+
entry.updatedAt = new Date().toISOString()
|
|
772
|
+
if (manifest.baseBranch === undefined) manifest.baseBranch = baseRef
|
|
773
|
+
await this.writeManifest(root, manifest)
|
|
774
|
+
return { prUrl, method }
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* PR / merge status for one repo (or one named worktree of it): whether
|
|
780
|
+
* the branch was pushed, whether the remote branch is gone, and whether
|
|
781
|
+
* it is merged into the base. Runs a best-effort `git fetch origin
|
|
782
|
+
* --prune` first so a merged-and-deleted remote branch reads as merged.
|
|
783
|
+
*
|
|
784
|
+
* A full-repo answer is cached: this endpoint is polled whenever the panel
|
|
785
|
+
* opens, while every mutation this manager performs invalidates the entry, so
|
|
786
|
+
* a poll is a memory read in the normal flow. `force` (the explicit refresh
|
|
787
|
+
* action) and a name-filtered probe always go to git.
|
|
788
|
+
*/
|
|
789
|
+
async status(input: StatusInput): Promise<WorktreeStatusIndex> {
|
|
790
|
+
const root = await this.repoRoot(input.repoPath)
|
|
791
|
+
if (root === null) {
|
|
792
|
+
throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
|
|
793
|
+
}
|
|
794
|
+
if (input.name !== undefined) return await this.probeStatus(root, input.name)
|
|
795
|
+
if (input.force !== true) {
|
|
796
|
+
const cached = this.cachedStatus(root)
|
|
797
|
+
if (cached !== undefined) return cached
|
|
798
|
+
}
|
|
799
|
+
const index = await this.probeStatus(root, undefined)
|
|
800
|
+
this.statusCache.set(root, { index, at: Date.now() })
|
|
801
|
+
return index
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** The cached status for a repo, or undefined when absent or past its TTL. */
|
|
805
|
+
private cachedStatus(root: string): WorktreeStatusIndex | undefined {
|
|
806
|
+
const ttl = this.statusTtlMs
|
|
807
|
+
if (ttl <= 0) return undefined
|
|
808
|
+
const hit = this.statusCache.get(root)
|
|
809
|
+
if (hit === undefined) return undefined
|
|
810
|
+
if (Date.now() - hit.at >= ttl) {
|
|
811
|
+
this.statusCache.delete(root)
|
|
812
|
+
return undefined
|
|
813
|
+
}
|
|
814
|
+
return hit.index
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Drop the cached status for a repo; every manifest write calls this. */
|
|
818
|
+
private invalidateStatus(root: string): void {
|
|
819
|
+
this.statusCache.delete(root)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** One uncached probe of the repository. */
|
|
823
|
+
private async probeStatus(root: string, name: string | undefined): Promise<WorktreeStatusIndex> {
|
|
824
|
+
const baseBranch = await this.remoteDefaultBranch(root) ?? 'main'
|
|
825
|
+
const manifest = await this.readManifest(root)
|
|
826
|
+
const effectiveBase = manifest.baseBranch ?? baseBranch
|
|
827
|
+
await this.fetchPruneBestEffort(root)
|
|
828
|
+
const listResult = await this.runner.run(worktreeListPorcelain(), root)
|
|
829
|
+
const parsed = listResult.exitCode === 0 ? parsePorcelainWorktrees(listResult.stdout) : []
|
|
830
|
+
const worktrees: WorktreeStatusView[] = []
|
|
831
|
+
for (const wt of parsed) {
|
|
832
|
+
if (wt.path === root) continue
|
|
833
|
+
if (!wt.path.startsWith(worktreeRoot(root) + '/') && !wt.path.startsWith(worktreeRoot(root) + '\\')) continue
|
|
834
|
+
if (name !== undefined && wt.branch !== name) continue
|
|
835
|
+
const entry = manifest.worktrees.find((e) => e.path === wt.path)
|
|
836
|
+
const branch = wt.branch
|
|
837
|
+
if (branch === null) {
|
|
838
|
+
worktrees.push({
|
|
839
|
+
name: entry?.name ?? deriveNameFromPath(wt.path),
|
|
840
|
+
path: wt.path,
|
|
841
|
+
branch: null,
|
|
842
|
+
pushed: false,
|
|
843
|
+
remoteBranchGone: false,
|
|
844
|
+
merged: false,
|
|
845
|
+
prUrl: entry?.prUrl ?? null,
|
|
846
|
+
})
|
|
847
|
+
continue
|
|
848
|
+
}
|
|
849
|
+
const statusView = await this.worktreeStatus(root, branch, effectiveBase, entry)
|
|
850
|
+
worktrees.push({
|
|
851
|
+
name: entry?.name ?? deriveNameFromPath(wt.path),
|
|
852
|
+
path: wt.path,
|
|
853
|
+
branch,
|
|
854
|
+
...statusView,
|
|
855
|
+
prUrl: entry?.prUrl ?? null,
|
|
856
|
+
})
|
|
857
|
+
}
|
|
858
|
+
const branchResult = await this.runner.run(BRANCH_LIST_ARGV, root)
|
|
859
|
+
const branches = branchResult.exitCode === 0 ? parseBranchList(branchResult.stdout) : []
|
|
860
|
+
return { baseBranch: effectiveBase, worktrees, branches }
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Clean up a merged worktree: validate no sessions are running, verify
|
|
865
|
+
* the branch is merged (or the user forced), remove the worktree
|
|
866
|
+
* directory, delete the local branch, unregister the workspace, and
|
|
867
|
+
* archive its sessions. `force` abandons an unmerged branch and drops
|
|
868
|
+
* uncommitted changes.
|
|
869
|
+
*/
|
|
870
|
+
async cleanup(input: CleanupWorktreeInput): Promise<{ name: string; path: string }> {
|
|
871
|
+
if (!isValidBranchName(input.name)) {
|
|
872
|
+
throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name: input.name })
|
|
873
|
+
}
|
|
874
|
+
const root = await this.repoRoot(input.repoPath)
|
|
875
|
+
if (root === null) {
|
|
876
|
+
throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
|
|
877
|
+
}
|
|
878
|
+
const dir = worktreeDir(root, input.name)
|
|
879
|
+
const dirExists = existsSync(dir)
|
|
880
|
+
// A missing directory is an orphan: skip the worktree-remove step and
|
|
881
|
+
// still delete the branch / manifest entry / workspace below.
|
|
882
|
+
return this.withLock(root, async () => {
|
|
883
|
+
// Never clean up a worktree a live session is bound to — except the
|
|
884
|
+
// conversation that is itself asking (see liveSessionOnLane).
|
|
885
|
+
const workspace = await this.workspaceForPath(dir)
|
|
886
|
+
const busySession = await this.liveSessionOnLane(root, dir, workspace, input.sessionId)
|
|
887
|
+
if (busySession !== undefined) {
|
|
888
|
+
throw new WorktreeError('worktree-busy', defaultMessage('worktree-busy'), { sessionId: busySession })
|
|
889
|
+
}
|
|
890
|
+
const baseBranch = await this.remoteDefaultBranch(root) ?? 'main'
|
|
891
|
+
const manifest = await this.readManifest(root)
|
|
892
|
+
const effectiveBase = manifest.baseBranch ?? baseBranch
|
|
893
|
+
const entry = manifest.worktrees.find((e) => e.name === input.name)
|
|
894
|
+
// Deleting the local branch is this flow's job, so it requires the host's
|
|
895
|
+
// own record of the lane. Without it, any name that happens to be a local
|
|
896
|
+
// branch - a directory nobody recorded, or a branch with no lane at all -
|
|
897
|
+
// would be deleted here with nothing tying it to this plugin. The directory
|
|
898
|
+
// may be gone (an orphan lane) as long as the record survives.
|
|
899
|
+
if (entry === undefined) {
|
|
900
|
+
throw new WorktreeError('worktree-not-found', defaultMessage('worktree-not-found'), { name: input.name })
|
|
901
|
+
}
|
|
902
|
+
await this.fetchPruneBestEffort(root)
|
|
903
|
+
// Always probe. The merged decision must not depend on whether THIS
|
|
904
|
+
// plugin created the PR: a branch pushed from the lane terminal has to
|
|
905
|
+
// reach the same answer, or the panel offers a cleanup the host refuses.
|
|
906
|
+
const status = await this.worktreeStatus(root, input.name, effectiveBase, entry)
|
|
907
|
+
const merged = status.merged || input.force === true
|
|
908
|
+
if (!merged) {
|
|
909
|
+
throw new WorktreeError('not-merged', defaultMessage('not-merged'), { branch: input.name, base: effectiveBase })
|
|
910
|
+
}
|
|
911
|
+
if (dirExists) {
|
|
912
|
+
const remove = await this.runner.run(worktreeRemoveArgv(dir, input.force === true), root)
|
|
913
|
+
if (remove.exitCode !== 0) {
|
|
914
|
+
throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
|
|
915
|
+
argv: [...worktreeRemoveArgv(dir, input.force === true)], stderr: remove.stderr,
|
|
916
|
+
})
|
|
917
|
+
}
|
|
918
|
+
await this.runner.run(WORKTREE_PRUNE_ARGV, root)
|
|
919
|
+
}
|
|
920
|
+
// Delete the local branch only when the branch exists (detached worktrees skip).
|
|
921
|
+
const verify = await this.runner.run(['rev-parse', '--verify', '--quiet', `refs/heads/${input.name}`], root)
|
|
922
|
+
if (verify.exitCode === 0) {
|
|
923
|
+
const deleteArgv = branchDeleteArgv(input.name, input.force === true)
|
|
924
|
+
const deleted = await this.runner.run(deleteArgv, root)
|
|
925
|
+
if (deleted.exitCode !== 0) {
|
|
926
|
+
// `-d` also refuses a branch merged into the PR base but not into the
|
|
927
|
+
// main worktree's current HEAD. The merge state is verified above, so
|
|
928
|
+
// escalate instead of reporting success with the branch left behind.
|
|
929
|
+
const forced = input.force === true ? deleted : await this.runner.run(branchDeleteArgv(input.name, true), root)
|
|
930
|
+
if (forced.exitCode !== 0) {
|
|
931
|
+
throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
|
|
932
|
+
argv: [...deleteArgv], stderr: forced.stderr,
|
|
933
|
+
})
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
await this.archiveBoundSession(root, input.name)
|
|
938
|
+
await this.removeManifestEntry(root, input.name)
|
|
939
|
+
if (workspace !== null) {
|
|
940
|
+
await this.archiveAndDeleteWorkspace(workspace)
|
|
941
|
+
}
|
|
942
|
+
return { name: input.name, path: dir }
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ─── lane workspace lifecycle ─────────────────────────────────────────────
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Retire the workspace registration of a lane whose conversation was archived.
|
|
950
|
+
*
|
|
951
|
+
* A lane's directory must be a registered workspace while its conversation
|
|
952
|
+
* runs — the platform accounts a session to a workspace only when the session
|
|
953
|
+
* cwd equals that workspace's path, and a blank conversation no workspace
|
|
954
|
+
* accounts for renders an inert composer — so the sidebar row is unavoidable
|
|
955
|
+
* while the lane is alive. It does not have to outlive the conversation:
|
|
956
|
+
* archiving retires the registration, which keeps the worktree, the branch and
|
|
957
|
+
* the binding, so picking that branch again re-registers the directory in one
|
|
958
|
+
* call and nothing the user did is discarded.
|
|
959
|
+
*
|
|
960
|
+
* The `workspace/session-activity` waterfall runs BEFORE the durable archive
|
|
961
|
+
* write, so this waits for the archive set to show the session instead of
|
|
962
|
+
* assuming the write lands: an archive refused for running work retires
|
|
963
|
+
* nothing. A lane that still has an unarchived conversation keeps its
|
|
964
|
+
* workspace, or that conversation would lose the workspace it runs in.
|
|
965
|
+
* @param sessionId - the conversation that was archived.
|
|
966
|
+
* @param options - bounded-poll knobs (test seam).
|
|
967
|
+
* @returns whether a lane workspace was retired.
|
|
968
|
+
*/
|
|
969
|
+
async retireArchivedLane(sessionId: string, options: { attempts?: number; pollMs?: number } = {}): Promise<boolean> {
|
|
970
|
+
const attempts = Math.max(1, options.attempts ?? LANE_RETIRE_ATTEMPTS)
|
|
971
|
+
const pollMs = options.pollMs ?? LANE_RETIRE_POLL_MS
|
|
972
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
973
|
+
if (this.isArchivedSession(sessionId)) return await this.retireLaneWorkspaceOf(sessionId)
|
|
974
|
+
if (attempt >= attempts) return false
|
|
975
|
+
await new Promise((resolve) => { setTimeout(resolve, pollMs) })
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Retire every lane workspace whose recorded conversation is archived.
|
|
981
|
+
*
|
|
982
|
+
* Rows left behind by an earlier build, or by an archive that happened while
|
|
983
|
+
* this plugin was absent, must not survive a restart; this walks the registered
|
|
984
|
+
* workspaces once and drops exactly those lanes. Anything else is left alone: a
|
|
985
|
+
* workspace outside `<repo>/.dsh/workspaces/`, a lane whose manifest records no
|
|
986
|
+
* conversation, a lane whose recorded conversation is still live, and a lane
|
|
987
|
+
* that still has an unarchived conversation.
|
|
988
|
+
* @returns how many lane workspaces were retired.
|
|
989
|
+
*/
|
|
990
|
+
async reconcileLaneWorkspaces(): Promise<number> {
|
|
991
|
+
let workspaces: readonly WorkspaceLike[]
|
|
992
|
+
try {
|
|
993
|
+
workspaces = this.ctx.workspaceRegistry.list() as readonly WorkspaceLike[]
|
|
994
|
+
} catch (error) {
|
|
995
|
+
console.warn('[dsh-worktree] lane workspace reconciliation skipped:', error)
|
|
996
|
+
return 0
|
|
997
|
+
}
|
|
998
|
+
let retired = 0
|
|
999
|
+
for (const workspace of workspaces) {
|
|
1000
|
+
if (typeof workspace.path !== 'string' || workspace.path === '') continue
|
|
1001
|
+
const root = await this.repoRoot(workspace.path)
|
|
1002
|
+
if (root === null) continue
|
|
1003
|
+
if (!isInsidePath(worktreeRoot(root), workspace.path)) continue
|
|
1004
|
+
const manifest = await this.readManifest(root)
|
|
1005
|
+
const entry = manifest.worktrees.find((candidate) => samePath(candidate.path, workspace.path))
|
|
1006
|
+
if (entry?.sessionId === undefined || !this.isArchivedSession(entry.sessionId)) continue
|
|
1007
|
+
if (!this.hasNoLiveConversation(workspace)) continue
|
|
1008
|
+
if (await this.retireLaneWorkspace(root, entry)) retired += 1
|
|
1009
|
+
}
|
|
1010
|
+
return retired
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// ─── private helpers ──────────────────────────────────────────────────────
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Run one git command under a deadline. An aborted command surfaces as
|
|
1017
|
+
* exitCode 124 with the reason on stderr, so every caller keeps its existing
|
|
1018
|
+
* exitCode check as the single failure path. Without this a stalled
|
|
1019
|
+
* `git fetch` held the per-repo lock forever and left the panel loading.
|
|
1020
|
+
*/
|
|
1021
|
+
private async runBounded(argv: readonly string[], cwd: string, timeoutMs: number): Promise<GitRunResult> {
|
|
1022
|
+
try {
|
|
1023
|
+
return await this.runner.run(argv, cwd, AbortSignal.timeout(timeoutMs))
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
return {
|
|
1026
|
+
exitCode: 124,
|
|
1027
|
+
stdout: '',
|
|
1028
|
+
stderr: `git timed out after ${String(timeoutMs)}ms: ${error instanceof Error ? error.message : String(error)}`,
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** Acquire the per-repo lock for the duration of `fn`. */
|
|
1034
|
+
private async withLock<T>(repoRoot: string, fn: () => Promise<T>): Promise<T> {
|
|
1035
|
+
const previous = this.locks.get(repoRoot) ?? Promise.resolve()
|
|
1036
|
+
let release: (() => void) | undefined
|
|
1037
|
+
const next = new Promise<void>((resolve) => { release = resolve })
|
|
1038
|
+
this.locks.set(repoRoot, previous.then(() => next))
|
|
1039
|
+
await previous
|
|
1040
|
+
try {
|
|
1041
|
+
return await fn()
|
|
1042
|
+
} finally {
|
|
1043
|
+
release!()
|
|
1044
|
+
if (this.locks.get(repoRoot) === next) this.locks.delete(repoRoot)
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Resolve the canonical git common-dir for `path`, or null when `path`
|
|
1050
|
+
* is not inside a git working tree. We do this with `git rev-parse
|
|
1051
|
+
* --path-format=absolute --git-common-dir` so the result follows
|
|
1052
|
+
* `git`'s notion of canonicalization (worktrees resolve to the main
|
|
1053
|
+
* repo), not `realpath`'s.
|
|
1054
|
+
*/
|
|
1055
|
+
private async repoRoot(path: string): Promise<string | null> {
|
|
1056
|
+
if (path === '' || !existsSync(path)) return null
|
|
1057
|
+
const result = await this.runner.run(
|
|
1058
|
+
['rev-parse', '--path-format=absolute', '--git-common-dir'],
|
|
1059
|
+
path,
|
|
1060
|
+
)
|
|
1061
|
+
if (result.exitCode !== 0) return null
|
|
1062
|
+
const trimmed = result.stdout.trim()
|
|
1063
|
+
if (trimmed === '') return null
|
|
1064
|
+
return trimmed.endsWith('/.git') || trimmed.endsWith('\\.git')
|
|
1065
|
+
? dirname(trimmed)
|
|
1066
|
+
: trimmed.replace(/[\\/]+$/, '')
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ─── PR lifecycle helpers ────────────────────────────────────────────────
|
|
1070
|
+
|
|
1071
|
+
/** Resolve the `origin` remote URL, or null when no remote is configured. */
|
|
1072
|
+
private async originRemoteUrl(root: string): Promise<string | null> {
|
|
1073
|
+
const list = await this.runner.run(REMOTE_LIST_ARGV, root)
|
|
1074
|
+
if (list.exitCode !== 0) return null
|
|
1075
|
+
if (!list.stdout.split('\n').map((line) => line.trim()).includes('origin')) return null
|
|
1076
|
+
const url = await this.runner.run(remoteGetUrlArgv('origin'), root)
|
|
1077
|
+
if (url.exitCode !== 0) return null
|
|
1078
|
+
const trimmed = url.stdout.trim()
|
|
1079
|
+
return trimmed === '' ? null : trimmed
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** Remote default branch (`refs/remotes/origin/HEAD`), or null when unset. */
|
|
1083
|
+
private async remoteDefaultBranch(root: string): Promise<string | null> {
|
|
1084
|
+
const result = await this.runner.run(REMOTE_HEAD_ARGV, root)
|
|
1085
|
+
if (result.exitCode !== 0) return null
|
|
1086
|
+
const ref = result.stdout.trim()
|
|
1087
|
+
if (ref === '') return null
|
|
1088
|
+
return ref.startsWith('refs/remotes/origin/') ? ref.slice('refs/remotes/origin/'.length) : ref
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/** Best-effort `git fetch origin --prune`; a failure (offline) is ignored. */
|
|
1092
|
+
private async fetchPruneBestEffort(root: string): Promise<void> {
|
|
1093
|
+
try {
|
|
1094
|
+
await this.runBounded(FETCH_PRUNE_ARGV, root, this.networkTimeoutMs)
|
|
1095
|
+
} catch {
|
|
1096
|
+
// offline / no network — keep stale refs and report from what we have
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** Whether `refs/remotes/origin/<branch>` exists after the last fetch. */
|
|
1101
|
+
private async remoteBranchPushed(root: string, branch: string): Promise<boolean> {
|
|
1102
|
+
const result = await this.runner.run(verifyRemoteRefArgv(branch), root)
|
|
1103
|
+
return result.exitCode === 0
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/** Whether the remote branch is gone (ls-remote --exit-code non-zero). */
|
|
1107
|
+
private async remoteBranchGone(root: string, branch: string): Promise<boolean> {
|
|
1108
|
+
const result = await this.runBounded(lsRemoteBranchArgv(branch), root, this.networkTimeoutMs)
|
|
1109
|
+
// A deadline is not evidence: an unanswered probe must not read as "gone",
|
|
1110
|
+
// because "gone" is a merged signal that lets a cleanup delete the lane.
|
|
1111
|
+
if (result.exitCode === 124) return false
|
|
1112
|
+
return result.exitCode !== 0
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/** Whether `origin/<branch>` is an ancestor of `origin/<base>` (or local `<base>`). */
|
|
1116
|
+
private async isAncestorOfBase(root: string, branch: string, base: string): Promise<boolean> {
|
|
1117
|
+
const originBase = await this.runner.run(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${base}`], root)
|
|
1118
|
+
const baseRef = originBase.exitCode === 0 ? `refs/remotes/origin/${base}` : base
|
|
1119
|
+
const result = await this.runBounded(mergeBaseIsAncestorArgv(`refs/remotes/origin/${branch}`, baseRef), root, this.networkTimeoutMs)
|
|
1120
|
+
return result.exitCode === 0
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Whether the LOCAL branch tip is an ancestor of the base. Used when the
|
|
1125
|
+
* tracking ref is already pruned: the remote signal is gone, but the local
|
|
1126
|
+
* branch still answers whether its work landed in the base.
|
|
1127
|
+
*/
|
|
1128
|
+
private async isLocalAncestorOfBase(root: string, branch: string, base: string): Promise<boolean> {
|
|
1129
|
+
const originBase = await this.runner.run(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${base}`], root)
|
|
1130
|
+
const baseRef = originBase.exitCode === 0 ? `refs/remotes/origin/${base}` : base
|
|
1131
|
+
const result = await this.runBounded(mergeBaseIsAncestorArgv(`refs/heads/${branch}`, baseRef), root, this.networkTimeoutMs)
|
|
1132
|
+
return result.exitCode === 0
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Whether the local branch moved past the commit the lane was cut from. A
|
|
1137
|
+
* lane that never committed is an ancestor of its base too, so this is what
|
|
1138
|
+
* separates "merged work" from "no work yet".
|
|
1139
|
+
*/
|
|
1140
|
+
private async branchTipMoved(root: string, branch: string, baseCommit: string): Promise<boolean> {
|
|
1141
|
+
if (baseCommit === '') return false
|
|
1142
|
+
const result = await this.runner.run(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], root)
|
|
1143
|
+
if (result.exitCode !== 0) return false
|
|
1144
|
+
const tip = result.stdout.trim()
|
|
1145
|
+
return tip !== '' && tip !== baseCommit
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* The merged signal for one branch, in two independent halves:
|
|
1150
|
+
*
|
|
1151
|
+
* 1. the remote branch still exists -> merged when its tip is an ancestor of
|
|
1152
|
+
* the base tip;
|
|
1153
|
+
* 2. the tracking ref is pruned -> either the branch was never pushed, or it
|
|
1154
|
+
* was deleted on merge ("delete branch on merge" is GitHub's default).
|
|
1155
|
+
* `remoteBranch` on the manifest only records a push this plugin made, so
|
|
1156
|
+
* a manually pushed branch would read as unmerged forever; the manifest's
|
|
1157
|
+
* `baseCommit` plus the local tip answer the same question without it.
|
|
1158
|
+
*/
|
|
1159
|
+
private async worktreeStatus(
|
|
1160
|
+
root: string,
|
|
1161
|
+
branch: string,
|
|
1162
|
+
base: string,
|
|
1163
|
+
entry: WorktreeManifestEntry | undefined,
|
|
1164
|
+
): Promise<{ pushed: boolean; remoteBranchGone: boolean; merged: boolean }> {
|
|
1165
|
+
const pushed = await this.remoteBranchPushed(root, branch)
|
|
1166
|
+
if (!pushed) {
|
|
1167
|
+
// tracking ref pruned: either never pushed, or pushed then deleted on merge
|
|
1168
|
+
const wasPushedBefore = entry?.remoteBranch === 'origin'
|
|
1169
|
+
const gone = wasPushedBefore && await this.remoteBranchGone(root, branch)
|
|
1170
|
+
const moved = entry?.baseCommit !== undefined && await this.branchTipMoved(root, branch, entry.baseCommit)
|
|
1171
|
+
const merged = gone || (moved && await this.isLocalAncestorOfBase(root, branch, base))
|
|
1172
|
+
return { pushed: false, remoteBranchGone: gone, merged }
|
|
1173
|
+
}
|
|
1174
|
+
const merged = await this.isAncestorOfBase(root, branch, base)
|
|
1175
|
+
return { pushed: true, remoteBranchGone: false, merged }
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Create the PR through the GitHub REST API when a token is configured
|
|
1180
|
+
* and the remote is GitHub. Returns null to fall back to the compare URL
|
|
1181
|
+
* when no token is configured, the remote is not GitHub, or the API call
|
|
1182
|
+
* fails (the compare URL remains the zero-credential default).
|
|
1183
|
+
*/
|
|
1184
|
+
private async createPullRequestViaApi(
|
|
1185
|
+
root: string,
|
|
1186
|
+
remoteUrl: string,
|
|
1187
|
+
branch: string,
|
|
1188
|
+
baseRef: string,
|
|
1189
|
+
title: string,
|
|
1190
|
+
): Promise<{ htmlUrl: string; number: number } | null> {
|
|
1191
|
+
const parsed = githubOwnerRepo(remoteUrl)
|
|
1192
|
+
if (parsed === null) return null
|
|
1193
|
+
const tokenEnv = this.options.tokenEnv
|
|
1194
|
+
if (tokenEnv === undefined || tokenEnv === '') return null
|
|
1195
|
+
const credentials = (this.ctx as unknown as { credentials?: { resolve?: (ref: string) => Promise<{ value?: string } | undefined> } }).credentials
|
|
1196
|
+
if (credentials?.resolve === undefined) return null
|
|
1197
|
+
const credential = await credentials.resolve(tokenEnv)
|
|
1198
|
+
const token = credential?.value
|
|
1199
|
+
if (token === undefined || token === '') return null
|
|
1200
|
+
try {
|
|
1201
|
+
const response = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}/pulls`, {
|
|
1202
|
+
method: 'POST',
|
|
1203
|
+
headers: {
|
|
1204
|
+
authorization: `Bearer ${token}`,
|
|
1205
|
+
accept: 'application/vnd.github+json',
|
|
1206
|
+
'content-type': 'application/json',
|
|
1207
|
+
},
|
|
1208
|
+
body: JSON.stringify({ title, head: branch, base: baseRef }),
|
|
1209
|
+
})
|
|
1210
|
+
if (!response.ok) {
|
|
1211
|
+
console.warn(`[dsh-worktree] GitHub PR create failed with status ${response.status}; falling back to compare URL`)
|
|
1212
|
+
return null
|
|
1213
|
+
}
|
|
1214
|
+
const payload = await response.json() as { html_url?: string; number?: number }
|
|
1215
|
+
if (typeof payload.html_url !== 'string' || typeof payload.number !== 'number') return null
|
|
1216
|
+
return { htmlUrl: payload.html_url, number: payload.number }
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
console.warn('[dsh-worktree] GitHub API unreachable; falling back to compare URL:', error)
|
|
1219
|
+
return null
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/** Resolve the DSH workspace registered for a worktree directory, or null. */
|
|
1224
|
+
private async workspaceForPath(dir: string): Promise<WorkspaceLike | null> {
|
|
1225
|
+
try {
|
|
1226
|
+
const ws = await this.ctx.workspaceRegistry.resolveByPath(dir)
|
|
1227
|
+
return ws === undefined || ws === null ? null : ws
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
console.warn('[dsh-worktree] resolveByPath failed:', error)
|
|
1230
|
+
return null
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** Whether the registry's durable archive set holds this session. */
|
|
1235
|
+
private isArchivedSession(sessionId: string): boolean {
|
|
1236
|
+
try {
|
|
1237
|
+
const registry = this.ctx.workspaceRegistry as unknown as { archivedSessionIds?: readonly string[] }
|
|
1238
|
+
return (registry.archivedSessionIds ?? []).includes(sessionId)
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
console.warn('[dsh-worktree] could not read the workspace archive set:', error)
|
|
1241
|
+
return false
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** Whether every conversation a workspace accounts for has been archived. */
|
|
1246
|
+
private hasNoLiveConversation(workspace: WorkspaceLike): boolean {
|
|
1247
|
+
return this.sessionIdsOf(workspace).every((sessionId) => this.isArchivedSession(sessionId))
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** Retire the workspace of the lane that records this conversation. */
|
|
1251
|
+
private async retireLaneWorkspaceOf(sessionId: string): Promise<boolean> {
|
|
1252
|
+
const cwd = this.sessionCwd(sessionId)
|
|
1253
|
+
if (cwd === undefined) return false
|
|
1254
|
+
const root = await this.repoRoot(cwd)
|
|
1255
|
+
if (root === null) return false
|
|
1256
|
+
const manifest = await this.readManifest(root)
|
|
1257
|
+
const entry = manifest.worktrees.find((candidate) => candidate.sessionId === sessionId)
|
|
1258
|
+
if (entry === undefined) return false
|
|
1259
|
+
return await this.retireLaneWorkspace(root, entry)
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Drop one lane's workspace registration and the id recorded for it. The
|
|
1264
|
+
* directory, the worktree record, the branch and the session binding survive,
|
|
1265
|
+
* so picking that branch again re-registers the directory in one call.
|
|
1266
|
+
*/
|
|
1267
|
+
private async retireLaneWorkspace(root: string, entry: WorktreeManifestEntry): Promise<boolean> {
|
|
1268
|
+
return this.withLock(root, async () => {
|
|
1269
|
+
const workspace = await this.workspaceForPath(entry.path)
|
|
1270
|
+
if (workspace === null) return false
|
|
1271
|
+
// A lane can host more than one conversation (two sessions on the same
|
|
1272
|
+
// branch). Retiring the workspace under a live one would take its composer
|
|
1273
|
+
// away, so the row stays until every conversation there is archived.
|
|
1274
|
+
if (!this.hasNoLiveConversation(workspace)) return false
|
|
1275
|
+
try {
|
|
1276
|
+
await this.ctx.workspaceRegistry.delete(workspace.id as WorkspaceId)
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
console.warn('[dsh-worktree] lane workspace retirement failed:', error)
|
|
1279
|
+
return false
|
|
1280
|
+
}
|
|
1281
|
+
await this.updateManifestEntry(root, entry.name, { workspaceId: undefined })
|
|
1282
|
+
return true
|
|
1283
|
+
})
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** Session ids recorded on a workspace (structural — avoids registry type churn). */
|
|
1287
|
+
private sessionIdsOf(workspace: WorkspaceLike): readonly string[] {
|
|
1288
|
+
return workspace.sessionIds ?? []
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** Whether a session id is currently live in the session store. */
|
|
1292
|
+
private isLiveSession(sessionId: string): boolean {
|
|
1293
|
+
const sessions = optionalService<{ get?: (id: string) => unknown }>(this.ctx, 'sessions')
|
|
1294
|
+
return sessions?.get?.(sessionId) !== undefined
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* The live session still using this lane, if any.
|
|
1299
|
+
*
|
|
1300
|
+
* Two sources: the lane's workspace accounts for the sessions whose cwd is that
|
|
1301
|
+
* directory, and the manifest entry records the session this plugin created
|
|
1302
|
+
* there. The manifest half is what stops `cleanup`/`remove` from deleting the
|
|
1303
|
+
* directory a running conversation is working in.
|
|
1304
|
+
*
|
|
1305
|
+
* `self` is the conversation asking for the teardown through its own lane
|
|
1306
|
+
* chip. It may excuse itself — otherwise the interlock would make a lane the
|
|
1307
|
+
* conversation sits on impossible to ever tear down (the chip is the only
|
|
1308
|
+
* surface left once the conversation is gone), and the teardown archives that
|
|
1309
|
+
* session anyway. The excusal is not a bypass: the lane has to actually
|
|
1310
|
+
* record the id (as its bound session, or as a session of its workspace), so
|
|
1311
|
+
* an unrecorded or foreign id excuses nothing.
|
|
1312
|
+
*/
|
|
1313
|
+
private async liveSessionOnLane(
|
|
1314
|
+
root: string,
|
|
1315
|
+
dir: string,
|
|
1316
|
+
workspace: WorkspaceLike | null,
|
|
1317
|
+
self?: string,
|
|
1318
|
+
): Promise<string | undefined> {
|
|
1319
|
+
const recorded = new Set<string>()
|
|
1320
|
+
if (workspace !== null) for (const sessionId of this.sessionIdsOf(workspace)) recorded.add(sessionId)
|
|
1321
|
+
const manifest = await this.readManifest(root)
|
|
1322
|
+
const entry = manifest.worktrees.find((candidate) => candidate.path === dir)
|
|
1323
|
+
if (entry?.sessionId !== undefined) recorded.add(entry.sessionId)
|
|
1324
|
+
const excused = self !== undefined && recorded.has(self) ? self : undefined
|
|
1325
|
+
for (const sessionId of recorded) {
|
|
1326
|
+
if (sessionId !== excused && this.isLiveSession(sessionId)) return sessionId
|
|
1327
|
+
}
|
|
1328
|
+
return undefined
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** Archive every session of a worktree workspace, then unregister it. */
|
|
1332
|
+
private async archiveAndDeleteWorkspace(workspace: WorkspaceLike): Promise<void> {
|
|
1333
|
+
for (const sessionId of this.sessionIdsOf(workspace)) {
|
|
1334
|
+
try {
|
|
1335
|
+
// See archiveBoundSession: 0.2 archives write first, then stop the
|
|
1336
|
+
// session's running work, instead of refusing an active session.
|
|
1337
|
+
await this.ctx.workspaceRegistry.archiveSession(sessionId as SessionId, { stopActivity: true })
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
console.warn(`[dsh-worktree] archive session ${sessionId} failed:`, error)
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
await this.ctx.workspaceRegistry.delete(workspace.id as WorkspaceId)
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
console.warn(`[dsh-worktree] workspace unregister failed for ${workspace.id}:`, error)
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Resolve the DSH workspace registered for a path. `resolveByPath` compares
|
|
1351
|
+
* the literal string, but git reports canonicalized paths (`/private/tmp/...`
|
|
1352
|
+
* on macOS while the registry holds `/tmp/...`), so a miss falls back to
|
|
1353
|
+
* comparing realpaths. Without this the main row's Open / New session buttons
|
|
1354
|
+
* stay disabled for every workspace behind a symlinked path.
|
|
1355
|
+
*/
|
|
1356
|
+
private async workspaceIdFor(path: string): Promise<WorkspaceId | null> {
|
|
1357
|
+
try {
|
|
1358
|
+
const direct = await this.ctx.workspaceRegistry.resolveByPath(path)
|
|
1359
|
+
if (direct !== undefined && direct !== null) return direct.id
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
console.warn('[dsh-worktree] resolveByPath failed:', error)
|
|
1362
|
+
}
|
|
1363
|
+
const wanted = canonicalPath(path)
|
|
1364
|
+
if (wanted === null) return null
|
|
1365
|
+
try {
|
|
1366
|
+
const listed = this.ctx.workspaceRegistry.list() as readonly { id: string; path?: unknown }[]
|
|
1367
|
+
for (const workspace of listed) {
|
|
1368
|
+
if (typeof workspace.path !== 'string') continue
|
|
1369
|
+
if (canonicalPath(workspace.path) === wanted) return workspace.id as WorkspaceId
|
|
1370
|
+
}
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
console.warn('[dsh-worktree] workspace list failed:', error)
|
|
1373
|
+
}
|
|
1374
|
+
return null
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Build the per-repo project view: main + linked worktrees under
|
|
1379
|
+
* `<repo>/.dsh/workspaces/`. The `git worktree list --porcelain`
|
|
1380
|
+
* output is parsed by hand because it is the only stable format and
|
|
1381
|
+
* we want zero dependencies.
|
|
1382
|
+
*/
|
|
1383
|
+
private async buildProject(repoPath: string): Promise<WorktreeProject | null> {
|
|
1384
|
+
const listResult = await this.runner.run(worktreeListPorcelain(), repoPath)
|
|
1385
|
+
const parsed = listResult.exitCode === 0 ? parsePorcelainWorktrees(listResult.stdout) : []
|
|
1386
|
+
const [mainBranch, mainDirty] = await Promise.all([
|
|
1387
|
+
this.headBranch(repoPath),
|
|
1388
|
+
this.dirtyCount(repoPath),
|
|
1389
|
+
])
|
|
1390
|
+
const manifest = await this.readManifest(repoPath)
|
|
1391
|
+
const worktrees: WorktreeView[] = []
|
|
1392
|
+
const mainWorkspaceId = await this.workspaceIdFor(repoPath)
|
|
1393
|
+
for (const wt of parsed) {
|
|
1394
|
+
if (wt.path === repoPath) continue
|
|
1395
|
+
// Only include worktrees that live under `<repo>/.dsh/workspaces/`.
|
|
1396
|
+
if (!wt.path.startsWith(worktreeRoot(repoPath) + '/') && !wt.path.startsWith(worktreeRoot(repoPath) + '\\')) continue
|
|
1397
|
+
const entry = manifest.worktrees.find((e) => e.path === wt.path || e.name === (wt.branch ?? deriveNameFromPath(wt.path)))
|
|
1398
|
+
const workspaceId = await this.workspaceIdFor(wt.path)
|
|
1399
|
+
worktrees.push({
|
|
1400
|
+
name: entry?.name ?? deriveNameFromPath(wt.path),
|
|
1401
|
+
path: wt.path,
|
|
1402
|
+
branch: wt.branch,
|
|
1403
|
+
head: await this.headShort(wt.path),
|
|
1404
|
+
exists: existsSync(wt.path),
|
|
1405
|
+
workspaceId,
|
|
1406
|
+
// The manifest is the only binding a session-bound lane has; it is what
|
|
1407
|
+
// lets the panel open the conversation instead of a (nonexistent) workspace.
|
|
1408
|
+
sessionId: (entry?.sessionId as SessionId | undefined) ?? null,
|
|
1409
|
+
})
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
repoPath,
|
|
1413
|
+
mainPath: repoPath,
|
|
1414
|
+
mainWorkspaceId,
|
|
1415
|
+
name: basename(repoPath),
|
|
1416
|
+
mainBranch,
|
|
1417
|
+
dirty: mainDirty,
|
|
1418
|
+
worktrees,
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/** Read the manifest from disk; return an empty manifest when missing. */
|
|
1423
|
+
private async readManifest(repoPath: string): Promise<ParsedWorktreeManifest> { const file = manifestPath(repoPath)
|
|
1424
|
+
try {
|
|
1425
|
+
const text = await readFile(file, 'utf8')
|
|
1426
|
+
return parseManifest(text)
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return emptyManifest(repoPath)
|
|
1429
|
+
throw new WorktreeError('manifest-corrupt', defaultMessage('manifest-corrupt'), { file, cause: String(error) })
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
private async writeManifest(repoPath: string, manifest: ParsedWorktreeManifest): Promise<void> {
|
|
1434
|
+
const file = manifestPath(repoPath)
|
|
1435
|
+
await mkdir(dirname(file), { recursive: true })
|
|
1436
|
+
await writeFileAtomic(file, JSON.stringify(manifest, null, 2) + '\n', { mode: 0o644 })
|
|
1437
|
+
// Every plugin mutation lands here, so the status cache can never serve a
|
|
1438
|
+
// pre-mutation answer for a lane the user just created, bound or removed.
|
|
1439
|
+
this.invalidateStatus(repoPath)
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private async appendManifestEntry(repoPath: string, entry: WorktreeManifestEntry): Promise<void> {
|
|
1443
|
+
const manifest = await this.readManifest(repoPath)
|
|
1444
|
+
if (manifest.worktrees.some((e) => e.name === entry.name)) {
|
|
1445
|
+
throw new WorktreeError('worktree-exists', defaultMessage('worktree-exists'), { name: entry.name })
|
|
1446
|
+
}
|
|
1447
|
+
manifest.worktrees.push(entry)
|
|
1448
|
+
await this.writeManifest(repoPath, manifest)
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private async removeManifestEntry(repoPath: string, name: string): Promise<void> {
|
|
1452
|
+
const manifest = await this.readManifest(repoPath)
|
|
1453
|
+
manifest.worktrees = manifest.worktrees.filter((e) => e.name !== name)
|
|
1454
|
+
await this.writeManifest(repoPath, manifest)
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
private async ensureWorkspaceDir(repoPath: string): Promise<void> {
|
|
1458
|
+
await mkdir(worktreeRoot(repoPath), { recursive: true })
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
private async headBranch(cwd: string): Promise<string | null> {
|
|
1462
|
+
const r = await this.runner.run(['symbolic-ref', '-q', '--short', 'HEAD'], cwd)
|
|
1463
|
+
if (r.exitCode !== 0) return null
|
|
1464
|
+
const trimmed = r.stdout.trim()
|
|
1465
|
+
return trimmed === '' ? null : trimmed
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
private async headShort(cwd: string): Promise<string | null> {
|
|
1469
|
+
const r = await this.runner.run(['rev-parse', '--short', 'HEAD'], cwd)
|
|
1470
|
+
if (r.exitCode !== 0) return null
|
|
1471
|
+
const trimmed = r.stdout.trim()
|
|
1472
|
+
return trimmed === '' ? null : trimmed
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
private async dirtyCount(cwd: string): Promise<number | null> {
|
|
1476
|
+
if (!existsSync(cwd)) return null
|
|
1477
|
+
const r = await this.runner.run(['status', '--porcelain'], cwd)
|
|
1478
|
+
if (r.exitCode !== 0) return null
|
|
1479
|
+
return r.stdout.split('\n').filter((line) => line.trim() !== '').length
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// ─── module-level helpers ────────────────────────────────────────────────────
|
|
1484
|
+
|
|
1485
|
+
interface ParsedWorktree {
|
|
1486
|
+
path: string
|
|
1487
|
+
branch: string | null
|
|
1488
|
+
head: string | null
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/** Parse the `git worktree list --porcelain` output. */
|
|
1492
|
+
function parsePorcelainWorktrees(stdout: string): ParsedWorktree[] {
|
|
1493
|
+
const out: ParsedWorktree[] = []
|
|
1494
|
+
let current: ParsedWorktree | null = null
|
|
1495
|
+
for (const raw of stdout.split('\n')) {
|
|
1496
|
+
const line = raw.replace(/\r$/, '')
|
|
1497
|
+
if (line.startsWith('worktree ')) {
|
|
1498
|
+
if (current !== null) out.push(current)
|
|
1499
|
+
current = { path: line.slice('worktree '.length).trim(), branch: null, head: null }
|
|
1500
|
+
} else if (current !== null && line.startsWith('HEAD ')) {
|
|
1501
|
+
current.head = line.slice('HEAD '.length).trim() || null
|
|
1502
|
+
} else if (current !== null && line.startsWith('branch ')) {
|
|
1503
|
+
const ref = line.slice('branch '.length).trim()
|
|
1504
|
+
current.branch = ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref
|
|
1505
|
+
} else if (current !== null && line === 'detached') {
|
|
1506
|
+
current.branch = null
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (current !== null) out.push(current)
|
|
1510
|
+
return out
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function basename(path: string): string {
|
|
1514
|
+
const segments = path.replace(/[\\/]+$/, '').split(/[\\/]/)
|
|
1515
|
+
return segments[segments.length - 1] ?? path
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function deriveNameFromPath(path: string): string {
|
|
1519
|
+
return basename(path)
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Look a service up the way cordis exposes it (`ctx.get(name, false)`) and fall
|
|
1524
|
+
* back to the property alias. Both shapes exist across DSH versions, and a
|
|
1525
|
+
* silent miss here means the header chip and the lane tool report "no lane"
|
|
1526
|
+
* while the panel shows the lane from the manifest.
|
|
1527
|
+
*/
|
|
1528
|
+
function optionalService<T>(ctx: Context, name: string): T | undefined {
|
|
1529
|
+
const get = (ctx as { get?: (id: string, strict?: boolean) => unknown }).get
|
|
1530
|
+
if (typeof get === 'function') {
|
|
1531
|
+
try {
|
|
1532
|
+
const found = get.call(ctx, name, false) as T | undefined
|
|
1533
|
+
if (found !== undefined && found !== null) return found
|
|
1534
|
+
} catch {
|
|
1535
|
+
// fall through to the property alias
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return (ctx as unknown as Record<string, T | undefined>)[name]
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/** The real path when it exists; the trimmed path otherwise (never ''). */
|
|
1542
|
+
function canonicalPath(path: string): string | null {
|
|
1543
|
+
if (path === '') return null
|
|
1544
|
+
try {
|
|
1545
|
+
return realpathSync.native(path)
|
|
1546
|
+
} catch {
|
|
1547
|
+
const trimmed = path.replace(/[\\/]+$/, '')
|
|
1548
|
+
return trimmed === '' ? null : trimmed
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Re-export for callers/tests.
|
|
1553
|
+
export { WORKSPACE_DIR, worktreeDir, worktreeRoot }
|