mixdog 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +2 -2
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/release-paths.mjs +39 -2
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/references/actions.md +5 -2
- package/src/defaults/skills/visualize/SKILL.md +157 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/headless-role.mjs +2 -2
- package/src/help.mjs +2 -0
- package/src/lib/rules-builder.cjs +0 -23
- package/src/rules/lead/LEAD.md +2 -2
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -1
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +11 -8
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
- package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +0 -10
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/channels/index.mjs +1 -5
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
- package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
- package/src/runtime/channels/lib/worker-main.mjs +4 -34
- package/src/runtime/shared/atomic-file.mjs +8 -2
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -118
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -2
- package/src/runtime/shared/provider-api-key.mjs +21 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/semver.d.mts +1 -0
- package/src/runtime/shared/semver.mjs +57 -0
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-surface.mjs +9 -2
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
- package/src/runtime/shared/update-checker.mjs +2 -31
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/boot/apis.mjs +35 -0
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +20 -0
- package/src/session-runtime/boot/infrastructure.mjs +1 -0
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +9 -4
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +11 -6
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/notification-bus.mjs +9 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/resource-extension-sync.mjs +30 -7
- package/src/session-runtime/resource-mcp-api.mjs +27 -10
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-bootstrap.mjs +0 -4
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-paths.mjs +0 -1
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-profile-api.mjs +27 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +11 -4
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
- package/src/session-runtime/starter-guide-state.mjs +62 -0
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/standalone/channel-binding.mjs +1 -2
- package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-channels-loader.mjs +2 -3
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +4 -1
- package/src/standalone/daemon.mjs +139 -5
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +13 -78
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +17 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +35 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/dist/index.mjs +332 -36
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +12 -8
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +18 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +2 -0
- package/src/tui/session/session-api/shared.mjs +10 -6
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +15 -1
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +39 -10
- package/src/runtime/channels/tool-defs.mjs +0 -17
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contracts for Lead-session git worktrees.
|
|
3
|
+
*
|
|
4
|
+
* Pure shapes, validators and constants every worktree scope builds on: the
|
|
5
|
+
* session workspace record (kept separate from project identity), the managed
|
|
6
|
+
* worktree registry entry, the owner record, the branch occupancy DTO, stable
|
|
7
|
+
* error codes, the managed path layout and the per-root cleanup registry.
|
|
8
|
+
* Nothing here touches git or the filesystem.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import { isPlainObject } from '../object.mjs';
|
|
13
|
+
import { resolvePluginData } from '../plugin-paths.mjs';
|
|
14
|
+
|
|
15
|
+
/** Versioned directory under the data dir that holds every managed worktree. */
|
|
16
|
+
export const WORKTREE_STORE_DIR = 'worktrees-v1';
|
|
17
|
+
/** Default ref prefix for branches created for managed worktrees. */
|
|
18
|
+
export const DEFAULT_WORKTREE_BRANCH_PREFIX = 'mixdog/';
|
|
19
|
+
|
|
20
|
+
export const WORKSPACE_TRANSITION_STATES = Object.freeze(['idle', 'provisioning', 'switching', 'removing', 'recovery']);
|
|
21
|
+
const WORKTREE_ENTRY_STATES = Object.freeze(['creating', 'ready', 'deleting', 'recovery']);
|
|
22
|
+
const BRANCH_OCCUPANCY_KINDS = Object.freeze(['current', 'session', 'external', 'free']);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Stable error codes. Clients branch on `error.code`, never on message text.
|
|
26
|
+
* @readonly
|
|
27
|
+
*/
|
|
28
|
+
export const WORKTREE_ERROR_CODES = Object.freeze({
|
|
29
|
+
WORKTREE_BUSY_JOBS: 'WORKTREE_BUSY_JOBS',
|
|
30
|
+
WORKTREE_BRANCH_OCCUPIED: 'WORKTREE_BRANCH_OCCUPIED',
|
|
31
|
+
WORKTREE_DISK_INSUFFICIENT: 'WORKTREE_DISK_INSUFFICIENT',
|
|
32
|
+
WORKTREE_LARGE_REPO_CONFIRM: 'WORKTREE_LARGE_REPO_CONFIRM',
|
|
33
|
+
WORKTREE_DIRTY: 'WORKTREE_DIRTY',
|
|
34
|
+
WORKTREE_UNMERGED: 'WORKTREE_UNMERGED',
|
|
35
|
+
WORKTREE_MISSING: 'WORKTREE_MISSING',
|
|
36
|
+
WORKTREE_UNSUPPORTED_REPO: 'WORKTREE_UNSUPPORTED_REPO',
|
|
37
|
+
WORKTREE_REVISION_CONFLICT: 'WORKTREE_REVISION_CONFLICT',
|
|
38
|
+
WORKTREE_NOT_IMPLEMENTED: 'WORKTREE_NOT_IMPLEMENTED',
|
|
39
|
+
WORKTREE_CONFIRM_REQUIRED: 'WORKTREE_CONFIRM_REQUIRED',
|
|
40
|
+
WORKTREE_TARGET_MISMATCH: 'WORKTREE_TARGET_MISMATCH',
|
|
41
|
+
WORKTREE_PROJECT_MISMATCH: 'WORKTREE_PROJECT_MISMATCH',
|
|
42
|
+
WORKTREE_PARTIAL_TRANSITION: 'WORKTREE_PARTIAL_TRANSITION',
|
|
43
|
+
WORKTREE_PROJECT_UNKNOWN: 'WORKTREE_PROJECT_UNKNOWN',
|
|
44
|
+
WORKTREE_REMOVAL_INCOMPLETE: 'WORKTREE_REMOVAL_INCOMPLETE',
|
|
45
|
+
WORKTREE_PATH_UNSAFE: 'WORKTREE_PATH_UNSAFE',
|
|
46
|
+
MERGE_CONFLICT: 'MERGE_CONFLICT',
|
|
47
|
+
MERGE_TARGET_DIRTY: 'MERGE_TARGET_DIRTY',
|
|
48
|
+
MERGE_FAILED: 'MERGE_FAILED',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** @typedef {keyof typeof WORKTREE_ERROR_CODES} WorktreeErrorCode */
|
|
52
|
+
|
|
53
|
+
const DEFAULT_ERROR_MESSAGES = Object.freeze({
|
|
54
|
+
WORKTREE_BUSY_JOBS: 'The session has running jobs; wait for them or stop them before changing its workspace.',
|
|
55
|
+
WORKTREE_BRANCH_OCCUPIED: 'The branch is checked out in another worktree.',
|
|
56
|
+
WORKTREE_DISK_INSUFFICIENT: 'Not enough free disk space to create the worktree.',
|
|
57
|
+
WORKTREE_LARGE_REPO_CONFIRM: 'The repository is large; confirm before creating a worktree.',
|
|
58
|
+
WORKTREE_DIRTY: 'The worktree has uncommitted changes.',
|
|
59
|
+
WORKTREE_UNMERGED: 'The worktree branch has commits that are not merged.',
|
|
60
|
+
WORKTREE_MISSING: 'The managed worktree no longer exists.',
|
|
61
|
+
WORKTREE_UNSUPPORTED_REPO: 'This repository does not support managed worktrees.',
|
|
62
|
+
WORKTREE_REVISION_CONFLICT: 'The session workspace changed; reload and try again.',
|
|
63
|
+
WORKTREE_NOT_IMPLEMENTED: 'Worktree support is not implemented yet.',
|
|
64
|
+
WORKTREE_CONFIRM_REQUIRED: 'A merge requires explicit confirmation.',
|
|
65
|
+
WORKTREE_TARGET_MISMATCH: 'The project folder is not on the branch the merge was confirmed for.',
|
|
66
|
+
WORKTREE_PROJECT_MISMATCH: 'The retained worktree belongs to another project or repository.',
|
|
67
|
+
WORKTREE_PARTIAL_TRANSITION: 'The branch change failed part-way; the project folder stayed on the new branch.',
|
|
68
|
+
WORKTREE_PROJECT_UNKNOWN: 'Project is not available.',
|
|
69
|
+
WORKTREE_REMOVAL_INCOMPLETE:
|
|
70
|
+
'The worktree was only partly removed (a file or folder is still in use); remove it again to finish.',
|
|
71
|
+
WORKTREE_PATH_UNSAFE: 'The worktree folder is not a managed checkout of this repository; nothing was deleted.',
|
|
72
|
+
MERGE_CONFLICT: 'The merge would conflict.',
|
|
73
|
+
MERGE_TARGET_DIRTY: 'The merge target has uncommitted changes.',
|
|
74
|
+
MERGE_FAILED: 'The merge failed; the project folder is unchanged.',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** Typed worktree failure; `code` is one of WORKTREE_ERROR_CODES. */
|
|
78
|
+
export class WorktreeError extends Error {
|
|
79
|
+
/**
|
|
80
|
+
* @param {WorktreeErrorCode} code
|
|
81
|
+
* @param {string} [message]
|
|
82
|
+
* @param {Record<string, unknown>} [details] JSON-safe context (paths, branch, counts).
|
|
83
|
+
*/
|
|
84
|
+
constructor(code, message, details) {
|
|
85
|
+
super(message || DEFAULT_ERROR_MESSAGES[code] || code);
|
|
86
|
+
this.name = 'WorktreeError';
|
|
87
|
+
this.code = code;
|
|
88
|
+
if (details !== undefined) this.details = details;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build a typed worktree error. Unknown codes are a programming error.
|
|
94
|
+
* @param {WorktreeErrorCode} code
|
|
95
|
+
* @param {string} [message]
|
|
96
|
+
* @param {Record<string, unknown>} [details]
|
|
97
|
+
* @returns {WorktreeError}
|
|
98
|
+
*/
|
|
99
|
+
export function worktreeError(code, message, details) {
|
|
100
|
+
if (!Object.hasOwn(WORKTREE_ERROR_CODES, code)) throw new TypeError(`Unknown worktree error code "${code}".`);
|
|
101
|
+
return new WorktreeError(code, message, details);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {unknown} error
|
|
106
|
+
* @param {WorktreeErrorCode} [code] When given, also require this code.
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function isWorktreeError(error, code) {
|
|
110
|
+
const actual = /** @type {{code?: unknown}} */ (error)?.code;
|
|
111
|
+
if (typeof actual !== 'string' || !Object.hasOwn(WORKTREE_ERROR_CODES, actual)) return false;
|
|
112
|
+
return code === undefined || actual === code;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Field validators
|
|
117
|
+
|
|
118
|
+
function text(value, name) {
|
|
119
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${name} must be a non-empty string.`);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function nullableText(value, name) {
|
|
124
|
+
return value === undefined || value === null ? null : text(value, name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function oneOf(value, allowed, name) {
|
|
128
|
+
if (!allowed.includes(value)) throw new TypeError(`${name} must be one of ${allowed.join(', ')}.`);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const COMMIT_PATTERN = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
133
|
+
|
|
134
|
+
function nullableCommit(value, name) {
|
|
135
|
+
if (value === undefined || value === null) return null;
|
|
136
|
+
if (typeof value !== 'string' || !COMMIT_PATTERN.test(value)) {
|
|
137
|
+
throw new TypeError(`${name} must be a full lowercase commit id.`);
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function record(value, name) {
|
|
143
|
+
if (!isPlainObject(value)) throw new TypeError(`${name} must be an object.`);
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Session workspace
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @typedef {'idle'|'provisioning'|'switching'|'removing'|'recovery'} WorkspaceTransitionState
|
|
152
|
+
* @typedef {{ state: WorkspaceTransitionState, txId: string|null }} WorkspaceTransition
|
|
153
|
+
* `txId` is null exactly when `state` is idle.
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Where a Lead session executes. Project identity (`projectPath`) stays the
|
|
158
|
+
* registered Project even while `cwd` points into a managed worktree.
|
|
159
|
+
* @typedef {object} SessionWorkspace
|
|
160
|
+
* @property {string} projectPath Registered Project root.
|
|
161
|
+
* @property {string|null} repoCommonDir Canonical git common dir; null outside a git repository.
|
|
162
|
+
* @property {string} cwd Execution root for the Lead and its agents.
|
|
163
|
+
* @property {string|null} worktreeId Managed worktree id; null when running in the project folder.
|
|
164
|
+
* @property {string|null} branch Checked-out branch at `cwd`; null when detached or not a repository.
|
|
165
|
+
* @property {string|null} baseCommit Commit the worktree branch was created from.
|
|
166
|
+
* @property {string|null} mergeTarget Branch the worktree merges into (the project folder's branch).
|
|
167
|
+
* @property {number} revision Monotonic; advances on every committed workspace change.
|
|
168
|
+
* @property {WorkspaceTransition} transition In-flight transition marker.
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validate and normalize a session workspace record. Throws TypeError on any
|
|
173
|
+
* invalid field or broken invariant; returns a fresh plain object.
|
|
174
|
+
* @param {unknown} input
|
|
175
|
+
* @returns {SessionWorkspace}
|
|
176
|
+
*/
|
|
177
|
+
export function normalizeSessionWorkspace(input) {
|
|
178
|
+
const value = record(input, 'workspace');
|
|
179
|
+
const transitionInput = value.transition === undefined ? { state: 'idle', txId: null } : value.transition;
|
|
180
|
+
const transitionRecord = record(transitionInput, 'workspace.transition');
|
|
181
|
+
const state = oneOf(transitionRecord.state, WORKSPACE_TRANSITION_STATES, 'workspace.transition.state');
|
|
182
|
+
const txId = nullableText(transitionRecord.txId, 'workspace.transition.txId');
|
|
183
|
+
if ((state === 'idle') !== (txId === null)) {
|
|
184
|
+
throw new TypeError('workspace.transition.txId must be null exactly when the state is idle.');
|
|
185
|
+
}
|
|
186
|
+
const revision = value.revision === undefined ? 0 : value.revision;
|
|
187
|
+
if (!Number.isSafeInteger(revision) || revision < 0) {
|
|
188
|
+
throw new TypeError('workspace.revision must be a non-negative integer.');
|
|
189
|
+
}
|
|
190
|
+
const workspace = {
|
|
191
|
+
projectPath: text(value.projectPath, 'workspace.projectPath'),
|
|
192
|
+
repoCommonDir: nullableText(value.repoCommonDir, 'workspace.repoCommonDir'),
|
|
193
|
+
cwd: text(value.cwd, 'workspace.cwd'),
|
|
194
|
+
worktreeId: nullableText(value.worktreeId, 'workspace.worktreeId'),
|
|
195
|
+
branch: nullableText(value.branch, 'workspace.branch'),
|
|
196
|
+
baseCommit: nullableCommit(value.baseCommit, 'workspace.baseCommit'),
|
|
197
|
+
mergeTarget: nullableText(value.mergeTarget, 'workspace.mergeTarget'),
|
|
198
|
+
revision,
|
|
199
|
+
transition: { state, txId },
|
|
200
|
+
};
|
|
201
|
+
if (workspace.worktreeId !== null && (workspace.repoCommonDir === null || workspace.branch === null)) {
|
|
202
|
+
throw new TypeError('A worktree-bound workspace requires repoCommonDir and branch.');
|
|
203
|
+
}
|
|
204
|
+
return workspace;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The project-folder workspace a new Lead session starts in.
|
|
209
|
+
* @param {{ projectPath: string, cwd?: string, repoCommonDir?: string|null, branch?: string|null }} input
|
|
210
|
+
* @returns {SessionWorkspace}
|
|
211
|
+
*/
|
|
212
|
+
export function projectWorkspace({ projectPath, cwd = projectPath, repoCommonDir = null, branch = null }) {
|
|
213
|
+
return normalizeSessionWorkspace({ projectPath, cwd, repoCommonDir, branch, revision: 0 });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** @param {SessionWorkspace|null|undefined} workspace */
|
|
217
|
+
export function isWorktreeBound(workspace) {
|
|
218
|
+
return typeof workspace?.worktreeId === 'string' && workspace.worktreeId.length > 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** @param {SessionWorkspace} workspace @returns {number} The revision a committed change must carry. */
|
|
222
|
+
export function nextRevision(workspace) {
|
|
223
|
+
const revision = workspace?.revision;
|
|
224
|
+
if (!Number.isSafeInteger(revision) || revision < 0) throw new TypeError('workspace.revision is invalid.');
|
|
225
|
+
return revision + 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Managed worktree registry entry and owner
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @typedef {'creating'|'ready'|'deleting'|'recovery'} WorktreeEntryState
|
|
233
|
+
* @typedef {object} WorktreeEntry
|
|
234
|
+
* @property {string} id Slug, unique per repository.
|
|
235
|
+
* @property {string} repoCommonDir
|
|
236
|
+
* @property {string} projectPath
|
|
237
|
+
* @property {string} path Absolute worktree folder (see managedWorktreePath).
|
|
238
|
+
* @property {string} branch Full branch name, e.g. "mixdog/fix-login".
|
|
239
|
+
* @property {string|null} baseCommit
|
|
240
|
+
* @property {number} createdAt Epoch milliseconds.
|
|
241
|
+
* @property {string|null} ownerSessionId Lead session that retains it; null once released.
|
|
242
|
+
* @property {WorktreeEntryState} state
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {unknown} input
|
|
247
|
+
* @returns {WorktreeEntry}
|
|
248
|
+
*/
|
|
249
|
+
export function normalizeWorktreeEntry(input) {
|
|
250
|
+
const value = record(input, 'worktree');
|
|
251
|
+
if (!Number.isSafeInteger(value.createdAt) || value.createdAt < 0) {
|
|
252
|
+
throw new TypeError('worktree.createdAt must be epoch milliseconds.');
|
|
253
|
+
}
|
|
254
|
+
const id = text(value.id, 'worktree.id');
|
|
255
|
+
const idError = worktreeNameError(id);
|
|
256
|
+
if (idError) throw new TypeError(`worktree.id is invalid: ${idError}`);
|
|
257
|
+
return {
|
|
258
|
+
id,
|
|
259
|
+
repoCommonDir: text(value.repoCommonDir, 'worktree.repoCommonDir'),
|
|
260
|
+
projectPath: text(value.projectPath, 'worktree.projectPath'),
|
|
261
|
+
path: text(value.path, 'worktree.path'),
|
|
262
|
+
branch: text(value.branch, 'worktree.branch'),
|
|
263
|
+
baseCommit: nullableCommit(value.baseCommit, 'worktree.baseCommit'),
|
|
264
|
+
createdAt: value.createdAt,
|
|
265
|
+
ownerSessionId: nullableText(value.ownerSessionId, 'worktree.ownerSessionId'),
|
|
266
|
+
state: oneOf(value.state, WORKTREE_ENTRY_STATES, 'worktree.state'),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Session-scoped ownership: a restarted runtime of the same session keeps the
|
|
272
|
+
* worktree; `runtimeToken` tells a live owner from a stale one and `pid` is a
|
|
273
|
+
* liveness hint only.
|
|
274
|
+
* @typedef {{ sessionId: string, runtimeToken: string, pid: number|null }} WorktreeOwner
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {unknown} input
|
|
279
|
+
* @returns {WorktreeOwner}
|
|
280
|
+
*/
|
|
281
|
+
export function normalizeWorktreeOwner(input) {
|
|
282
|
+
const value = record(input, 'owner');
|
|
283
|
+
const pid = value.pid === undefined || value.pid === null ? null : value.pid;
|
|
284
|
+
if (pid !== null && (!Number.isSafeInteger(pid) || pid <= 0)) {
|
|
285
|
+
throw new TypeError('owner.pid must be a positive integer or null.');
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
sessionId: text(value.sessionId, 'owner.sessionId'),
|
|
289
|
+
runtimeToken: text(value.runtimeToken, 'owner.runtimeToken'),
|
|
290
|
+
pid,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Branch occupancy
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Who holds a branch. `current`: checked out in the project folder; `session`:
|
|
299
|
+
* a managed worktree retained by `sessionId`; `external`: another worktree not
|
|
300
|
+
* managed by Mixdog; `free`: not checked out anywhere.
|
|
301
|
+
* @typedef {'current'|'session'|'external'|'free'} BranchOccupancyKind
|
|
302
|
+
* @typedef {{ branch: string, kind: BranchOccupancyKind, sessionId?: string, path?: string }} BranchOccupancy
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {unknown} input
|
|
307
|
+
* @returns {BranchOccupancy}
|
|
308
|
+
*/
|
|
309
|
+
export function normalizeBranchOccupancy(input) {
|
|
310
|
+
const value = record(input, 'occupancy');
|
|
311
|
+
const branch = text(value.branch, 'occupancy.branch');
|
|
312
|
+
const kind = oneOf(value.kind, BRANCH_OCCUPANCY_KINDS, 'occupancy.kind');
|
|
313
|
+
if (kind === 'free') {
|
|
314
|
+
if (value.sessionId != null || value.path != null) {
|
|
315
|
+
throw new TypeError('A free branch carries no sessionId or path.');
|
|
316
|
+
}
|
|
317
|
+
return { branch, kind };
|
|
318
|
+
}
|
|
319
|
+
if (kind !== 'session' && value.sessionId != null) {
|
|
320
|
+
throw new TypeError('Only a session occupancy carries a sessionId.');
|
|
321
|
+
}
|
|
322
|
+
const occupancy = { branch, kind };
|
|
323
|
+
if (kind === 'session') occupancy.sessionId = text(value.sessionId, 'occupancy.sessionId');
|
|
324
|
+
if (value.path != null) occupancy.path = text(value.path, 'occupancy.path');
|
|
325
|
+
return occupancy;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The branch menu's rows (the desktop DesktopGitBranch shape) for the local
|
|
330
|
+
* branch names the manager lists: `current` marks the branch checked out in
|
|
331
|
+
* the project folder; managed lists hold local branches only.
|
|
332
|
+
* @param {string[]} names
|
|
333
|
+
* @param {BranchOccupancy[]} occupancy
|
|
334
|
+
* @returns {Array<{ name: string, current: boolean, remote: false, upstream: string }>}
|
|
335
|
+
*/
|
|
336
|
+
export function branchMenuEntries(names, occupancy) {
|
|
337
|
+
const current = new Set(occupancy.filter((row) => row.kind === 'current').map((row) => row.branch));
|
|
338
|
+
return names.map((name) => ({ name, current: current.has(name), remote: false, upstream: '' }));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Names and managed paths
|
|
343
|
+
|
|
344
|
+
const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
345
|
+
const WORKTREE_NAME_MAX_LENGTH = 64;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Why `name` cannot be a worktree slug (folder name and branch suffix), or
|
|
349
|
+
* null when it can.
|
|
350
|
+
* @param {unknown} name
|
|
351
|
+
* @returns {string|null}
|
|
352
|
+
*/
|
|
353
|
+
export function worktreeNameError(name) {
|
|
354
|
+
if (typeof name !== 'string' || !name) return 'a name is required';
|
|
355
|
+
if (name.length > WORKTREE_NAME_MAX_LENGTH) return `at most ${WORKTREE_NAME_MAX_LENGTH} characters`;
|
|
356
|
+
if (!NAME_PATTERN.test(name)) return 'use letters, digits, ".", "_" or "-", starting with a letter or digit';
|
|
357
|
+
if (name.includes('..') || name.endsWith('.') || name.toLowerCase().endsWith('.lock')) {
|
|
358
|
+
return 'not a valid git branch component';
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Stable short hash of a canonical git common dir. Case-insensitive on
|
|
365
|
+
* Windows, where the same repository may be spelled with different casing.
|
|
366
|
+
* @param {string} repoCommonDir
|
|
367
|
+
* @param {NodeJS.Platform} [platform]
|
|
368
|
+
* @returns {string} 16 lowercase hex characters.
|
|
369
|
+
*/
|
|
370
|
+
export function repoHash(repoCommonDir, platform = process.platform) {
|
|
371
|
+
let key = resolve(text(repoCommonDir, 'repoCommonDir')).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
372
|
+
if (platform === 'win32') key = key.toLowerCase();
|
|
373
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* <dataDir>/worktrees-v1/<repoHash> — the root that holds one repository's
|
|
378
|
+
* managed worktrees and the unit runRootCleanups operates on.
|
|
379
|
+
* @param {string} repoCommonDir
|
|
380
|
+
* @param {{ dataDir?: string, platform?: NodeJS.Platform }} [options]
|
|
381
|
+
*/
|
|
382
|
+
export function managedWorktreeRoot(repoCommonDir, { dataDir = resolvePluginData(), platform } = {}) {
|
|
383
|
+
return join(dataDir, WORKTREE_STORE_DIR, repoHash(repoCommonDir, platform));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* <dataDir>/worktrees-v1/<repoHash>/<slug>
|
|
388
|
+
* @param {string} repoCommonDir
|
|
389
|
+
* @param {string} slug
|
|
390
|
+
* @param {{ dataDir?: string, platform?: NodeJS.Platform }} [options]
|
|
391
|
+
*/
|
|
392
|
+
export function managedWorktreePath(repoCommonDir, slug, options) {
|
|
393
|
+
const error = worktreeNameError(slug);
|
|
394
|
+
if (error) throw new TypeError(`worktree name is invalid: ${error}`);
|
|
395
|
+
return join(managedWorktreeRoot(repoCommonDir, options), slug);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @param {string} slug
|
|
400
|
+
* @param {string} [prefix]
|
|
401
|
+
* @returns {string} e.g. "mixdog/fix-login"
|
|
402
|
+
*/
|
|
403
|
+
export function worktreeBranchName(slug, prefix = DEFAULT_WORKTREE_BRANCH_PREFIX) {
|
|
404
|
+
const error = worktreeNameError(slug);
|
|
405
|
+
if (error) throw new TypeError(`worktree name is invalid: ${error}`);
|
|
406
|
+
return `${prefix}${slug}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// Root cleanup registry
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Cleanup a module performs for one managed root (or a worktree path inside
|
|
414
|
+
* it) when that root is removed: caches, indexes, snapshots keyed by path.
|
|
415
|
+
* @callback RootCleanup
|
|
416
|
+
* @param {string} root Absolute path being removed.
|
|
417
|
+
* @param {{ signal?: AbortSignal, reason?: string } & Record<string, unknown>} ctx
|
|
418
|
+
* @returns {Promise<unknown>|unknown} JSON-safe result for diagnostics.
|
|
419
|
+
*/
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* @typedef {{ name: string, ok: true, result: unknown } | { name: string, ok: false, error: { code: string|null, message: string } }} RootCleanupResult
|
|
423
|
+
*/
|
|
424
|
+
|
|
425
|
+
/** A fresh, independent registry (tests and isolated hosts). */
|
|
426
|
+
export function createRootCleanupRegistry() {
|
|
427
|
+
/** @type {Map<string, RootCleanup>} */
|
|
428
|
+
const cleanups = new Map();
|
|
429
|
+
return {
|
|
430
|
+
/**
|
|
431
|
+
* @param {string} name Unique module name.
|
|
432
|
+
* @param {RootCleanup} cleanup
|
|
433
|
+
* @returns {() => void} Unregister.
|
|
434
|
+
*/
|
|
435
|
+
register(name, cleanup) {
|
|
436
|
+
text(name, 'cleanup name');
|
|
437
|
+
if (typeof cleanup !== 'function') throw new TypeError('cleanup must be a function.');
|
|
438
|
+
if (cleanups.has(name)) throw new TypeError(`Root cleanup "${name}" is already registered.`);
|
|
439
|
+
cleanups.set(name, cleanup);
|
|
440
|
+
return () => {
|
|
441
|
+
if (cleanups.get(name) === cleanup) cleanups.delete(name);
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
/**
|
|
445
|
+
* Run every registered cleanup in registration order. One failure never
|
|
446
|
+
* stops the rest; each module reports its own result.
|
|
447
|
+
* @param {string} root
|
|
448
|
+
* @param {Record<string, unknown>} [ctx]
|
|
449
|
+
* @returns {Promise<RootCleanupResult[]>}
|
|
450
|
+
*/
|
|
451
|
+
async run(root, ctx = {}) {
|
|
452
|
+
text(root, 'root');
|
|
453
|
+
const results = [];
|
|
454
|
+
for (const [name, cleanup] of [...cleanups]) {
|
|
455
|
+
try {
|
|
456
|
+
results.push({ name, ok: true, result: (await cleanup(root, ctx)) ?? null });
|
|
457
|
+
} catch (error) {
|
|
458
|
+
results.push({
|
|
459
|
+
name,
|
|
460
|
+
ok: false,
|
|
461
|
+
error: {
|
|
462
|
+
code: typeof error?.code === 'string' ? error.code : null,
|
|
463
|
+
message: String(error?.message || error),
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return results;
|
|
469
|
+
},
|
|
470
|
+
/** @returns {string[]} */
|
|
471
|
+
names() {
|
|
472
|
+
return [...cleanups.keys()];
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const processRootCleanups = createRootCleanupRegistry();
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Register a process-wide cleanup for removed worktree roots.
|
|
481
|
+
* @param {string} name
|
|
482
|
+
* @param {RootCleanup} cleanup
|
|
483
|
+
* @returns {() => void} Unregister.
|
|
484
|
+
*/
|
|
485
|
+
export function registerRootCleanup(name, cleanup) {
|
|
486
|
+
return processRootCleanups.register(name, cleanup);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @param {string} root
|
|
491
|
+
* @param {Record<string, unknown>} [ctx]
|
|
492
|
+
* @returns {Promise<RootCleanupResult[]>}
|
|
493
|
+
*/
|
|
494
|
+
export function runRootCleanups(root, ctx) {
|
|
495
|
+
return processRootCleanups.run(root, ctx);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const processRootReleasers = createRootCleanupRegistry();
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Register a process-wide releaser: it drops live handles that keep a root
|
|
502
|
+
* open (desktop terminals started there). Releasers run before the root
|
|
503
|
+
* cleanups and before the checkout is removed.
|
|
504
|
+
* @param {string} name
|
|
505
|
+
* @param {RootCleanup} releaser
|
|
506
|
+
* @returns {() => void} Unregister.
|
|
507
|
+
*/
|
|
508
|
+
export function registerRootReleaser(name, releaser) {
|
|
509
|
+
return processRootReleasers.register(name, releaser);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* @param {string} root
|
|
514
|
+
* @param {Record<string, unknown>} [ctx]
|
|
515
|
+
* @returns {Promise<RootCleanupResult[]>}
|
|
516
|
+
*/
|
|
517
|
+
export function runRootReleasers(root, ctx) {
|
|
518
|
+
return processRootReleasers.run(root, ctx);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// Transition API (interface only; implemented by the workspace transition scope)
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Where a transition moves the session.
|
|
526
|
+
* @typedef {{ kind: 'branch', branch: string, create?: boolean }
|
|
527
|
+
* | { kind: 'worktree', name?: string|null }
|
|
528
|
+
* | { kind: 'remove-worktree', force?: boolean }} WorkspaceTransitionTarget
|
|
529
|
+
* `branch`: run in the project folder on `branch` (the worktree stays retained);
|
|
530
|
+
* `create: true` first creates `branch` from the project folder's current branch;
|
|
531
|
+
* `worktree`: run in this Lead's retained worktree, creating it (optionally named) on first use;
|
|
532
|
+
* `remove-worktree`: delete the retained worktree after dirty/unmerged checks.
|
|
533
|
+
*/
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* @typedef {object} WorkspaceTransitionOptions
|
|
537
|
+
* @property {number} [expectedRevision] Reject with WORKTREE_REVISION_CONFLICT when the session moved on.
|
|
538
|
+
* @property {boolean} [confirmLarge] Acknowledges WORKTREE_LARGE_REPO_CONFIRM.
|
|
539
|
+
* @property {AbortSignal} [signal]
|
|
540
|
+
*/
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Serialized, revisioned workspace change for one Lead session. Fails with a
|
|
544
|
+
* WorktreeError (WORKTREE_BUSY_JOBS, WORKTREE_BRANCH_OCCUPIED, ...) and leaves
|
|
545
|
+
* the previous workspace intact on failure.
|
|
546
|
+
* @callback ApplyWorkspaceTransition
|
|
547
|
+
* @param {{ sessionId: string }} sessionRef
|
|
548
|
+
* @param {WorkspaceTransitionTarget} target
|
|
549
|
+
* @param {WorkspaceTransitionOptions} [options]
|
|
550
|
+
* @returns {Promise<{ revision: number, workspace: SessionWorkspace }>}
|
|
551
|
+
*/
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk estimate for a new worktree: tracked checkout bytes (blob sizes of the
|
|
3
|
+
* base commit's tree) + include copies + a safety margin, compared with free
|
|
4
|
+
* space on the DESTINATION volume.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { statfs } from 'node:fs/promises';
|
|
8
|
+
import { dirname, resolve } from 'node:path';
|
|
9
|
+
import { runGit } from './git.mjs';
|
|
10
|
+
|
|
11
|
+
const MARGIN_MIN_BYTES = 64 * 1024 * 1024;
|
|
12
|
+
const MARGIN_RATIO = 0.1;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sum of blob sizes in `commit`'s tree; gitlinks flag submodules.
|
|
16
|
+
* @returns {Promise<{ bytes: number, files: number, submodules: boolean }>}
|
|
17
|
+
*/
|
|
18
|
+
export async function trackedCheckoutBytes(cwd, commit, { signal } = {}) {
|
|
19
|
+
const result = await runGit(cwd, ['ls-tree', '-r', '-l', '-z', '--full-tree', commit], {
|
|
20
|
+
signal,
|
|
21
|
+
timeoutMs: 600_000,
|
|
22
|
+
maxBytes: 1024 * 1024 * 1024,
|
|
23
|
+
});
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
let files = 0;
|
|
26
|
+
let submodules = false;
|
|
27
|
+
for (const record of result.stdout.split('\0')) {
|
|
28
|
+
if (!record) continue;
|
|
29
|
+
const tab = record.indexOf('\t');
|
|
30
|
+
const [, type, , size] = record.slice(0, tab).trim().split(/\s+/);
|
|
31
|
+
if (type === 'commit') {
|
|
32
|
+
submodules = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (type !== 'blob') continue;
|
|
36
|
+
files += 1;
|
|
37
|
+
bytes += Number(size) || 0;
|
|
38
|
+
}
|
|
39
|
+
return { bytes, files, submodules };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bytes needed for `contentBytes` of checkout and copies. */
|
|
43
|
+
export function requiredBytes(contentBytes) {
|
|
44
|
+
return contentBytes + Math.max(MARGIN_MIN_BYTES, Math.ceil(contentBytes * MARGIN_RATIO));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Free bytes available to this user on the volume holding `path`. */
|
|
48
|
+
export async function freeBytesAt(path) {
|
|
49
|
+
let dir = resolve(path);
|
|
50
|
+
while (!existsSync(dir)) {
|
|
51
|
+
const parent = dirname(dir);
|
|
52
|
+
if (parent === dir) break;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
const info = await statfs(dir);
|
|
56
|
+
return Number(info.bavail) * Number(info.bsize);
|
|
57
|
+
}
|