borgmcp 1.1.14 → 2.0.1
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/CONTRIBUTING.md +15 -0
- package/LICENSE +201 -33
- package/NOTICE +2 -0
- package/README.md +86 -37
- package/SECURITY.md +7 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +63 -1
- package/dist/agent-runtime.js.map +1 -0
- package/dist/assimilate-cmd.d.ts +96 -17
- package/dist/assimilate-cmd.d.ts.map +1 -0
- package/dist/assimilate-cmd.js +1396 -48
- package/dist/assimilate-cmd.js.map +1 -0
- package/dist/assimilate-deps.d.ts.map +1 -0
- package/dist/assimilate-deps.js +384 -3
- package/dist/assimilate-deps.js.map +1 -0
- package/dist/assimilate-guard.d.ts +5 -7
- package/dist/assimilate-guard.d.ts.map +1 -0
- package/dist/assimilate-guard.js +60 -2
- package/dist/assimilate-guard.js.map +1 -0
- package/dist/assimilate-welcome.d.ts +1 -1
- package/dist/assimilate-welcome.d.ts.map +1 -0
- package/dist/assimilate-welcome.js +32 -2
- package/dist/assimilate-welcome.js.map +1 -0
- package/dist/auth-env.d.ts +7 -50
- package/dist/auth-env.d.ts.map +1 -0
- package/dist/auth-env.js +22 -1
- package/dist/auth-env.js.map +1 -0
- package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
- package/dist/backends/launch-all-pastelist.js +11 -4
- package/dist/backends/launch-all-pastelist.js.map +1 -0
- package/dist/backends/launch-all-tmux.d.ts.map +1 -0
- package/dist/backends/launch-all-tmux.js +49 -1
- package/dist/backends/launch-all-tmux.js.map +1 -0
- package/dist/backends/launch-all-windows.d.ts.map +1 -0
- package/dist/backends/launch-all-windows.js +86 -12
- package/dist/backends/launch-all-windows.js.map +1 -0
- package/dist/bare-launch-menu.d.ts.map +1 -0
- package/dist/bare-launch-menu.js +83 -6
- package/dist/bare-launch-menu.js.map +1 -0
- package/dist/claude-launch-args.d.ts.map +1 -0
- package/dist/claude-launch-args.js +34 -1
- package/dist/claude-launch-args.js.map +1 -0
- package/dist/claude.d.ts.map +1 -0
- package/dist/claude.js +486 -14
- package/dist/claude.js.map +1 -0
- package/dist/cleanup-cmd.d.ts +5 -5
- package/dist/cleanup-cmd.d.ts.map +1 -0
- package/dist/cleanup-cmd.js +453 -14
- package/dist/cleanup-cmd.js.map +1 -0
- package/dist/clear-rewake-core.d.ts.map +1 -0
- package/dist/clear-rewake-core.js +23 -2
- package/dist/clear-rewake-core.js.map +1 -0
- package/dist/clear-rewake.d.ts.map +1 -0
- package/dist/clear-rewake.js +19 -1
- package/dist/clear-rewake.js.map +1 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +104 -56
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-platform.d.ts.map +1 -0
- package/dist/cli-platform.js +106 -4
- package/dist/cli-platform.js.map +1 -0
- package/dist/cli-tool-approval.d.ts +84 -0
- package/dist/cli-tool-approval.d.ts.map +1 -0
- package/dist/cli-tool-approval.js +583 -0
- package/dist/cli-tool-approval.js.map +1 -0
- package/dist/codex-app-server.d.ts.map +1 -0
- package/dist/codex-app-server.js +228 -4
- package/dist/codex-app-server.js.map +1 -0
- package/dist/codex-app-wake.d.ts.map +1 -0
- package/dist/codex-app-wake.js +432 -2
- package/dist/codex-app-wake.js.map +1 -0
- package/dist/codex-launch.d.ts.map +1 -0
- package/dist/codex-launch.js +102 -2
- package/dist/codex-launch.js.map +1 -0
- package/dist/codex-remote.d.ts +6 -1
- package/dist/codex-remote.d.ts.map +1 -0
- package/dist/codex-remote.js +394 -1
- package/dist/codex-remote.js.map +1 -0
- package/dist/codex-wake-resolve.d.ts.map +1 -0
- package/dist/codex-wake-resolve.js +127 -1
- package/dist/codex-wake-resolve.js.map +1 -0
- package/dist/config-utils.d.ts +2 -2
- package/dist/config-utils.d.ts.map +1 -0
- package/dist/config-utils.js +766 -3
- package/dist/config-utils.js.map +1 -0
- package/dist/config.d.ts +98 -61
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +426 -1
- package/dist/config.js.map +1 -0
- package/dist/console-prefix.d.ts.map +1 -0
- package/dist/console-prefix.js +96 -1
- package/dist/console-prefix.js.map +1 -0
- package/dist/cube-name.d.ts.map +1 -0
- package/dist/cube-name.js +67 -1
- package/dist/cube-name.js.map +1 -0
- package/dist/cubes.d.ts +126 -18
- package/dist/cubes.d.ts.map +1 -0
- package/dist/cubes.js +412 -4
- package/dist/cubes.js.map +1 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +71 -1
- package/dist/debug.js.map +1 -0
- package/dist/direct-log.d.ts.map +1 -0
- package/dist/direct-log.js +11 -1
- package/dist/direct-log.js.map +1 -0
- package/dist/docs-sections.d.ts +11 -10
- package/dist/docs-sections.d.ts.map +1 -0
- package/dist/docs-sections.js +113 -3
- package/dist/docs-sections.js.map +1 -0
- package/dist/drone-lifecycle.d.ts +5 -21
- package/dist/drone-lifecycle.d.ts.map +1 -0
- package/dist/drone-lifecycle.js +57 -8
- package/dist/drone-lifecycle.js.map +1 -0
- package/dist/drone-management.d.ts +29 -0
- package/dist/drone-management.d.ts.map +1 -0
- package/dist/drone-management.js +109 -0
- package/dist/drone-management.js.map +1 -0
- package/dist/ensure-mcp-config.d.ts.map +1 -0
- package/dist/ensure-mcp-config.js +36 -1
- package/dist/ensure-mcp-config.js.map +1 -0
- package/dist/evict-drone.d.ts +10 -2
- package/dist/evict-drone.d.ts.map +1 -0
- package/dist/evict-drone.js +60 -1
- package/dist/evict-drone.js.map +1 -0
- package/dist/gc-orphan-inboxes.d.ts.map +1 -0
- package/dist/gc-orphan-inboxes.js +177 -1
- package/dist/gc-orphan-inboxes.js.map +1 -0
- package/dist/inbox-monitor.d.ts +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -0
- package/dist/inbox-monitor.js +856 -3
- package/dist/inbox-monitor.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1034 -37
- package/dist/index.js.map +1 -0
- package/dist/launch-all-cmd.d.ts.map +1 -0
- package/dist/launch-all-cmd.js +377 -37
- package/dist/launch-all-cmd.js.map +1 -0
- package/dist/launch-all-command.d.ts.map +1 -0
- package/dist/launch-all-command.js +24 -1
- package/dist/launch-all-command.js.map +1 -0
- package/dist/launch-all-deps.d.ts +3 -8
- package/dist/launch-all-deps.d.ts.map +1 -0
- package/dist/launch-all-deps.js +99 -1
- package/dist/launch-all-deps.js.map +1 -0
- package/dist/launch-all-discovery.d.ts +1 -0
- package/dist/launch-all-discovery.d.ts.map +1 -0
- package/dist/launch-all-discovery.js +90 -4
- package/dist/launch-all-discovery.js.map +1 -0
- package/dist/launch-all-locks.d.ts.map +1 -0
- package/dist/launch-all-locks.js +54 -1
- package/dist/launch-all-locks.js.map +1 -0
- package/dist/launch-gate.d.ts +2 -2
- package/dist/launch-gate.d.ts.map +1 -0
- package/dist/launch-gate.js +73 -1
- package/dist/launch-gate.js.map +1 -0
- package/dist/lifecycle-log-guard.d.ts.map +1 -0
- package/dist/lifecycle-log-guard.js +93 -2
- package/dist/lifecycle-log-guard.js.map +1 -0
- package/dist/list-roles-render.d.ts.map +1 -0
- package/dist/list-roles-render.js +40 -6
- package/dist/list-roles-render.js.map +1 -0
- package/dist/local-log-routing.d.ts +19 -0
- package/dist/local-log-routing.d.ts.map +1 -0
- package/dist/local-log-routing.js +67 -0
- package/dist/local-log-routing.js.map +1 -0
- package/dist/local-manage-tool-result.d.ts +8 -0
- package/dist/local-manage-tool-result.d.ts.map +1 -0
- package/dist/local-manage-tool-result.js +10 -0
- package/dist/local-manage-tool-result.js.map +1 -0
- package/dist/local-server-cursor.d.ts +34 -0
- package/dist/local-server-cursor.d.ts.map +1 -0
- package/dist/local-server-cursor.js +151 -0
- package/dist/local-server-cursor.js.map +1 -0
- package/dist/log-audit.d.ts.map +1 -0
- package/dist/log-audit.js +194 -3
- package/dist/log-audit.js.map +1 -0
- package/dist/log-stream.d.ts +31 -18
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +1214 -12
- package/dist/log-stream.js.map +1 -0
- package/dist/model-presets.d.ts.map +1 -0
- package/dist/model-presets.js +19 -1
- package/dist/model-presets.js.map +1 -0
- package/dist/name-validator.d.ts.map +1 -0
- package/dist/name-validator.js +22 -1
- package/dist/name-validator.js.map +1 -0
- package/dist/opencode-drone.d.ts.map +1 -0
- package/dist/opencode-drone.js +438 -4
- package/dist/opencode-drone.js.map +1 -0
- package/dist/opencode-plugin.d.ts +1 -1
- package/dist/opencode-plugin.d.ts.map +1 -0
- package/dist/opencode-plugin.js +29 -4
- package/dist/opencode-plugin.js.map +1 -0
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -0
- package/dist/parse-assimilate-args.js +125 -1
- package/dist/parse-assimilate-args.js.map +1 -0
- package/dist/parse-launch-all-args.d.ts.map +1 -0
- package/dist/parse-launch-all-args.js +70 -1
- package/dist/parse-launch-all-args.js.map +1 -0
- package/dist/postinstall-banner.d.ts.map +1 -0
- package/dist/postinstall-banner.js +27 -2
- package/dist/postinstall-banner.js.map +1 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +32 -5
- package/dist/postinstall.js.map +1 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +9 -1
- package/dist/readiness-probe.js.map +1 -0
- package/dist/regen-format.d.ts.map +1 -0
- package/dist/regen-format.js +545 -13
- package/dist/regen-format.js.map +1 -0
- package/dist/regen.d.ts.map +1 -0
- package/dist/regen.js +141 -5
- package/dist/regen.js.map +1 -0
- package/dist/remote-client.d.ts +65 -175
- package/dist/remote-client.d.ts.map +1 -0
- package/dist/remote-client.js +942 -1
- package/dist/remote-client.js.map +1 -0
- package/dist/reset-local-seat-cmd.d.ts +50 -0
- package/dist/reset-local-seat-cmd.d.ts.map +1 -0
- package/dist/reset-local-seat-cmd.js +169 -0
- package/dist/reset-local-seat-cmd.js.map +1 -0
- package/dist/role-match.d.ts.map +1 -0
- package/dist/role-match.js +23 -1
- package/dist/role-match.js.map +1 -0
- package/dist/role-resolver.d.ts.map +1 -0
- package/dist/role-resolver.js +60 -1
- package/dist/role-resolver.js.map +1 -0
- package/dist/roster-render.d.ts.map +1 -0
- package/dist/roster-render.js +121 -3
- package/dist/roster-render.js.map +1 -0
- package/dist/seat-probe.d.ts +32 -13
- package/dist/seat-probe.d.ts.map +1 -0
- package/dist/seat-probe.js +78 -1
- package/dist/seat-probe.js.map +1 -0
- package/dist/seat-store.d.ts +76 -0
- package/dist/seat-store.d.ts.map +1 -0
- package/dist/seat-store.js +309 -0
- package/dist/seat-store.js.map +1 -0
- package/dist/seats.d.ts +303 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +551 -0
- package/dist/seats.js.map +1 -0
- package/dist/self-path.d.ts +27 -0
- package/dist/self-path.d.ts.map +1 -0
- package/dist/self-path.js +43 -0
- package/dist/self-path.js.map +1 -0
- package/dist/server-endpoint.d.ts +9 -0
- package/dist/server-endpoint.d.ts.map +1 -0
- package/dist/server-endpoint.js +38 -0
- package/dist/server-endpoint.js.map +1 -0
- package/dist/server-errors.d.ts +50 -0
- package/dist/server-errors.d.ts.map +1 -0
- package/dist/server-errors.js +79 -0
- package/dist/server-errors.js.map +1 -0
- package/dist/server-handshake.d.ts +197 -0
- package/dist/server-handshake.d.ts.map +1 -0
- package/dist/server-handshake.js +505 -0
- package/dist/server-handshake.js.map +1 -0
- package/dist/server-response.d.ts +3 -0
- package/dist/server-response.d.ts.map +1 -0
- package/dist/server-response.js +67 -0
- package/dist/server-response.js.map +1 -0
- package/dist/server-trust.d.ts +14 -0
- package/dist/server-trust.d.ts.map +1 -0
- package/dist/server-trust.js +225 -0
- package/dist/server-trust.js.map +1 -0
- package/dist/setup-confirm.d.ts +3 -3
- package/dist/setup-confirm.d.ts.map +1 -0
- package/dist/setup-confirm.js +126 -2
- package/dist/setup-confirm.js.map +1 -0
- package/dist/setup.d.ts +5 -3
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +169 -46
- package/dist/setup.js.map +1 -0
- package/dist/shell-escape.d.ts.map +1 -0
- package/dist/shell-escape.js +22 -1
- package/dist/shell-escape.js.map +1 -0
- package/dist/spawn.d.ts +2 -2
- package/dist/spawn.d.ts.map +1 -0
- package/dist/spawn.js +29 -10
- package/dist/spawn.js.map +1 -0
- package/dist/startup-services.d.ts +0 -1
- package/dist/startup-services.d.ts.map +1 -0
- package/dist/startup-services.js +26 -1
- package/dist/startup-services.js.map +1 -0
- package/dist/stream-owner.d.ts +6 -2
- package/dist/stream-owner.d.ts.map +1 -0
- package/dist/stream-owner.js +547 -2
- package/dist/stream-owner.js.map +1 -0
- package/dist/stream-status.d.ts +1 -1
- package/dist/stream-status.d.ts.map +1 -0
- package/dist/stream-status.js +268 -3
- package/dist/stream-status.js.map +1 -0
- package/dist/sync-roles-render.d.ts.map +1 -0
- package/dist/sync-roles-render.js +118 -3
- package/dist/sync-roles-render.js.map +1 -0
- package/dist/sync.d.ts +1 -1
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +286 -22
- package/dist/sync.js.map +1 -0
- package/dist/terminal-title.d.ts.map +1 -0
- package/dist/terminal-title.js +68 -1
- package/dist/terminal-title.js.map +1 -0
- package/dist/token-store.d.ts +17 -111
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +81 -1
- package/dist/token-store.js.map +1 -0
- package/dist/tool-manifest.d.ts +4 -8
- package/dist/tool-manifest.d.ts.map +1 -0
- package/dist/tool-manifest.js +554 -1
- package/dist/tool-manifest.js.map +1 -0
- package/dist/tool-scope.d.ts +8 -12
- package/dist/tool-scope.d.ts.map +1 -0
- package/dist/tool-scope.js +97 -1
- package/dist/tool-scope.js.map +1 -0
- package/dist/types.d.ts +1 -5
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -0
- package/dist/unknown-subcommand.js +37 -1
- package/dist/unknown-subcommand.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +78 -2
- package/dist/version.js.map +1 -0
- package/dist/working-repo.d.ts.map +1 -0
- package/dist/working-repo.js +81 -1
- package/dist/working-repo.js.map +1 -0
- package/dist/worktree-lifecycle.d.ts.map +1 -0
- package/dist/worktree-lifecycle.js +205 -2
- package/dist/worktree-lifecycle.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +40 -0
- package/docs/LOCAL_SERVER.md +103 -0
- package/docs/RELEASING.md +120 -0
- package/package.json +52 -34
- package/src/agent-runtime.ts +69 -0
- package/src/assimilate-cmd.ts +1864 -0
- package/src/assimilate-deps.ts +444 -0
- package/src/assimilate-guard.ts +84 -0
- package/src/assimilate-welcome.ts +39 -0
- package/src/auth-env.ts +21 -0
- package/src/backends/launch-all-pastelist.ts +18 -0
- package/src/backends/launch-all-tmux.ts +75 -0
- package/src/backends/launch-all-windows.ts +110 -0
- package/src/bare-launch-menu.ts +148 -0
- package/src/claude-launch-args.ts +35 -0
- package/src/claude.ts +561 -0
- package/src/cleanup-cmd.ts +581 -0
- package/src/clear-rewake-core.ts +34 -0
- package/src/clear-rewake.ts +18 -0
- package/src/cli-help.ts +116 -0
- package/src/cli-platform.ts +127 -0
- package/src/cli-tool-approval.ts +716 -0
- package/src/codex-app-server.ts +238 -0
- package/src/codex-app-wake.ts +512 -0
- package/src/codex-launch.ts +140 -0
- package/src/codex-remote.ts +500 -0
- package/src/codex-wake-resolve.ts +155 -0
- package/src/config-utils.ts +811 -0
- package/src/config.ts +595 -0
- package/src/console-prefix.ts +99 -0
- package/src/cube-name.ts +69 -0
- package/src/cubes.ts +583 -0
- package/src/debug.ts +73 -0
- package/src/direct-log.ts +9 -0
- package/src/docs-sections.ts +129 -0
- package/src/drone-lifecycle.ts +62 -0
- package/src/drone-management.ts +162 -0
- package/src/ensure-mcp-config.ts +54 -0
- package/src/evict-drone.ts +82 -0
- package/src/gc-orphan-inboxes.ts +229 -0
- package/src/inbox-monitor.ts +1005 -0
- package/src/index.ts +1201 -0
- package/src/launch-all-cmd.ts +450 -0
- package/src/launch-all-command.ts +30 -0
- package/src/launch-all-deps.ts +188 -0
- package/src/launch-all-discovery.ts +118 -0
- package/src/launch-all-locks.ts +74 -0
- package/src/launch-gate.ts +80 -0
- package/src/lifecycle-log-guard.ts +144 -0
- package/src/list-roles-render.ts +52 -0
- package/src/local-log-routing.ts +106 -0
- package/src/local-manage-tool-result.ts +10 -0
- package/src/local-server-cursor.ts +188 -0
- package/src/log-audit.ts +207 -0
- package/src/log-stream.ts +1505 -0
- package/src/model-presets.ts +22 -0
- package/src/name-validator.ts +23 -0
- package/src/opencode-drone.ts +528 -0
- package/src/opencode-plugin.ts +38 -0
- package/src/parse-assimilate-args.ts +125 -0
- package/src/parse-launch-all-args.ts +100 -0
- package/src/postinstall-banner.ts +37 -0
- package/src/postinstall.ts +36 -0
- package/src/readiness-probe.ts +12 -0
- package/src/regen-format.ts +651 -0
- package/src/regen.ts +150 -0
- package/src/remote-client.ts +1379 -0
- package/src/reset-local-seat-cmd.ts +227 -0
- package/src/role-match.ts +29 -0
- package/src/role-resolver.ts +93 -0
- package/src/roster-render.ts +196 -0
- package/src/seat-probe.ts +115 -0
- package/src/seat-store.ts +333 -0
- package/src/seats.ts +741 -0
- package/src/self-path.ts +51 -0
- package/src/server-endpoint.ts +38 -0
- package/src/server-errors.ts +93 -0
- package/src/server-handshake.ts +783 -0
- package/src/server-response.ts +76 -0
- package/src/server-trust.ts +245 -0
- package/src/setup-confirm.ts +171 -0
- package/src/setup.ts +186 -0
- package/src/shell-escape.ts +22 -0
- package/src/spawn.ts +31 -0
- package/src/startup-services.ts +36 -0
- package/src/stream-owner.ts +686 -0
- package/src/stream-status.ts +337 -0
- package/src/sync-roles-render.ts +163 -0
- package/src/sync.ts +363 -0
- package/src/terminal-title.ts +76 -0
- package/src/token-store.ts +102 -0
- package/src/tool-manifest.ts +610 -0
- package/src/tool-scope.ts +110 -0
- package/src/types.ts +5 -0
- package/src/unknown-subcommand.ts +35 -0
- package/src/version.ts +82 -0
- package/src/working-repo.ts +99 -0
- package/src/worktree-lifecycle.ts +254 -0
- package/dist/auth-recovery.d.ts +0 -33
- package/dist/auth-recovery.js +0 -1
- package/dist/auth.d.ts +0 -103
- package/dist/auth.js +0 -39
- package/dist/device-auth.d.ts +0 -75
- package/dist/device-auth.js +0 -1
- package/dist/drone-address.d.ts +0 -26
- package/dist/drone-address.js +0 -1
- package/dist/get-started.d.ts +0 -20
- package/dist/get-started.js +0 -2
- package/dist/health-beat.d.ts +0 -134
- package/dist/health-beat.js +0 -1
- package/dist/role-section.d.ts +0 -110
- package/dist/role-section.js +0 -8
- package/dist/setup-action.d.ts +0 -20
- package/dist/setup-action.js +0 -1
- package/dist/stale-version-check.d.ts +0 -40
- package/dist/stale-version-check.js +0 -1
- package/dist/subscription-retry.d.ts +0 -40
- package/dist/subscription-retry.js +0 -1
- package/dist/templates.d.ts +0 -107
- package/dist/templates.js +0 -359
- package/dist/token-crypto.d.ts +0 -50
- package/dist/token-crypto.js +0 -1
package/src/sync.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `borg sync` — worktree lifecycle management subcommand (gh#33).
|
|
3
|
+
*
|
|
4
|
+
* Reconciled to the per-worktree `wt-<suffix>` branch model (PR-B,
|
|
5
|
+
* ruling ea643b33). Replaces the previous main-centric semantics
|
|
6
|
+
* ("idle = on main"; "post-merge = checkout main") — under the approved
|
|
7
|
+
* gh#33 model `main` is NEVER a working branch in any worktree; it is
|
|
8
|
+
* purely the integration target. Every worktree works on a named
|
|
9
|
+
* `wt-<suffix>` branch and `borg sync` keeps it current with
|
|
10
|
+
* origin/main, returns to it after a feature branch merges, and absorbs
|
|
11
|
+
* upstream into an in-progress feature branch — never touching `main`
|
|
12
|
+
* as a checkout.
|
|
13
|
+
*
|
|
14
|
+
* All git mutation/decision logic is delegated to
|
|
15
|
+
* `src/worktree-lifecycle.ts` (the seam PR-A added:
|
|
16
|
+
* `adoptWorktree`, `syncWorktree`, `cleanupMerged`, `isMerged`,
|
|
17
|
+
* `localBranchExists`) so the never-discard guards (dirty / unmerged
|
|
18
|
+
* HEAD / unmerged target wt- branch) and the args-array subprocess
|
|
19
|
+
* shape are shared, not duplicated.
|
|
20
|
+
*
|
|
21
|
+
* Lifecycle states:
|
|
22
|
+
* 1. dirty — uncommitted changes; refuse (never discard).
|
|
23
|
+
* 2. on-wt — on the per-worktree `wt-<suffix>` branch;
|
|
24
|
+
* fast-forward it to origin/main (ff-only).
|
|
25
|
+
* 3. on-main — on `main`/`master` (or detached at a merged
|
|
26
|
+
* point); adopt the `wt-<suffix>` branch
|
|
27
|
+
* (Q4: main is never a working branch).
|
|
28
|
+
* 4. feature-mid-sprint — on a feature branch not yet merged; absorb
|
|
29
|
+
* origin/main into it via `git merge --no-edit`
|
|
30
|
+
* (no rebase — cube workflow rule (a)) when
|
|
31
|
+
* origin/main advanced; else no-op.
|
|
32
|
+
* 5. feature-merged — on a feature branch fully merged into
|
|
33
|
+
* origin/main; return to `wt-<suffix>` and
|
|
34
|
+
* ANNOUNCE the prunable feature branch (prune
|
|
35
|
+
* only with `--prune`, Q3).
|
|
36
|
+
*
|
|
37
|
+
* Anti-features (intentional, unchanged):
|
|
38
|
+
* - No auto-stash / auto-commit / auto-discard on dirty tree.
|
|
39
|
+
* - No force-push, no rebase, no --force-with-lease.
|
|
40
|
+
* - No remote-branch deletion (Coordinator owns merge actions).
|
|
41
|
+
* - Local feature-branch deletion only on explicit `--prune` (Q3).
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { spawnSync } from 'node:child_process';
|
|
45
|
+
import { basename } from 'node:path';
|
|
46
|
+
import chalk from 'chalk';
|
|
47
|
+
import {
|
|
48
|
+
adoptWorktree,
|
|
49
|
+
syncWorktree,
|
|
50
|
+
cleanupMerged,
|
|
51
|
+
isMerged,
|
|
52
|
+
perWorktreeBranchName,
|
|
53
|
+
type RunSync,
|
|
54
|
+
} from './worktree-lifecycle.js';
|
|
55
|
+
|
|
56
|
+
// ------------------------------------------------------------------
|
|
57
|
+
// Types
|
|
58
|
+
// ------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export interface SpawnSyncResult {
|
|
61
|
+
status: number | null;
|
|
62
|
+
stdout: string;
|
|
63
|
+
stderr: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SyncDeps {
|
|
67
|
+
/** Synchronous spawn — used for `git ...` invocations. */
|
|
68
|
+
runSync?: RunSync;
|
|
69
|
+
/** Inject for tests; defaults to `process.cwd()`. */
|
|
70
|
+
cwd?: () => string;
|
|
71
|
+
/** stderr sink — overridable for tests. */
|
|
72
|
+
stderr?: (line: string) => void;
|
|
73
|
+
/** stdout sink — overridable for tests. */
|
|
74
|
+
stdout?: (line: string) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface SyncOptions {
|
|
78
|
+
/** Q3: prune a merged feature branch instead of only announcing it. */
|
|
79
|
+
prune: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const defaultDeps: Required<SyncDeps> = {
|
|
83
|
+
runSync: (cmd, args, cwd) => {
|
|
84
|
+
const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
|
|
85
|
+
return {
|
|
86
|
+
status: r.status,
|
|
87
|
+
stdout: r.stdout ?? '',
|
|
88
|
+
stderr: r.stderr ?? '',
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
cwd: () => process.cwd(),
|
|
92
|
+
stderr: (line) => process.stderr.write(line),
|
|
93
|
+
stdout: (line) => process.stdout.write(line),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const DEFAULT_BRANCH = 'origin/main';
|
|
97
|
+
|
|
98
|
+
// ------------------------------------------------------------------
|
|
99
|
+
// wt- branch resolution
|
|
100
|
+
// ------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the per-worktree `wt-` branch for the current checkout,
|
|
104
|
+
* DETERMINISTICALLY per worktree.
|
|
105
|
+
*
|
|
106
|
+
* - current branch is already `wt-*` → that is the branch.
|
|
107
|
+
* - otherwise → derive `wt-<suffix>` from THIS worktree's directory
|
|
108
|
+
* basename minus the main worktree's (repo) basename prefix — the
|
|
109
|
+
* same `perWorktreeBranchName` derivation the spawn path uses, so
|
|
110
|
+
* the name matches what `borg assimilate --worktree` created.
|
|
111
|
+
*
|
|
112
|
+
* This must NOT list `git branch --list wt-*`: in linked worktrees the
|
|
113
|
+
* local branch namespace is SHARED across all siblings (gh#33 CR-v2
|
|
114
|
+
* blocker 32bc45da), so every drone's `wt-` branch is visible and a
|
|
115
|
+
* "single match" heuristic is never satisfied in a real multi-drone
|
|
116
|
+
* cube. The directory-derivation is unique per worktree and never
|
|
117
|
+
* ambiguous. The first `worktree <path>` line of
|
|
118
|
+
* `git worktree list --porcelain` is the main worktree (the repo dir);
|
|
119
|
+
* its basename is the prefix to strip. For an independent clone the
|
|
120
|
+
* main worktree IS this worktree, so the prefix == basename and the
|
|
121
|
+
* result is `wt-<basename>` (no strip) — consistent with PR-A's
|
|
122
|
+
* in-place adoption.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveWtBranch(
|
|
125
|
+
runSync: RunSync,
|
|
126
|
+
cwd: string,
|
|
127
|
+
currentBranch: string
|
|
128
|
+
): string {
|
|
129
|
+
if (currentBranch.startsWith('wt-')) return currentBranch;
|
|
130
|
+
const top = runSync('git', ['rev-parse', '--show-toplevel'], cwd);
|
|
131
|
+
const thisDir = top.status === 0 ? top.stdout.trim() : cwd;
|
|
132
|
+
const wtList = runSync('git', ['worktree', 'list', '--porcelain'], cwd);
|
|
133
|
+
let mainDir = thisDir;
|
|
134
|
+
if (wtList.status === 0) {
|
|
135
|
+
const firstWorktreeLine = wtList.stdout
|
|
136
|
+
.split('\n')
|
|
137
|
+
.find((l) => l.startsWith('worktree '));
|
|
138
|
+
if (firstWorktreeLine) mainDir = firstWorktreeLine.slice('worktree '.length).trim();
|
|
139
|
+
}
|
|
140
|
+
return perWorktreeBranchName(basename(thisDir), basename(mainDir));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ------------------------------------------------------------------
|
|
144
|
+
// State detection
|
|
145
|
+
// ------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
export type SyncState =
|
|
148
|
+
| { kind: 'dirty'; files: string[] }
|
|
149
|
+
| { kind: 'on-wt'; branch: string; wtBranch: string }
|
|
150
|
+
| { kind: 'on-main'; branch: string; wtBranch: string }
|
|
151
|
+
| { kind: 'feature-mid-sprint'; branch: string; wtBranch: string; commits: number }
|
|
152
|
+
| { kind: 'feature-merged'; branch: string; wtBranch: string }
|
|
153
|
+
| { kind: 'error'; reason: string };
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect the worktree's lifecycle state. Read-only except for the
|
|
157
|
+
* `git fetch origin --prune` needed to measure against the latest tip.
|
|
158
|
+
*/
|
|
159
|
+
export function detectState(deps: Required<SyncDeps>): SyncState {
|
|
160
|
+
const { runSync, cwd } = deps;
|
|
161
|
+
const cwdValue = cwd();
|
|
162
|
+
|
|
163
|
+
// (1) git repo?
|
|
164
|
+
if (runSync('git', ['rev-parse', '--show-toplevel'], cwdValue).status !== 0) {
|
|
165
|
+
return { kind: 'error', reason: `not in a git repository (cwd: ${cwdValue})` };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// (2) dirty FIRST — never act on uncommitted changes.
|
|
169
|
+
const status = runSync('git', ['status', '--porcelain'], cwdValue);
|
|
170
|
+
if (status.status !== 0) {
|
|
171
|
+
return { kind: 'error', reason: `git status failed: ${status.stderr.trim()}` };
|
|
172
|
+
}
|
|
173
|
+
const dirty = status.stdout.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
|
|
174
|
+
if (dirty.length > 0) return { kind: 'dirty', files: dirty };
|
|
175
|
+
|
|
176
|
+
// (3) current branch.
|
|
177
|
+
const branchProbe = runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwdValue);
|
|
178
|
+
if (branchProbe.status !== 0) {
|
|
179
|
+
return { kind: 'error', reason: `cannot resolve current branch: ${branchProbe.stderr.trim()}` };
|
|
180
|
+
}
|
|
181
|
+
const branch = branchProbe.stdout.trim();
|
|
182
|
+
if (branch === 'HEAD') {
|
|
183
|
+
return { kind: 'error', reason: 'detached HEAD; run `borg assimilate` to adopt a wt- branch first' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// (4) fetch — fatal on failure (can't reason about lifecycle offline).
|
|
187
|
+
const fetch = runSync('git', ['fetch', 'origin', '--prune'], cwdValue);
|
|
188
|
+
if (fetch.status !== 0) {
|
|
189
|
+
return { kind: 'error', reason: `git fetch origin failed: ${fetch.stderr.trim()}` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const wtBranch = resolveWtBranch(runSync, cwdValue, branch);
|
|
193
|
+
|
|
194
|
+
// (5) on the per-worktree wt- branch.
|
|
195
|
+
if (branch.startsWith('wt-')) {
|
|
196
|
+
return { kind: 'on-wt', branch, wtBranch: branch };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// (6) on main/master — Q4: never a working branch; adopt wt-.
|
|
200
|
+
if (branch === 'main' || branch === 'master') {
|
|
201
|
+
return { kind: 'on-main', branch, wtBranch };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// (7) feature branch — merged vs mid-sprint.
|
|
205
|
+
// ASSUMPTION (gh#33 CR NIT 0e19637a): merged-detection keys off commit
|
|
206
|
+
// ancestry (`isMerged` = HEAD is an ancestor of origin/main), which
|
|
207
|
+
// holds for merge-commit / fast-forward integration. A squash- or
|
|
208
|
+
// rebase-merged PR leaves the feature tip a NON-ancestor → it would
|
|
209
|
+
// classify here as mid-sprint, not merged. That degrades safely
|
|
210
|
+
// (merge-back of origin/main, no discard), and this cube integrates
|
|
211
|
+
// via merge commits, so it is not triggered. If squash-merge is ever
|
|
212
|
+
// adopted, switch this to PR-merged-state detection.
|
|
213
|
+
if (isMerged(runSync, cwdValue, 'HEAD', DEFAULT_BRANCH)) {
|
|
214
|
+
return { kind: 'feature-merged', branch, wtBranch };
|
|
215
|
+
}
|
|
216
|
+
// not merged → mid-sprint. Count how far origin/main advanced past the
|
|
217
|
+
// merge-base so the message can report it (0 → no-op).
|
|
218
|
+
const mergeBase = runSync('git', ['merge-base', 'HEAD', DEFAULT_BRANCH], cwdValue);
|
|
219
|
+
const base = mergeBase.status === 0 ? mergeBase.stdout.trim() : '';
|
|
220
|
+
const count = runSync('git', ['rev-list', '--count', `${base}..${DEFAULT_BRANCH}`], cwdValue);
|
|
221
|
+
const commits = parseInt(count.stdout.trim(), 10) || 0;
|
|
222
|
+
return { kind: 'feature-mid-sprint', branch, wtBranch, commits };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ------------------------------------------------------------------
|
|
226
|
+
// Orchestrator
|
|
227
|
+
// ------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
export async function runSync(deps: SyncDeps = {}, opts: SyncOptions = { prune: false }): Promise<number> {
|
|
230
|
+
const merged = { ...defaultDeps, ...deps };
|
|
231
|
+
const { runSync: run, cwd, stderr, stdout } = merged;
|
|
232
|
+
const state = detectState(merged);
|
|
233
|
+
|
|
234
|
+
if (state.kind === 'error') {
|
|
235
|
+
stderr(chalk.red(`◼ borg sync: ${state.reason}\n`));
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (state.kind === 'dirty') {
|
|
240
|
+
stderr(chalk.yellow(`◼ Working tree has uncommitted changes.\n`));
|
|
241
|
+
for (const line of state.files.slice(0, 5)) stderr(chalk.gray(` ${line}\n`));
|
|
242
|
+
if (state.files.length > 5) stderr(chalk.gray(` ... and ${state.files.length - 5} more\n`));
|
|
243
|
+
stderr(chalk.yellow(`◼ Commit, stash, or restore before running \`borg sync\`. Nothing was changed.\n`));
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// on-wt: fast-forward the per-worktree branch to origin/main (ff-only,
|
|
248
|
+
// clean-gated, never merge/rebase). Delegates to syncWorktree.
|
|
249
|
+
if (state.kind === 'on-wt') {
|
|
250
|
+
const res = syncWorktree(run, cwd(), state.wtBranch, DEFAULT_BRANCH);
|
|
251
|
+
if (res.action === 'fast-forwarded') {
|
|
252
|
+
stdout(chalk.blue(`◼ On \`${state.wtBranch}\`; fast-forwarded to ${DEFAULT_BRANCH}.\n`));
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
if (res.action === 'already-current') {
|
|
256
|
+
stdout(chalk.blue(`◼ On \`${state.wtBranch}\`; up to date with ${DEFAULT_BRANCH}.\n`));
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
// skipped-diverged: the wt- branch has local commits not on origin/main.
|
|
260
|
+
stderr(chalk.yellow(`◼ ${res.message ?? 'sync skipped'}.\n`));
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// on-main: adopt the wt- branch (Q4 — move off main). adoptWorktree
|
|
265
|
+
// applies the dirty / unmerged-HEAD / unmerged-target guards.
|
|
266
|
+
if (state.kind === 'on-main') {
|
|
267
|
+
return adoptAndReport(state.wtBranch, run, cwd, stdout, stderr);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// feature-mid-sprint: absorb origin/main into the feature branch (no
|
|
271
|
+
// rebase). Leaves the drone on the feature branch to keep working.
|
|
272
|
+
if (state.kind === 'feature-mid-sprint') {
|
|
273
|
+
if (state.commits === 0) {
|
|
274
|
+
stdout(chalk.blue(`◼ On \`${state.branch}\` (feature branch); up to date with ${DEFAULT_BRANCH}.\n`));
|
|
275
|
+
stdout(chalk.gray(`◼ Continue your sprint, or post REVIEW-READY when complete.\n`));
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const merge = run('git', ['merge', '--no-edit', DEFAULT_BRANCH], cwd());
|
|
279
|
+
if (merge.status !== 0) {
|
|
280
|
+
stderr(chalk.red(`◼ borg sync: git merge ${DEFAULT_BRANCH} failed (likely conflict). Resolve manually:\n${merge.stderr.trim()}\n`));
|
|
281
|
+
return 1;
|
|
282
|
+
}
|
|
283
|
+
stdout(chalk.blue(`◼ On \`${state.branch}\`; merged ${state.commits} commit${state.commits === 1 ? '' : 's'} from ${DEFAULT_BRANCH} (no rebase).\n`));
|
|
284
|
+
stdout(chalk.gray(`◼ Re-run tests; continue your sprint.\n`));
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// feature-merged: the PR merged. Return to the wt- branch (adopt) and
|
|
289
|
+
// announce the prunable feature branch (prune only with --prune, Q3).
|
|
290
|
+
if (state.kind === 'feature-merged') {
|
|
291
|
+
const feature = state.branch;
|
|
292
|
+
const code = adoptAndReport(state.wtBranch, run, cwd, stdout, stderr, {
|
|
293
|
+
adoptedPrefix: `◼ \`${feature}\` is merged into ${DEFAULT_BRANCH};`,
|
|
294
|
+
});
|
|
295
|
+
if (code !== 0) return code; // adoption blocked (dirty/unmerged target) — don't prune
|
|
296
|
+
// Now on the wt- branch — safe to prune/announce the merged feature.
|
|
297
|
+
const cleanup = cleanupMerged(run, cwd(), feature, DEFAULT_BRANCH, { prune: opts.prune });
|
|
298
|
+
if (cleanup.action === 'pruned') {
|
|
299
|
+
stdout(chalk.blue(`◼ Pruned merged branch \`${feature}\`.\n`));
|
|
300
|
+
} else if (cleanup.action === 'announced') {
|
|
301
|
+
stdout(chalk.gray(`◼ ${cleanup.message}\n`));
|
|
302
|
+
}
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Exhaustiveness.
|
|
307
|
+
const _exhaustive: never = state;
|
|
308
|
+
stderr(chalk.red(`◼ borg sync: unhandled state\n`));
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Shared adopt-the-wt-branch path for the on-main + feature-merged
|
|
314
|
+
* states. Surfaces the never-discard outcomes; returns the process exit
|
|
315
|
+
* code (0 adopted, 1 blocked/ambiguous).
|
|
316
|
+
*/
|
|
317
|
+
function adoptAndReport(
|
|
318
|
+
wtBranch: string,
|
|
319
|
+
run: RunSync,
|
|
320
|
+
cwd: () => string,
|
|
321
|
+
stdout: (l: string) => void,
|
|
322
|
+
stderr: (l: string) => void,
|
|
323
|
+
opts: { adoptedPrefix?: string } = {}
|
|
324
|
+
): number {
|
|
325
|
+
const res = adoptWorktree(run, cwd(), wtBranch, DEFAULT_BRANCH);
|
|
326
|
+
if (res.action === 'adopted') {
|
|
327
|
+
if (opts.adoptedPrefix) {
|
|
328
|
+
stdout(chalk.blue(`${opts.adoptedPrefix} switched to \`${wtBranch}\` at ${DEFAULT_BRANCH}.\n`));
|
|
329
|
+
} else {
|
|
330
|
+
stdout(chalk.blue(`◼ On \`${wtBranch}\` at ${DEFAULT_BRANCH}.\n`));
|
|
331
|
+
}
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
// skipped-dirty handled upstream (dirty state), but defensively surface.
|
|
335
|
+
stderr(chalk.yellow(`◼ borg sync: ${res.message ?? 'not adopted'}. Nothing was changed.\n`));
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ------------------------------------------------------------------
|
|
340
|
+
// CLI surface
|
|
341
|
+
// ------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
export type ParseResult = { ok: true; options: SyncOptions } | { ok: false; error: string };
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Parse args after `borg sync`. Supports `--prune` (Q3: delete a merged
|
|
347
|
+
* feature branch after returning to the wt- branch). Rejects anything
|
|
348
|
+
* else to keep room for future flags.
|
|
349
|
+
*/
|
|
350
|
+
export function parseSyncArgs(rawArgs: string[]): ParseResult {
|
|
351
|
+
let prune = false;
|
|
352
|
+
for (const arg of rawArgs) {
|
|
353
|
+
if (arg === '--prune') {
|
|
354
|
+
prune = true;
|
|
355
|
+
} else {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
error: `unexpected argument: ${arg}. Usage: borg sync [--prune]`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return { ok: true, options: { prune } };
|
|
363
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-title setter for borg drone sessions.
|
|
3
|
+
*
|
|
4
|
+
* Multiple Claude Code sessions across sibling worktrees are visually
|
|
5
|
+
* indistinguishable in Cmd-Tab / tab bars / Mission Control on macOS,
|
|
6
|
+
* and likewise on most Linux terminal emulators. Setting the terminal
|
|
7
|
+
* title via the OSC 0 / OSC 2 escape gives each window a free per-
|
|
8
|
+
* session identity.
|
|
9
|
+
*
|
|
10
|
+
* Format (Queen-specified):
|
|
11
|
+
* - Assimilated drone session: `borg · <label> · <cubeName>`
|
|
12
|
+
* - Unassimilated session: `borg · <repo-basename>`
|
|
13
|
+
*
|
|
14
|
+
* Why OSC 0 (`\x1b]0;…\x07`): sets both window title AND icon name on
|
|
15
|
+
* most terminals. OSC 2 sets only window title; OSC 1 sets only icon
|
|
16
|
+
* name. OSC 0 is the maximally-portable choice — works in iTerm2,
|
|
17
|
+
* macOS Terminal, kitty, alacritty, ghostty, GNOME Terminal, xterm.
|
|
18
|
+
*
|
|
19
|
+
* Lifetime: the escape is emitted once, before spawning Claude Code.
|
|
20
|
+
* Claude Code itself does not set its own window title (verified
|
|
21
|
+
* 2026-05-11), so the borg-set title persists for the whole session.
|
|
22
|
+
*
|
|
23
|
+
* Limitations (acceptable for v1; flagged for future):
|
|
24
|
+
* - Title doesn't update mid-session on `borg_assimilate`. The
|
|
25
|
+
* borgmcp client process can't write the escape post-spawn
|
|
26
|
+
* because stdio is owned by Claude Code at that point (and stdio
|
|
27
|
+
* to Claude is JSON-RPC — terminal escapes would be parsed as
|
|
28
|
+
* invalid messages).
|
|
29
|
+
* - Falls back to repo-basename for the unassimilated case; loses
|
|
30
|
+
* drone identity until the cube is joined. Typical pattern is
|
|
31
|
+
* "drone has been around long enough that cubes.json is already
|
|
32
|
+
* populated," so the assimilated path is the common case.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pure: compose the title string for a session. Exported so tests can
|
|
37
|
+
* exercise every branch without TTY / process / fs dependencies.
|
|
38
|
+
*
|
|
39
|
+
* @param activeDrone — `{label, cubeName}` if this project is
|
|
40
|
+
* assimilated to a cube, null otherwise.
|
|
41
|
+
* @param repoBasename — fallback identity for the unassimilated case
|
|
42
|
+
* (typically `basename(process.cwd())`).
|
|
43
|
+
*/
|
|
44
|
+
export function composeTerminalTitle(
|
|
45
|
+
activeDrone: { label: string; cubeName: string } | null,
|
|
46
|
+
repoBasename: string
|
|
47
|
+
): string {
|
|
48
|
+
if (activeDrone) {
|
|
49
|
+
return `borg · ${activeDrone.label} · ${activeDrone.cubeName}`;
|
|
50
|
+
}
|
|
51
|
+
return `borg · ${repoBasename}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Side-effecting: emit the OSC 0 escape to stdout, but only if stdout
|
|
56
|
+
* is a TTY. When stdout is piped (CI, redirection, scripted
|
|
57
|
+
* invocation), emitting the raw escape would pollute the captured
|
|
58
|
+
* output without doing anything useful — so we no-op.
|
|
59
|
+
*
|
|
60
|
+
* Returns the title string that WOULD have been emitted, regardless of
|
|
61
|
+
* TTY state, so callers can log it independently for diagnostics.
|
|
62
|
+
*/
|
|
63
|
+
export function setTerminalTitle(
|
|
64
|
+
activeDrone: { label: string; cubeName: string } | null,
|
|
65
|
+
repoBasename: string,
|
|
66
|
+
stdout: NodeJS.WriteStream = process.stdout
|
|
67
|
+
): string {
|
|
68
|
+
const title = composeTerminalTitle(activeDrone, repoBasename);
|
|
69
|
+
if (stdout.isTTY) {
|
|
70
|
+
// OSC 0: ESC ] 0 ; <title> BEL
|
|
71
|
+
// (BEL terminator is honored by all OSC-supporting terminals;
|
|
72
|
+
// ST = `ESC \` is an alternative but BEL has wider compat.)
|
|
73
|
+
stdout.write(`\x1b]0;${title}\x07`);
|
|
74
|
+
}
|
|
75
|
+
return title;
|
|
76
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-server credential backend.
|
|
3
|
+
*
|
|
4
|
+
* config.ts's local-server credential group sits on top of this. Storage is the
|
|
5
|
+
* 0600 file store (Queen rescope) — the OS keychain (@napi-rs/keyring) is GONE.
|
|
6
|
+
* The raw secret rests only in the 0600 file, parity with the server's own TLS
|
|
7
|
+
* private keys; there is no keychain and no obfuscation-grade fallback.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { atomicWrite0600, readStoreFile } from './seat-store.js';
|
|
11
|
+
|
|
12
|
+
// The OS-keychain backend was retired with the Queen rescope; the only backend
|
|
13
|
+
// name is the 0600 file store.
|
|
14
|
+
export type TokenBackendName = 'file';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Account-agnostic key/value store over the 0600 credential file.
|
|
18
|
+
*/
|
|
19
|
+
export interface TokenBackend {
|
|
20
|
+
readonly name: TokenBackendName;
|
|
21
|
+
get(account: string): Promise<string | null>;
|
|
22
|
+
set(account: string, value: string): Promise<void>;
|
|
23
|
+
delete(account: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── 0600 file backend (Queen rescope: replaces the OS keychain) ─────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a TokenBackend over a single 0600 store file, all accounts held in one
|
|
30
|
+
* `{version, accounts}` map. get/set/delete read-modify-write the file via the
|
|
31
|
+
* seat-store's atomic 0600 writer — the RAW secret rests only in the 0600 file
|
|
32
|
+
* (parity with the server's TLS keys), never a keychain.
|
|
33
|
+
*
|
|
34
|
+
* These ops are NON-flocking by design: the config layer holds the single store
|
|
35
|
+
* lock (withStoreLock over CREDENTIALS_LOCK) continuously across each
|
|
36
|
+
* read-compare-write, so nesting a second lock here would deadlock the O_EXCL
|
|
37
|
+
* lockfile. Pure reads (get) are safe lock-free because atomicWrite0600's rename
|
|
38
|
+
* guarantees a reader only ever sees a complete file.
|
|
39
|
+
*/
|
|
40
|
+
export function makeFileBackend(filePath: string): TokenBackend {
|
|
41
|
+
const load = async (): Promise<Record<string, string>> => {
|
|
42
|
+
const raw = await readStoreFile(filePath);
|
|
43
|
+
// CR4 fail-closed: ONLY a missing file initializes empty. A present-but-
|
|
44
|
+
// malformed / wrong-version / schema-invalid credential store MUST NOT read as
|
|
45
|
+
// empty — a subsequent set/delete would OVERWRITE it and erase every stored
|
|
46
|
+
// account (parent enrollment credentials + pending records). Throw WITHOUT
|
|
47
|
+
// writing so the corrupt bytes are preserved for recovery.
|
|
48
|
+
if (raw === null) return {};
|
|
49
|
+
let parsed: unknown;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(raw);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error('Borg credential store is malformed; refusing to overwrite it');
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
!parsed ||
|
|
57
|
+
typeof parsed !== 'object' ||
|
|
58
|
+
Array.isArray(parsed) ||
|
|
59
|
+
(parsed as { version?: unknown }).version !== 1 ||
|
|
60
|
+
!(parsed as { accounts?: unknown }).accounts ||
|
|
61
|
+
typeof (parsed as { accounts?: unknown }).accounts !== 'object' ||
|
|
62
|
+
Array.isArray((parsed as { accounts?: unknown }).accounts)
|
|
63
|
+
) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'Borg credential store is malformed or has an unsupported version; refusing to overwrite it',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
// CR#2: FULL value validation — every account value MUST be a string. A
|
|
69
|
+
// non-string value (a corrupted/tampered entry) fails CLOSED without writing,
|
|
70
|
+
// so a following set/delete never overwrites the store off a bad read.
|
|
71
|
+
const accounts = (parsed as { accounts: Record<string, unknown> }).accounts;
|
|
72
|
+
for (const value of Object.values(accounts)) {
|
|
73
|
+
if (typeof value !== 'string') {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'Borg credential store is malformed (a non-string account value); refusing to overwrite it',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { ...(accounts as Record<string, string>) };
|
|
80
|
+
};
|
|
81
|
+
const save = (accounts: Record<string, string>): Promise<void> =>
|
|
82
|
+
atomicWrite0600(filePath, JSON.stringify({ version: 1, accounts }, null, 2) + '\n');
|
|
83
|
+
return {
|
|
84
|
+
name: 'file',
|
|
85
|
+
async get(account) {
|
|
86
|
+
const accounts = await load();
|
|
87
|
+
return Object.prototype.hasOwnProperty.call(accounts, account) ? accounts[account] : null;
|
|
88
|
+
},
|
|
89
|
+
async set(account, value) {
|
|
90
|
+
const accounts = await load();
|
|
91
|
+
accounts[account] = value;
|
|
92
|
+
await save(accounts);
|
|
93
|
+
},
|
|
94
|
+
async delete(account) {
|
|
95
|
+
const accounts = await load();
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(accounts, account)) {
|
|
97
|
+
delete accounts[account];
|
|
98
|
+
await save(accounts);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|