heyspex 0.1.0 → 0.2.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/.claude-plugin/marketplace.json +14 -0
- package/README.md +21 -0
- package/bin/hs.mjs +1089 -104
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1127 -103
- package/dist/cli/hs-claim.mjs +981 -63
- package/dist/cli/hs-connect.mjs +10059 -1729
- package/dist/cli/hs-defer.mjs +5507 -3072
- package/dist/cli/hs-dev.mjs +576 -0
- package/dist/cli/hs-env.mjs +416 -302
- package/dist/cli/hs-index-rebuild.mjs +7334 -0
- package/dist/cli/hs-index-reconcile.mjs +8138 -0
- package/dist/cli/hs-init.mjs +2840 -149
- package/dist/cli/hs-install.mjs +7214 -594
- package/dist/cli/hs-join.mjs +10640 -2421
- package/dist/cli/hs-vault-pull.mjs +8157 -6370
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +4029 -1555
- package/dist/cli/hs-workspace.mjs +2702 -538
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +33 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +249 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +80 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
- package/plugins/heyspex/hooks/hooks.json +418 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
- package/scripts/claude-hooks/_run.mjs +413 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +577 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +86 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +451 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
- package/scripts/cursor-hooks/manifest.mjs +281 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +288 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +496 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +272 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +199 -61
- package/scripts/lib/customer-kit.mjs +1203 -0
- package/scripts/lib/dev-mode.mjs +249 -32
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +13 -22
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -307
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/lib/window-placement.mjs +109 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +449 -0
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +275 -0
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +196 -0
- package/spexharness/src/intent-directive.mjs +626 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +849 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +96 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +77 -8
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
- package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
- package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
- package/vendor/heyspex-mcp/cli.mjs +207 -15524
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +822 -17
- package/vendor/heyspex-mcp/console-transport.mjs +172 -0
- package/vendor/heyspex-mcp/credentials.mjs +585 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +131 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +723 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
- package/vendor/heyspex-mcp/http-transport.mjs +302 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +107 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +908 -22
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
- package/vendor/heyspex-mcp/promote-capture.mjs +388 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +666 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +191 -15
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +83 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +625 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +109 -0
- package/vendor/heyspex-mcp/session-tools.mjs +162 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +453 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +304 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +720 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +92 -0
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -5,8 +5,307 @@ var __esm = (fn, res) => function __init() {
|
|
|
5
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
// scripts/lib/heyspex-home.mjs
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
function heyspexHome(env = process.env) {
|
|
11
|
+
const override = env?.HEYSPEX_HOME;
|
|
12
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
13
|
+
return override.trim();
|
|
14
|
+
}
|
|
15
|
+
return homedir();
|
|
16
|
+
}
|
|
17
|
+
var init_heyspex_home = __esm({
|
|
18
|
+
"scripts/lib/heyspex-home.mjs"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// vendor/heyspex-mcp/credentials.mjs
|
|
24
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
25
|
+
import os, { homedir as homedir2 } from "node:os";
|
|
26
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
27
|
+
function credentialsFilePath(homeDir = heyspexHome()) {
|
|
28
|
+
return join2(homeDir, ".heyspex", "credentials.yaml");
|
|
29
|
+
}
|
|
30
|
+
function parseMcpToken(raw) {
|
|
31
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
32
|
+
const token = raw.match(MCP_TOKEN_LINE_RE)?.[1]?.trim();
|
|
33
|
+
return token ? token : null;
|
|
34
|
+
}
|
|
35
|
+
function isValidAccountId(id) {
|
|
36
|
+
return typeof id === "string" && ACCOUNT_ID_RE.test(id);
|
|
37
|
+
}
|
|
38
|
+
function topLevelScalar(raw, key) {
|
|
39
|
+
const re = new RegExp(`^${key}:[ \\t]*["']?([^"'\\r\\n#]+)`, "m");
|
|
40
|
+
const value = raw.match(re)?.[1]?.trim();
|
|
41
|
+
return value ? value : null;
|
|
42
|
+
}
|
|
43
|
+
function decodeScalar(text2) {
|
|
44
|
+
const t = text2.trim();
|
|
45
|
+
if (t === "" || t === "null" || t === "~") return null;
|
|
46
|
+
if (t.startsWith('"')) {
|
|
47
|
+
try {
|
|
48
|
+
const v = JSON.parse(t);
|
|
49
|
+
return typeof v === "string" && v ? v : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return t.replace(/^"|"$/g, "") || null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (t.startsWith("'")) return t.replace(/^'|'$/g, "") || null;
|
|
55
|
+
const bare = t.replace(/\s+#.*$/, "").trim();
|
|
56
|
+
return bare || null;
|
|
57
|
+
}
|
|
58
|
+
function emptyAccount() {
|
|
59
|
+
return { mcp_token: null, console_url: null, email: null, paired_at: null };
|
|
60
|
+
}
|
|
61
|
+
function findAccountsBlock(lines) {
|
|
62
|
+
const start = lines.findIndex((l) => /^accounts:[ \t]*(#.*)?$/.test(l));
|
|
63
|
+
if (start < 0) return null;
|
|
64
|
+
let end = start + 1;
|
|
65
|
+
while (end < lines.length && (lines[end].trim() === "" || /^[ \t]/.test(lines[end]))) end += 1;
|
|
66
|
+
while (end > start + 1 && lines[end - 1].trim() === "") end -= 1;
|
|
67
|
+
return { start, end };
|
|
68
|
+
}
|
|
69
|
+
function parseAccountsBlock(lines) {
|
|
70
|
+
const accounts = {};
|
|
71
|
+
const block = findAccountsBlock(lines);
|
|
72
|
+
if (!block) return accounts;
|
|
73
|
+
let current = null;
|
|
74
|
+
for (const line of lines.slice(block.start + 1, block.end)) {
|
|
75
|
+
const header = line.match(/^ {2}([^\s:#][^:]*):[ \t]*(#.*)?$/);
|
|
76
|
+
if (header) {
|
|
77
|
+
const id = decodeScalar(header[1]);
|
|
78
|
+
current = isValidAccountId(id) ? id : null;
|
|
79
|
+
if (current) accounts[current] = emptyAccount();
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const field = line.match(/^ {4}([a-z_]+):(.*)$/);
|
|
83
|
+
if (field && current && ACCOUNT_FIELDS.includes(field[1])) {
|
|
84
|
+
accounts[current][field[1]] = decodeScalar(field[2]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return accounts;
|
|
88
|
+
}
|
|
89
|
+
function parseCredentialsStore(raw) {
|
|
90
|
+
const text2 = typeof raw === "string" ? raw : "";
|
|
91
|
+
if (topLevelScalar(text2, "schema_version") !== String(CREDENTIALS_SCHEMA_VERSION)) {
|
|
92
|
+
const token = parseMcpToken(text2);
|
|
93
|
+
if (!token) return { schemaVersion: 1, currentAccountId: null, accounts: {} };
|
|
94
|
+
return {
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
currentAccountId: LEGACY_ACCOUNT_ID,
|
|
97
|
+
accounts: {
|
|
98
|
+
[LEGACY_ACCOUNT_ID]: { ...emptyAccount(), mcp_token: token, console_url: topLevelScalar(text2, "console_url") }
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const accounts = parseAccountsBlock(text2.split(/\r?\n/));
|
|
103
|
+
const current = topLevelScalar(text2, "current_account_id");
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 2,
|
|
106
|
+
currentAccountId: current && accounts[current] ? current : null,
|
|
107
|
+
accounts
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function resolveBearer({ envToken, credentialsText, accountId: accountId2, legacyAccountId } = {}) {
|
|
111
|
+
const fromEnv = typeof envToken === "string" ? envToken.trim() : "";
|
|
112
|
+
if (fromEnv) return { token: fromEnv, source: "env" };
|
|
113
|
+
const store = parseCredentialsStore(credentialsText);
|
|
114
|
+
let entry;
|
|
115
|
+
if (typeof accountId2 === "string" && accountId2.trim()) {
|
|
116
|
+
const wanted = accountId2.trim();
|
|
117
|
+
entry = store.accounts[wanted];
|
|
118
|
+
if (!entry && store.schemaVersion === 1 && legacyAccountId && wanted === legacyAccountId) {
|
|
119
|
+
entry = store.accounts[LEGACY_ACCOUNT_ID];
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
entry = store.currentAccountId ? store.accounts[store.currentAccountId] : void 0;
|
|
123
|
+
}
|
|
124
|
+
if (entry?.mcp_token) return { token: entry.mcp_token, source: "credentials-file" };
|
|
125
|
+
return { token: null, source: null };
|
|
126
|
+
}
|
|
127
|
+
function readTextOrNull(read, path4) {
|
|
128
|
+
try {
|
|
129
|
+
const raw = read(path4);
|
|
130
|
+
return typeof raw === "string" ? raw : null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function readLegacyConfigAccountId(read, homeDir) {
|
|
136
|
+
const raw = readTextOrNull(read, join2(homeDir, ".heyspex", "config.yaml"));
|
|
137
|
+
const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
|
|
138
|
+
return value || null;
|
|
139
|
+
}
|
|
140
|
+
function readBearer({ env = process.env, homeDir = heyspexHome(), readFile: readFile2, accountId: accountId2 } = {}) {
|
|
141
|
+
const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
|
|
142
|
+
const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
|
|
143
|
+
const named = typeof accountId2 === "string" && accountId2.trim() ? accountId2.trim() : null;
|
|
144
|
+
const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
|
|
145
|
+
return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
|
|
146
|
+
}
|
|
147
|
+
function readCurrentAccountId({ homeDir = heyspexHome(), readFile: readFile2 } = {}) {
|
|
148
|
+
const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
|
|
149
|
+
const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
|
|
150
|
+
if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
|
|
151
|
+
return store.currentAccountId;
|
|
152
|
+
}
|
|
153
|
+
if (store.schemaVersion === 2 && store.currentAccountId === null) return null;
|
|
154
|
+
return readLegacyConfigAccountId(read, homeDir);
|
|
155
|
+
}
|
|
156
|
+
function describeBearerSource(source) {
|
|
157
|
+
if (source === "env") return `${MCP_TOKEN_ENV} (env)`;
|
|
158
|
+
if (source === "credentials-file") return "~/.heyspex/credentials.yaml (mcp_token)";
|
|
159
|
+
return "none";
|
|
160
|
+
}
|
|
161
|
+
var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FIELDS, ACCOUNT_ID_RE, CONFIG_ACCOUNT_ID_RE;
|
|
162
|
+
var init_credentials = __esm({
|
|
163
|
+
"vendor/heyspex-mcp/credentials.mjs"() {
|
|
164
|
+
"use strict";
|
|
165
|
+
init_heyspex_home();
|
|
166
|
+
MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
|
|
167
|
+
MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
168
|
+
CREDENTIALS_SCHEMA_VERSION = 2;
|
|
169
|
+
LEGACY_ACCOUNT_ID = "legacy";
|
|
170
|
+
ACCOUNT_FIELDS = ["mcp_token", "console_url", "email", "paired_at"];
|
|
171
|
+
ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
|
|
172
|
+
CONFIG_ACCOUNT_ID_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// scripts/lib/dev-mode.mjs
|
|
177
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
178
|
+
import { homedir as homedir7 } from "node:os";
|
|
179
|
+
import { join as join8 } from "node:path";
|
|
180
|
+
function devModeFilePath(homeDir = heyspexHome()) {
|
|
181
|
+
return join8(homeDir, ".heyspex", DEV_MODE_FILENAME);
|
|
182
|
+
}
|
|
183
|
+
function normalizeMode(raw) {
|
|
184
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
185
|
+
if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
|
|
186
|
+
if (v === "dev") return { mode: "dev", reliance: null };
|
|
187
|
+
if (v === "local") return { mode: "dev", reliance: "local" };
|
|
188
|
+
if (v === "cloud") return { mode: "dev", reliance: "cloud" };
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
function normalizeReliance(raw) {
|
|
192
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
193
|
+
return v === "local" || v === "cloud" ? v : null;
|
|
194
|
+
}
|
|
195
|
+
function deriveSemantics(mode, reliance) {
|
|
196
|
+
const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
|
|
197
|
+
const local = mode === "dev" && effectiveReliance === "local";
|
|
198
|
+
return {
|
|
199
|
+
reliance: effectiveReliance,
|
|
200
|
+
consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
|
|
201
|
+
readTier: local ? "disk" : "neon",
|
|
202
|
+
vaultWriteMode: local ? "local" : "cloud"
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile: readFile2 = defaultReadFile } = {}) {
|
|
206
|
+
let raw;
|
|
207
|
+
try {
|
|
208
|
+
raw = readFile2(devModeFilePath(homeDir));
|
|
209
|
+
} catch {
|
|
210
|
+
return { record: null, parseError: null };
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(raw);
|
|
214
|
+
const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
|
|
215
|
+
const reliance = normalizeReliance(parsed?.reliance);
|
|
216
|
+
if (!mode || mode === "dev" && !reliance) {
|
|
217
|
+
return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
record: {
|
|
221
|
+
mode,
|
|
222
|
+
reliance: mode === "prod" ? "cloud" : reliance,
|
|
223
|
+
console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
|
|
224
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
|
|
225
|
+
source: typeof parsed.source === "string" ? parsed.source : null
|
|
226
|
+
},
|
|
227
|
+
parseError: null
|
|
228
|
+
};
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function resolveDevMode({
|
|
234
|
+
env = process.env,
|
|
235
|
+
homeDir = heyspexHome(),
|
|
236
|
+
readFile: readFile2 = defaultReadFile,
|
|
237
|
+
cloudSetting = void 0
|
|
238
|
+
} = {}) {
|
|
239
|
+
const local = readLocalDevModeRecord({ homeDir, readFile: readFile2 });
|
|
240
|
+
const cloudKnown = cloudSetting !== void 0;
|
|
241
|
+
const cloudValue = cloudKnown && cloudSetting ? {
|
|
242
|
+
mode: cloudSetting.mode === "dev" ? "dev" : "prod",
|
|
243
|
+
reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
|
|
244
|
+
} : null;
|
|
245
|
+
let mode;
|
|
246
|
+
let reliance;
|
|
247
|
+
let source;
|
|
248
|
+
let relianceSource;
|
|
249
|
+
const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
|
|
250
|
+
if (envParsed) {
|
|
251
|
+
mode = envParsed.mode;
|
|
252
|
+
const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
|
|
253
|
+
reliance = envReliance ?? "local";
|
|
254
|
+
source = "env";
|
|
255
|
+
relianceSource = envReliance ? "env" : "assumed-local";
|
|
256
|
+
} else if (local.record) {
|
|
257
|
+
mode = local.record.mode;
|
|
258
|
+
reliance = local.record.reliance;
|
|
259
|
+
source = "local-override";
|
|
260
|
+
} else if (cloudValue) {
|
|
261
|
+
mode = cloudValue.mode;
|
|
262
|
+
reliance = cloudValue.reliance;
|
|
263
|
+
source = "cloud-setting";
|
|
264
|
+
} else {
|
|
265
|
+
mode = "prod";
|
|
266
|
+
reliance = "cloud";
|
|
267
|
+
source = "default";
|
|
268
|
+
}
|
|
269
|
+
const semantics = deriveSemantics(mode, reliance);
|
|
270
|
+
const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
|
|
271
|
+
const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
|
|
272
|
+
return {
|
|
273
|
+
mode,
|
|
274
|
+
reliance: semantics.reliance,
|
|
275
|
+
consoleUrl,
|
|
276
|
+
readTier: semantics.readTier,
|
|
277
|
+
vaultWriteMode: semantics.vaultWriteMode,
|
|
278
|
+
sources: {
|
|
279
|
+
mode: source,
|
|
280
|
+
reliance: relianceSource ?? source,
|
|
281
|
+
consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
|
|
282
|
+
readTier: "derived",
|
|
283
|
+
vaultWriteMode: "derived"
|
|
284
|
+
},
|
|
285
|
+
localOverride: {
|
|
286
|
+
present: local.record !== null,
|
|
287
|
+
record: local.record,
|
|
288
|
+
parseError: local.parseError,
|
|
289
|
+
staleAgainstCloud
|
|
290
|
+
},
|
|
291
|
+
cloudSetting: { known: cloudKnown, value: cloudValue }
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
|
|
295
|
+
var init_dev_mode = __esm({
|
|
296
|
+
"scripts/lib/dev-mode.mjs"() {
|
|
297
|
+
"use strict";
|
|
298
|
+
init_heyspex_home();
|
|
299
|
+
init_credentials();
|
|
300
|
+
CLOUD_CONSOLE_URL = "https://app.heyspex.com";
|
|
301
|
+
LOCAL_CONSOLE_URL = "http://localhost:3000";
|
|
302
|
+
DEV_MODE_FILENAME = "dev-mode.json";
|
|
303
|
+
defaultReadFile = (p) => readFileSync7(p, "utf8");
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
8
307
|
// dist/cli/._sql-orm-inert.cli-shim.mjs
|
|
9
|
-
var inert, sql, and, eq, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
|
|
308
|
+
var inert, customType, sql, and, eq, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
|
|
10
309
|
var init_sql_orm_inert_cli_shim = __esm({
|
|
11
310
|
"dist/cli/._sql-orm-inert.cli-shim.mjs"() {
|
|
12
311
|
"use strict";
|
|
@@ -24,6 +323,7 @@ var init_sql_orm_inert_cli_shim = __esm({
|
|
|
24
323
|
return inert;
|
|
25
324
|
}
|
|
26
325
|
});
|
|
326
|
+
customType = inert;
|
|
27
327
|
sql = inert;
|
|
28
328
|
and = inert;
|
|
29
329
|
eq = inert;
|
|
@@ -69,7 +369,7 @@ var init_interfaces = __esm({
|
|
|
69
369
|
});
|
|
70
370
|
|
|
71
371
|
// spex-console/src/lib/db/schema/postgres.ts
|
|
72
|
-
var edgeTypeEnum, validationSeverityEnum, outboxOpEnum, specs, tags, edges, wikilinks, aliases, validationIssues, indexMeta, frontmatterFields, eventOutbox, captureSessions, handoffContext, claimEventKindEnum, activeClaimStatusEnum, pendingReasonEnum, focusEventKindEnum, specEventKindEnum, focusPin, captureFocusPin, captureFocusSelection, focusSelection, focusSkips, activeClaims, claimWaiters, claimEvents, focusEvents, focusAreas, focusAreasEvents, specEvents, rateLimitBuckets, userVaults, milestoneSnapshots, modelRegistryEntries, launchIntentStatusEnum, launchIntents, companionRelayPresence, companionSettingsStore, wipLedger, gatherResults, agentSessions, usageEvents, streamEvents, jobStreams, resolutionStatusEnum, resolutionFocusPin, resolutionLedger, workspaces, vaults, accountSettings, vaultGrants, githubInstallations, fleetMachines, fleetTasks, fleetResults, userApiKeys, apiKeys, mcpSessions, pairingCodes, companionSessions, inviteRequests, notifications, webhookIngestions, specSettlements;
|
|
372
|
+
var edgeTypeEnum, validationSeverityEnum, outboxOpEnum, tsvector, specs, tags, edges, wikilinks, specLinks, aliases, validationIssues, indexMeta, frontmatterFields, eventOutbox, captureSessions, handoffContext, claimEventKindEnum, activeClaimStatusEnum, pendingReasonEnum, focusEventKindEnum, specEventKindEnum, focusPin, captureFocusPin, captureFocusSelection, focusSelection, focusSkips, activeClaims, claimWaiters, fileClaims, claimEvents, focusEvents, focusAreas, focusAreasEvents, specEvents, rateLimitBuckets, userVaults, milestoneSnapshots, modelRegistryEntries, launchIntentStatusEnum, launchIntents, companionRelayPresence, companionSettingsStore, consoleSettingsStore, wipLedger, wipLedgerArchive, gatherResults, agentSessions, usageEvents, streamEvents, jobStreams, resolutionStatusEnum, resolutionFocusPin, resolutionLedger, workspaces, vaults, accountSettings, vaultGrants, pendingVaultGrants, githubInstallations, fleetMachines, fleetTasks, fleetResults, userApiKeys, apiKeys, mcpSessions, pairingCodes, companionSessions, inviteRequests, notifications, webhookIngestions, specSettlements, sessionBranchDeclarations;
|
|
73
373
|
var init_postgres = __esm({
|
|
74
374
|
"spex-console/src/lib/db/schema/postgres.ts"() {
|
|
75
375
|
"use strict";
|
|
@@ -84,6 +384,11 @@ var init_postgres = __esm({
|
|
|
84
384
|
]);
|
|
85
385
|
validationSeverityEnum = pgEnum("validation_severity", ["error", "warning"]);
|
|
86
386
|
outboxOpEnum = pgEnum("outbox_op", ["upsert", "delete"]);
|
|
387
|
+
tsvector = customType({
|
|
388
|
+
dataType() {
|
|
389
|
+
return "tsvector";
|
|
390
|
+
}
|
|
391
|
+
});
|
|
87
392
|
specs = pgTable(
|
|
88
393
|
"specs",
|
|
89
394
|
{
|
|
@@ -124,11 +429,47 @@ var init_postgres = __esm({
|
|
|
124
429
|
// `family:` field when present, else derives it via `familyOf` (parse
|
|
125
430
|
// fallback from `id`/basename). Nothing should string-parse family from `id`
|
|
126
431
|
// once this column is populated — see `dev-factory/handoffs/NAMING-V3-MIGRATION-BRIEF.md`.
|
|
127
|
-
family: text("family")
|
|
432
|
+
family: text("family"),
|
|
433
|
+
// Lexical search vector ([[HS-0E1G-8KK5-TASK-generated-search-vector-column-gin]]
|
|
434
|
+
// of [[HS-FI33-8KK5-EPIC-lexical-retrieval-in-postgres-generated]]; research:
|
|
435
|
+
// [[HS-HPEH-8KK5-RES-infinitely-scaling-spec-vault-nothing]] §Lexical retrieval).
|
|
436
|
+
// GENERATED ALWAYS … STORED: Postgres rewrites it on every INSERT/UPDATE of
|
|
437
|
+
// `name` / `body_text`, so the write path never touches it and it can never
|
|
438
|
+
// drift from the row. `coalesce` is mandatory — a NULL operand poisons the
|
|
439
|
+
// whole vector (NULL || tsvector = NULL). Title is weight A, body weight B so
|
|
440
|
+
// `ts_rank_cd` ranks a title hit above a body hit. Two-argument
|
|
441
|
+
// `to_tsvector('english', …)` is required inside a generated column (the
|
|
442
|
+
// one-argument form depends on a session setting and is not IMMUTABLE).
|
|
443
|
+
// Still lexical-only: no embeddings ([[retrieval-substrate-lexical-not-embeddings-gate-b-no]]).
|
|
444
|
+
searchVector: tsvector("search_vector").generatedAlwaysAs(
|
|
445
|
+
sql`setweight(to_tsvector('english', coalesce("name", '')), 'A') || setweight(to_tsvector('english', coalesce("body_text", '')), 'B')`
|
|
446
|
+
),
|
|
447
|
+
// Single-column tenant-qualified key ([[HS-ZUZF-8KK5-TASK-per-vault-database-routing-switch-and]]
|
|
448
|
+
// of [[HS-91S9-8KK5-EPIC-large-tenant-tier-bm25-search]]). The specs PRIMARY
|
|
449
|
+
// KEY is COMPOSITE (vault_id, id) — but ParadeDB `pg_search` (the BM25 task,
|
|
450
|
+
// [[HS-KZJ1-8KK5-TASK-bm25-pg-search-index-and-query-path]]) requires a
|
|
451
|
+
// SINGLE-column unique key for its index. `vault_id || ':' || id` is that
|
|
452
|
+
// key: both operands are NOT NULL so no `coalesce` is needed (unlike
|
|
453
|
+
// `search_vector` above), `||` on text is IMMUTABLE so it is legal in a
|
|
454
|
+
// GENERATED … STORED expression, and STORED means the write path never sets
|
|
455
|
+
// it and it can never drift from the row. The unique index below is the
|
|
456
|
+
// constraint pg_search will point at.
|
|
457
|
+
specKey: text("spec_key").generatedAlwaysAs(sql`"vault_id" || ':' || "id"`)
|
|
128
458
|
},
|
|
129
459
|
(table) => [
|
|
130
460
|
// Composite PK (vault_id, id): the same HS-XXXX id may exist once PER VAULT.
|
|
131
461
|
primaryKey({ columns: [table.vaultId, table.id] }),
|
|
462
|
+
// Full-text search: `search_vector @@ websearch_to_tsquery('english', $q)`
|
|
463
|
+
// answers from this GIN index instead of scanning bodies. Built plainly in
|
|
464
|
+
// the migration (scripts/migrate.mjs wraps each migration in a transaction,
|
|
465
|
+
// and CREATE INDEX CONCURRENTLY cannot run inside one); for a large live
|
|
466
|
+
// table use scripts/build-search-indexes-concurrently.mjs
|
|
467
|
+
// (docs/search-index-runbook.md).
|
|
468
|
+
index("specs_search_idx").using("gin", table.searchVector),
|
|
469
|
+
// Near-miss title lookups (`name % $q` / `similarity(name, $q)` / `ILIKE
|
|
470
|
+
// '%term%'`) via pg_trgm. Needs `CREATE EXTENSION pg_trgm` — hand-added to
|
|
471
|
+
// the same migration because drizzle-kit cannot express extensions.
|
|
472
|
+
index("specs_name_trgm_idx").using("gin", table.name.op("gin_trgm_ops")),
|
|
132
473
|
// SpexConsole main list views filter by type + status
|
|
133
474
|
index("specs_type_status_idx").on(table.type, table.status),
|
|
134
475
|
// Family-graph lookups (Naming V3 step 1) — group specs by family key.
|
|
@@ -141,7 +482,22 @@ var init_postgres = __esm({
|
|
|
141
482
|
// (deleted_at IS NULL) so soft-deleted orphans don't block re-indexing. The
|
|
142
483
|
// upsert path soft-deletes any conflicting row first. Scoped by vault so the
|
|
143
484
|
// same rel_path can be indexed independently under two vaults.
|
|
144
|
-
uniqueIndex("specs_relpath_user_uq").on(table.relPath, table.userId, table.vaultId).where(sql`${table.deletedAt} IS NULL`)
|
|
485
|
+
uniqueIndex("specs_relpath_user_uq").on(table.relPath, table.userId, table.vaultId).where(sql`${table.deletedAt} IS NULL`),
|
|
486
|
+
// Keyset paging ([[HS-C6FO-8KK5-TASK-keyset-spec-list-endpoint-query-layer]]):
|
|
487
|
+
// the immutable sort key is (created, id) — never `updated_at`, which moves
|
|
488
|
+
// edited rows between pages. Partial (deleted_at IS NULL) so soft-deleted
|
|
489
|
+
// rows never cost the index a page. Unfiltered list-query root.
|
|
490
|
+
index("specs_vault_created_id_idx").on(table.vaultId, table.created, table.id).where(sql`${table.deletedAt} IS NULL`),
|
|
491
|
+
// Same keyset shape, pre-filtered by type — the Docs list's most common
|
|
492
|
+
// filter (e.g. `type=task`).
|
|
493
|
+
index("specs_vault_type_created_id_idx").on(table.vaultId, table.type, table.created, table.id).where(sql`${table.deletedAt} IS NULL`),
|
|
494
|
+
// Same keyset shape, pre-filtered by status — the Docs list's other common
|
|
495
|
+
// filter (e.g. `status=open`).
|
|
496
|
+
index("specs_vault_status_created_id_idx").on(table.vaultId, table.status, table.created, table.id).where(sql`${table.deletedAt} IS NULL`),
|
|
497
|
+
// The single-column unique constraint `pg_search` needs (see `specKey`).
|
|
498
|
+
// NOT partial: pg_search indexes the whole table, and a partial unique
|
|
499
|
+
// index would not qualify as its key.
|
|
500
|
+
uniqueIndex("specs_spec_key_uq").on(table.specKey)
|
|
145
501
|
]
|
|
146
502
|
);
|
|
147
503
|
tags = pgTable(
|
|
@@ -221,6 +577,30 @@ var init_postgres = __esm({
|
|
|
221
577
|
index("wikilinks_target_idx").on(table.targetBasename)
|
|
222
578
|
]
|
|
223
579
|
);
|
|
580
|
+
specLinks = pgTable(
|
|
581
|
+
"spec_links",
|
|
582
|
+
{
|
|
583
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
584
|
+
vaultId: text("vault_id").notNull(),
|
|
585
|
+
subjectId: text("subject_id").notNull(),
|
|
586
|
+
objectId: text("object_id"),
|
|
587
|
+
objectKey: text("object_key").notNull(),
|
|
588
|
+
kind: text("kind").notNull().default("wikilink").$type(),
|
|
589
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
590
|
+
},
|
|
591
|
+
(table) => [
|
|
592
|
+
foreignKey({
|
|
593
|
+
columns: [table.vaultId, table.subjectId],
|
|
594
|
+
foreignColumns: [specs.vaultId, specs.id]
|
|
595
|
+
}).onDelete("cascade"),
|
|
596
|
+
// Forward refs: what does this spec point at?
|
|
597
|
+
index("spec_links_subject_idx").on(table.vaultId, table.subjectId, table.id),
|
|
598
|
+
// Backlinks: what points at this spec?
|
|
599
|
+
index("spec_links_object_idx").on(table.vaultId, table.objectId, table.id),
|
|
600
|
+
// Backlinks by basename (unresolved forward references / rename survival).
|
|
601
|
+
index("spec_links_object_key_idx").on(table.vaultId, table.objectKey, table.id)
|
|
602
|
+
]
|
|
603
|
+
);
|
|
224
604
|
aliases = pgTable(
|
|
225
605
|
"aliases",
|
|
226
606
|
{
|
|
@@ -431,7 +811,13 @@ var init_postgres = __esm({
|
|
|
431
811
|
"priority_change",
|
|
432
812
|
"parent_change",
|
|
433
813
|
"claim_acquired",
|
|
434
|
-
"claim_released"
|
|
814
|
+
"claim_released",
|
|
815
|
+
// Collaborator write audit (HS-FWB6, migration 0077).
|
|
816
|
+
"spec_created",
|
|
817
|
+
"spec_edited",
|
|
818
|
+
"spec_deleted",
|
|
819
|
+
"capture_written",
|
|
820
|
+
"wip_recorded"
|
|
435
821
|
]);
|
|
436
822
|
focusPin = pgTable(
|
|
437
823
|
"focus_pin",
|
|
@@ -624,6 +1010,38 @@ var init_postgres = __esm({
|
|
|
624
1010
|
)
|
|
625
1011
|
]
|
|
626
1012
|
);
|
|
1013
|
+
fileClaims = pgTable(
|
|
1014
|
+
"file_claims",
|
|
1015
|
+
{
|
|
1016
|
+
// Tenant-isolation scope ([[HS-ZP5H-TASK]]) — mirrors `active_claims.vault_id`.
|
|
1017
|
+
vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
|
|
1018
|
+
filePath: text("file_path").notNull(),
|
|
1019
|
+
// Persistent human accountable for this lease ([[HS-LLBO-ALE0-RES-multi-user-teams-on-a-shared-spec-vault]]).
|
|
1020
|
+
// Resolved from the caller's vault scope at the call site — never a
|
|
1021
|
+
// literal `SINGLE_USER_ID` baked in here. Distinct column from `agent_id`
|
|
1022
|
+
// below: the agent is the ephemeral worker, never the owner.
|
|
1023
|
+
ownerUserId: text("owner_user_id").notNull().default(SINGLE_USER_ID),
|
|
1024
|
+
agentId: text("agent_id").notNull(),
|
|
1025
|
+
sessionId: text("session_id").notNull(),
|
|
1026
|
+
// Spec id the agent is working on when claiming this file, if any.
|
|
1027
|
+
// Informational only — mirrors `FileClaim.workingSpecId`.
|
|
1028
|
+
workingSpecId: text("working_spec_id"),
|
|
1029
|
+
claimedAt: timestamp("claimed_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1030
|
+
// Reuses `active_claim_status` — same value domain as the file-side
|
|
1031
|
+
// `LeaseStatus` union ("working" | "pending" | "blocked").
|
|
1032
|
+
status: activeClaimStatusEnum("status").notNull().default("working"),
|
|
1033
|
+
lastActivityAt: timestamp("last_activity_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1034
|
+
heartbeatAt: timestamp("heartbeat_at", { withTimezone: true }),
|
|
1035
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
1036
|
+
// Shared global order: DEFAULT nextval on the SAME sequence `active_claims`
|
|
1037
|
+
// and `claim_waiters` mint from — not a per-table bigserial, which would
|
|
1038
|
+
// mint a second, unrelated counter and defeat the shared fencing contract.
|
|
1039
|
+
sequence: bigint("sequence", { mode: "number" }).notNull().unique().default(sql`nextval('active_claims_sequence_seq')`),
|
|
1040
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1041
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
1042
|
+
},
|
|
1043
|
+
(table) => [primaryKey({ columns: [table.vaultId, table.filePath] })]
|
|
1044
|
+
);
|
|
627
1045
|
claimEvents = pgTable(
|
|
628
1046
|
"claim_events",
|
|
629
1047
|
{
|
|
@@ -750,6 +1168,7 @@ var init_postgres = __esm({
|
|
|
750
1168
|
"rate_limit_buckets",
|
|
751
1169
|
{
|
|
752
1170
|
userId: text("user_id").notNull(),
|
|
1171
|
+
vaultId: text("vault_id").notNull().default(""),
|
|
753
1172
|
bucket: text("bucket").notNull(),
|
|
754
1173
|
windowStart: timestamp("window_start", { withTimezone: true }).notNull(),
|
|
755
1174
|
count: integer("count").notNull().default(0),
|
|
@@ -757,7 +1176,7 @@ var init_postgres = __esm({
|
|
|
757
1176
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
758
1177
|
},
|
|
759
1178
|
(table) => [
|
|
760
|
-
primaryKey({ columns: [table.userId, table.bucket, table.windowStart] }),
|
|
1179
|
+
primaryKey({ columns: [table.userId, table.vaultId, table.bucket, table.windowStart] }),
|
|
761
1180
|
// Background reaper / dashboards: enumerate stale rows by `window_start`
|
|
762
1181
|
// older than the longest live window. The hot path uses the composite PK.
|
|
763
1182
|
index("rate_limit_buckets_window_idx").on(table.windowStart)
|
|
@@ -889,6 +1308,17 @@ var init_postgres = __esm({
|
|
|
889
1308
|
},
|
|
890
1309
|
(table) => [primaryKey({ columns: [table.userId, table.vaultId] })]
|
|
891
1310
|
);
|
|
1311
|
+
consoleSettingsStore = pgTable(
|
|
1312
|
+
"console_settings",
|
|
1313
|
+
{
|
|
1314
|
+
userId: text("user_id").notNull().default(SINGLE_USER_ID),
|
|
1315
|
+
vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
|
|
1316
|
+
namespace: text("namespace").notNull(),
|
|
1317
|
+
payload: jsonb("payload").notNull(),
|
|
1318
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
1319
|
+
},
|
|
1320
|
+
(table) => [primaryKey({ columns: [table.userId, table.vaultId, table.namespace] })]
|
|
1321
|
+
);
|
|
892
1322
|
wipLedger = pgTable(
|
|
893
1323
|
"wip_ledger",
|
|
894
1324
|
{
|
|
@@ -901,6 +1331,27 @@ var init_postgres = __esm({
|
|
|
901
1331
|
},
|
|
902
1332
|
(table) => [primaryKey({ columns: [table.vaultId, table.specId] })]
|
|
903
1333
|
);
|
|
1334
|
+
wipLedgerArchive = pgTable(
|
|
1335
|
+
"wip_ledger_archive",
|
|
1336
|
+
{
|
|
1337
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
1338
|
+
vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
|
|
1339
|
+
specId: text("spec_id").notNull(),
|
|
1340
|
+
/** The full WipEntry blob at the moment it was archived — see `WipEntry` in `src/lib/wip-state.ts`. */
|
|
1341
|
+
payload: jsonb("payload").notNull(),
|
|
1342
|
+
archivedAt: timestamp("archived_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
1343
|
+
},
|
|
1344
|
+
(table) => [
|
|
1345
|
+
// "newest archived record for this (vault, spec)" — the restore lookup.
|
|
1346
|
+
index("wip_ledger_archive_vault_spec_idx").on(
|
|
1347
|
+
table.vaultId,
|
|
1348
|
+
table.specId,
|
|
1349
|
+
table.archivedAt.desc()
|
|
1350
|
+
),
|
|
1351
|
+
// The purge cron's own scan: "every row older than a cutoff, any vault".
|
|
1352
|
+
index("wip_ledger_archive_archived_at_idx").on(table.archivedAt)
|
|
1353
|
+
]
|
|
1354
|
+
);
|
|
904
1355
|
gatherResults = pgTable(
|
|
905
1356
|
"gather_results",
|
|
906
1357
|
{
|
|
@@ -918,6 +1369,16 @@ var init_postgres = __esm({
|
|
|
918
1369
|
* ([[HS-RIZN-DIL5-DIAG-spexcrawl-search-returns-zero-pages-on]]).
|
|
919
1370
|
*/
|
|
920
1371
|
search: jsonb("search"),
|
|
1372
|
+
/**
|
|
1373
|
+
* A completed site walk's duplicate/orphan audit — which pages say
|
|
1374
|
+
* exactly the same thing as each other, and which pages nothing else
|
|
1375
|
+
* links to ([[HS-4TNX-ABF9-TASK-carry-the-site-audit-report-from-a]]).
|
|
1376
|
+
* The second job-level report beside `pages`, carried exactly as
|
|
1377
|
+
* `search` is: open shape (the Companion owns the fields), null for a
|
|
1378
|
+
* single-page gather, a research job, and uploads from Companions that
|
|
1379
|
+
* predate the report.
|
|
1380
|
+
*/
|
|
1381
|
+
siteAudit: jsonb("site_audit"),
|
|
921
1382
|
createdAt: timestamp("created_at", { withTimezone: true }).default(sql`now()`)
|
|
922
1383
|
},
|
|
923
1384
|
(table) => [primaryKey({ columns: [table.jobId, table.idempotencyKey] })]
|
|
@@ -1100,6 +1561,44 @@ var init_postgres = __esm({
|
|
|
1100
1561
|
repoRootAbs: text("repo_root_abs"),
|
|
1101
1562
|
githubRemote: text("github_remote"),
|
|
1102
1563
|
schemaVersion: integer("schema_version").notNull().default(1),
|
|
1564
|
+
/**
|
|
1565
|
+
* Price plan the vault is on — `free` | `pro` | `enterprise`, the same
|
|
1566
|
+
* vocabulary as `ApiKeyPlan` in `api-key-rate-limit.ts`
|
|
1567
|
+
* ([[HS-JRER-8KK5-TASK-per-vault-plan-tier-tier-limits-table]]). Drives the
|
|
1568
|
+
* per-tier statement timeout / work_mem / rate-limit budgets in
|
|
1569
|
+
* `src/lib/plan-tier.ts` (`PLAN_TIER_LIMITS`, `resolveVaultPlanTier`). How a
|
|
1570
|
+
* vault GETS a tier (billing) is out of scope — the column defaults every
|
|
1571
|
+
* vault to `free` so the most conservative limits apply until a tier is set.
|
|
1572
|
+
* Readers must treat an unknown value as `free`.
|
|
1573
|
+
*/
|
|
1574
|
+
planTier: text("plan_tier").notNull().default("free"),
|
|
1575
|
+
/**
|
|
1576
|
+
* Dedicated-database routing key ([[HS-ZUZF-8KK5-TASK-per-vault-database-routing-switch-and]]).
|
|
1577
|
+
*
|
|
1578
|
+
* NULL (the default, and every vault today) = this vault lives in the SHARED
|
|
1579
|
+
* database, i.e. the one `DATABASE_URL` points at. A non-null value is a
|
|
1580
|
+
* short opaque REF — never a connection string and never a secret. The ref
|
|
1581
|
+
* NAMES where the credential lives: environment variable
|
|
1582
|
+
* `HEYSPEX_DEDICATED_DB_<REF>` (uppercased, non-alphanumerics → `_`), or
|
|
1583
|
+
* `dedicated_databases.<ref>` in `~/.heyspex/credentials.yaml`. Resolution
|
|
1584
|
+
* and the per-ref pool memo live in `src/lib/db/clients/vault-router.ts`;
|
|
1585
|
+
* `scripts/migrate.mjs` migrates every ref listed in `DEDICATED_DATABASE_REFS`
|
|
1586
|
+
* alongside the shared database.
|
|
1587
|
+
*
|
|
1588
|
+
* The `vaults` table itself ALWAYS stays on the shared control plane — the
|
|
1589
|
+
* router reads this column through the admin connection, so the row that says
|
|
1590
|
+
* "vault X is elsewhere" is never itself elsewhere.
|
|
1591
|
+
*/
|
|
1592
|
+
databaseRef: text("database_ref"),
|
|
1593
|
+
/**
|
|
1594
|
+
* Which lexical-search substrate this vault's queries use — `gin` (the
|
|
1595
|
+
* built-in `search_vector` GIN index shipped in migration 0068) or `bm25`
|
|
1596
|
+
* (ParadeDB `pg_search`, evaluated by
|
|
1597
|
+
* [[HS-KZJ1-8KK5-TASK-bm25-pg-search-index-and-query-path]]). Declared here
|
|
1598
|
+
* so the BM25 task is a per-vault flag flip rather than a global cutover;
|
|
1599
|
+
* nothing reads it yet. Readers must treat an unknown value as `gin`.
|
|
1600
|
+
*/
|
|
1601
|
+
searchEngine: text("search_engine").notNull().default("gin"),
|
|
1103
1602
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1104
1603
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1105
1604
|
/** Vault equivalent of `workspaces.archived` — see that column's doc comment. */
|
|
@@ -1131,6 +1630,22 @@ var init_postgres = __esm({
|
|
|
1131
1630
|
},
|
|
1132
1631
|
(table) => [primaryKey({ columns: [table.vaultId, table.accountId] })]
|
|
1133
1632
|
);
|
|
1633
|
+
pendingVaultGrants = pgTable(
|
|
1634
|
+
"pending_vault_grants",
|
|
1635
|
+
{
|
|
1636
|
+
id: text("id").primaryKey(),
|
|
1637
|
+
vaultId: text("vault_id").notNull().references(() => vaults.id, { onDelete: "cascade" }),
|
|
1638
|
+
email: text("email").notNull(),
|
|
1639
|
+
access: text("access").notNull().default("read"),
|
|
1640
|
+
invitedBy: text("invited_by").notNull(),
|
|
1641
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1642
|
+
fulfilledAt: timestamp("fulfilled_at", { withTimezone: true }),
|
|
1643
|
+
fulfilledAccountId: text("fulfilled_account_id")
|
|
1644
|
+
},
|
|
1645
|
+
(table) => [
|
|
1646
|
+
uniqueIndex("pending_vault_grants_vault_id_email_idx").on(table.vaultId, table.email)
|
|
1647
|
+
]
|
|
1648
|
+
);
|
|
1134
1649
|
githubInstallations = pgTable(
|
|
1135
1650
|
"github_installations",
|
|
1136
1651
|
{
|
|
@@ -1361,6 +1876,31 @@ var init_postgres = __esm({
|
|
|
1361
1876
|
// new | invited | declined
|
|
1362
1877
|
/** Clerk invitation id, set when an approval actually issues one. */
|
|
1363
1878
|
clerkInvitationId: text("clerk_invitation_id"),
|
|
1879
|
+
/**
|
|
1880
|
+
* The Clerk invitation's accept URL, stored alongside the id at approve
|
|
1881
|
+
* time so the admin surface can show/copy/resend it without a live Clerk
|
|
1882
|
+
* round trip for every render
|
|
1883
|
+
* ([[HS-7DKO-JQ64-TASK-approve-sends-the-customer-a-resend]]).
|
|
1884
|
+
*/
|
|
1885
|
+
clerkInvitationUrl: text("clerk_invitation_url"),
|
|
1886
|
+
/** When the first-party approval email (Resend) was confirmed sent. Null until it succeeds. */
|
|
1887
|
+
approvalEmailSentAt: timestamp("approval_email_sent_at", { withTimezone: true }),
|
|
1888
|
+
/** Why the approval email failed or was skipped — never blocks approval, just recorded. */
|
|
1889
|
+
approvalEmailError: text("approval_email_error"),
|
|
1890
|
+
/**
|
|
1891
|
+
* Resend's message id for the approval email currently on the row — the
|
|
1892
|
+
* key a later `POST /api/webhooks/resend` delivery matches back to this
|
|
1893
|
+
* row ([[HS-HUK3-JQ64-TASK-approval-email-delivery-proof-store-the]]).
|
|
1894
|
+
* Reset (and re-set) on every approve/resend, since each send mints a new
|
|
1895
|
+
* Resend message id.
|
|
1896
|
+
*/
|
|
1897
|
+
approvalEmailId: text("approval_email_id"),
|
|
1898
|
+
/** Resend delivery-webhook outcome: sent | delayed | delivered | bounced | complained. Null until a webhook arrives. */
|
|
1899
|
+
approvalEmailDeliveryStatus: text("approval_email_delivery_status"),
|
|
1900
|
+
/** When the most recently applied delivery-webhook event was recorded. */
|
|
1901
|
+
approvalEmailDeliveryAt: timestamp("approval_email_delivery_at", { withTimezone: true }),
|
|
1902
|
+
/** Bounce/complaint detail from the webhook payload (e.g. the bounce message). */
|
|
1903
|
+
approvalEmailDeliveryDetail: text("approval_email_delivery_detail"),
|
|
1364
1904
|
/** Account id of the admin who approved/declined — audit trail for the decision. */
|
|
1365
1905
|
reviewedBy: text("reviewed_by"),
|
|
1366
1906
|
reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
|
|
@@ -1470,140 +2010,200 @@ var init_postgres = __esm({
|
|
|
1470
2010
|
// `owner/name`
|
|
1471
2011
|
branch: text("branch").notNull(),
|
|
1472
2012
|
commitSha: text("commit_sha").notNull(),
|
|
1473
|
-
settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
2013
|
+
settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
2014
|
+
// The pull request that carries this settlement
|
|
2015
|
+
// ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
|
|
2016
|
+
// the branch has gone quiet and a PR was opened or found; a write made
|
|
2017
|
+
// after its PR merged stays NULL, which is what triggers the next PR.
|
|
2018
|
+
pullRequestNumber: integer("pull_request_number"),
|
|
2019
|
+
pullRequestUrl: text("pull_request_url")
|
|
1474
2020
|
},
|
|
1475
2021
|
(table) => [
|
|
1476
2022
|
// "latest settlement for this spec" — the read path's only query.
|
|
1477
|
-
index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
|
|
2023
|
+
index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
|
|
2024
|
+
// The PR sweep's only query: branches with settlements not yet in a PR.
|
|
2025
|
+
index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
|
|
2026
|
+
]
|
|
2027
|
+
);
|
|
2028
|
+
sessionBranchDeclarations = pgTable(
|
|
2029
|
+
"session_branch_declarations",
|
|
2030
|
+
{
|
|
2031
|
+
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
|
2032
|
+
vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
|
|
2033
|
+
actorKey: text("actor_key").notNull(),
|
|
2034
|
+
sessionId: text("session_id").notNull(),
|
|
2035
|
+
branch: text("branch").notNull(),
|
|
2036
|
+
declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
2037
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
|
|
2038
|
+
},
|
|
2039
|
+
(table) => [
|
|
2040
|
+
uniqueIndex("session_branch_declarations_uq").on(
|
|
2041
|
+
table.vaultId,
|
|
2042
|
+
table.actorKey,
|
|
2043
|
+
table.sessionId
|
|
2044
|
+
),
|
|
2045
|
+
index("session_branch_declarations_lookup_idx").on(
|
|
2046
|
+
table.vaultId,
|
|
2047
|
+
table.actorKey,
|
|
2048
|
+
table.declaredAt.desc()
|
|
2049
|
+
)
|
|
1478
2050
|
]
|
|
1479
2051
|
);
|
|
1480
2052
|
}
|
|
1481
2053
|
});
|
|
1482
2054
|
|
|
1483
|
-
// spex-console/
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1499
|
-
function sleep(ms) {
|
|
1500
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1501
|
-
}
|
|
1502
|
-
function emitProbeFailure(options, failure) {
|
|
2055
|
+
// spex-console/bin/hs-workspace.ts
|
|
2056
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
2057
|
+
import path3 from "node:path";
|
|
2058
|
+
import process4 from "node:process";
|
|
2059
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
2060
|
+
|
|
2061
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
2062
|
+
import path from "node:path";
|
|
2063
|
+
|
|
2064
|
+
// scripts/lib/hs-command.mjs
|
|
2065
|
+
import { existsSync } from "node:fs";
|
|
2066
|
+
import { delimiter, dirname, join } from "node:path";
|
|
2067
|
+
import { fileURLToPath } from "node:url";
|
|
2068
|
+
var HS_BIN_NAME = "hs";
|
|
2069
|
+
var HS_DISPATCHER_PATH = (() => {
|
|
1503
2070
|
try {
|
|
1504
|
-
|
|
2071
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
2072
|
+
return join(dirname(dirname(here)), "bin", "hs.mjs");
|
|
1505
2073
|
} catch {
|
|
2074
|
+
return null;
|
|
1506
2075
|
}
|
|
2076
|
+
})();
|
|
2077
|
+
function shellQuote(p) {
|
|
2078
|
+
return /[\\\s]/.test(p) ? `"${p}"` : p;
|
|
1507
2079
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
attempt: i + 1,
|
|
1528
|
-
attempts,
|
|
1529
|
-
timeoutMs,
|
|
1530
|
-
elapsedMs,
|
|
1531
|
-
willRetry: true,
|
|
1532
|
-
error: err
|
|
1533
|
-
});
|
|
1534
|
-
startedAt = Date.now();
|
|
1535
|
-
i -= 1;
|
|
1536
|
-
continue;
|
|
1537
|
-
}
|
|
1538
|
-
const isLast = i === attempts - 1;
|
|
1539
|
-
const pastDeadline = Date.now() - startedAt >= deadlineMs;
|
|
1540
|
-
const willRetry = !isLast && !pastDeadline;
|
|
1541
|
-
emitProbeFailure(options, {
|
|
1542
|
-
reason: err instanceof NeonProbeTimeoutError ? "timeout" : "error",
|
|
1543
|
-
attempt: i + 1,
|
|
1544
|
-
attempts,
|
|
1545
|
-
timeoutMs,
|
|
1546
|
-
elapsedMs,
|
|
1547
|
-
willRetry,
|
|
1548
|
-
error: err
|
|
1549
|
-
});
|
|
1550
|
-
if (!willRetry) return false;
|
|
1551
|
-
await sleep(backoffMs[i]);
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
return false;
|
|
2080
|
+
var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
|
|
2081
|
+
|
|
2082
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
2083
|
+
init_heyspex_home();
|
|
2084
|
+
init_credentials();
|
|
2085
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2086
|
+
import { homedir as homedir5 } from "node:os";
|
|
2087
|
+
import { join as join6 } from "node:path";
|
|
2088
|
+
|
|
2089
|
+
// vendor/heyspex-mcp/session-owner.mjs
|
|
2090
|
+
init_heyspex_home();
|
|
2091
|
+
init_credentials();
|
|
2092
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2093
|
+
import { homedir as homedir3 } from "node:os";
|
|
2094
|
+
import { join as join3 } from "node:path";
|
|
2095
|
+
function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile: readFile2 } = {}) {
|
|
2096
|
+
const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
|
|
2097
|
+
if (fromEnv) return fromEnv;
|
|
2098
|
+
return readCurrentAccountId({ homeDir, readFile: readFile2 });
|
|
1555
2099
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
2100
|
+
|
|
2101
|
+
// vendor/heyspex-mcp/vault-less-endpoints.mjs
|
|
2102
|
+
var VAULT_LESS_ENDPOINTS = Object.freeze([
|
|
2103
|
+
"* /api/mcp/session",
|
|
2104
|
+
"* /api/mcp/pairing-codes",
|
|
2105
|
+
"POST /api/vaults",
|
|
2106
|
+
"GET /api/vaults",
|
|
2107
|
+
"POST /api/workspaces",
|
|
2108
|
+
"GET /api/workspaces",
|
|
2109
|
+
"* /api/admin/dev-mode"
|
|
2110
|
+
]);
|
|
2111
|
+
function pathnameOf(path4) {
|
|
2112
|
+
const raw = String(path4 ?? "").split("?")[0].split("#")[0];
|
|
2113
|
+
const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
|
|
2114
|
+
return noTrailing || "/";
|
|
2115
|
+
}
|
|
2116
|
+
function isVaultLessEndpoint(method, path4) {
|
|
2117
|
+
const m = String(method || "GET").toUpperCase();
|
|
2118
|
+
const pathname = pathnameOf(path4);
|
|
2119
|
+
return VAULT_LESS_ENDPOINTS.some((row) => {
|
|
2120
|
+
const [rowMethod, rowPath] = row.split(" ");
|
|
2121
|
+
return rowPath === pathname && (rowMethod === "*" || rowMethod === m);
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// vendor/heyspex-mcp/write-outbox.mjs
|
|
2126
|
+
import {
|
|
2127
|
+
readdirSync,
|
|
2128
|
+
readFileSync as readFileSync3,
|
|
2129
|
+
writeFileSync as writeFileSync2,
|
|
2130
|
+
mkdirSync as mkdirSync2,
|
|
2131
|
+
existsSync as existsSync2,
|
|
2132
|
+
renameSync as renameSync2,
|
|
2133
|
+
rmSync
|
|
2134
|
+
} from "node:fs";
|
|
2135
|
+
import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
|
|
2136
|
+
|
|
2137
|
+
// scripts/lib/shared-path-helpers.mjs
|
|
2138
|
+
import { isAbsolute, resolve as resolve2, sep } from "node:path";
|
|
2139
|
+
function resolveCodeRoot({
|
|
2140
|
+
explicit,
|
|
2141
|
+
packageRoot,
|
|
2142
|
+
vaultRoot,
|
|
2143
|
+
env = process.env
|
|
2144
|
+
} = {}) {
|
|
2145
|
+
const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
|
|
2146
|
+
if (fromExplicit) return fromExplicit;
|
|
2147
|
+
const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
|
|
2148
|
+
if (fromEnv) return fromEnv;
|
|
2149
|
+
const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
|
|
2150
|
+
if (fromPackage) return fromPackage;
|
|
2151
|
+
const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
|
|
2152
|
+
return fromVault;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
2156
|
+
init_heyspex_home();
|
|
2157
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
2158
|
+
import { homedir as homedir4, tmpdir } from "node:os";
|
|
2159
|
+
import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
|
|
2160
|
+
|
|
2161
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
2162
|
+
var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
|
|
2163
|
+
|
|
2164
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
2165
|
+
init_credentials();
|
|
2166
|
+
|
|
2167
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
2168
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2169
|
+
import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
|
|
2170
|
+
|
|
2171
|
+
// spex-console/src/lib/heyspex-home.ts
|
|
2172
|
+
import { homedir as homedir6 } from "node:os";
|
|
2173
|
+
function heyspexHome2(env = process.env) {
|
|
2174
|
+
const override = env?.HEYSPEX_HOME;
|
|
2175
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
2176
|
+
return override.trim();
|
|
1573
2177
|
}
|
|
1574
|
-
|
|
2178
|
+
return homedir6();
|
|
2179
|
+
}
|
|
1575
2180
|
|
|
1576
2181
|
// spex-console/src/lib/vault-registry/types.ts
|
|
2182
|
+
var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
|
|
2183
|
+
var VAULT_REGISTRY_SCHEMA_VERSION = 2;
|
|
1577
2184
|
function emptyVaultRegistry() {
|
|
1578
2185
|
return {
|
|
1579
2186
|
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
2187
|
+
current_account_id: null,
|
|
2188
|
+
accounts: {},
|
|
1580
2189
|
workspaces: [],
|
|
1581
|
-
vaults: []
|
|
1582
|
-
active_workspace_id: null,
|
|
1583
|
-
active_vault_id: null
|
|
2190
|
+
vaults: []
|
|
1584
2191
|
};
|
|
1585
2192
|
}
|
|
1586
|
-
var VAULT_REGISTRY_SCHEMA_VERSION;
|
|
1587
|
-
var init_types = __esm({
|
|
1588
|
-
"spex-console/src/lib/vault-registry/types.ts"() {
|
|
1589
|
-
"use strict";
|
|
1590
|
-
VAULT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1591
|
-
}
|
|
1592
|
-
});
|
|
1593
2193
|
|
|
1594
2194
|
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
1595
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync4, renameSync, writeFileSync } from "node:fs";
|
|
1596
|
-
import { homedir as homedir3 } from "node:os";
|
|
1597
|
-
import { dirname, join as join3 } from "node:path";
|
|
1598
2195
|
function vaultRegistryDir(opts = {}) {
|
|
1599
|
-
return opts.registryDir ??
|
|
2196
|
+
return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
|
|
1600
2197
|
}
|
|
1601
2198
|
function vaultRegistryFilePath(opts = {}) {
|
|
1602
|
-
return
|
|
2199
|
+
return join7(vaultRegistryDir(opts), "registry.json");
|
|
1603
2200
|
}
|
|
1604
2201
|
function isNonEmptyString(v) {
|
|
1605
2202
|
return typeof v === "string" && v.trim().length > 0;
|
|
1606
2203
|
}
|
|
2204
|
+
function accountOf(o) {
|
|
2205
|
+
return isNonEmptyString(o.account_id) ? o.account_id : LEGACY_REGISTRY_ACCOUNT_ID;
|
|
2206
|
+
}
|
|
1607
2207
|
function normalizeWorkspace(raw) {
|
|
1608
2208
|
if (!raw || typeof raw !== "object") return null;
|
|
1609
2209
|
const o = raw;
|
|
@@ -1614,7 +2214,8 @@ function normalizeWorkspace(raw) {
|
|
|
1614
2214
|
id: o.id,
|
|
1615
2215
|
display_name: o.display_name,
|
|
1616
2216
|
created_at: o.created_at,
|
|
1617
|
-
archived: o.archived === true
|
|
2217
|
+
archived: o.archived === true,
|
|
2218
|
+
account_id: accountOf(o)
|
|
1618
2219
|
};
|
|
1619
2220
|
if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
|
|
1620
2221
|
return record;
|
|
@@ -1635,46 +2236,143 @@ function normalizeVault(raw) {
|
|
|
1635
2236
|
repo_root_abs: o.repo_root_abs,
|
|
1636
2237
|
schema_version: o.schema_version,
|
|
1637
2238
|
registered_at: o.registered_at,
|
|
1638
|
-
archived: o.archived === true
|
|
2239
|
+
archived: o.archived === true,
|
|
2240
|
+
account_id: accountOf(o)
|
|
1639
2241
|
};
|
|
1640
2242
|
if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
|
|
1641
2243
|
return record;
|
|
1642
2244
|
}
|
|
2245
|
+
function normalizeStoredPointers(raw) {
|
|
2246
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
2247
|
+
const out = {};
|
|
2248
|
+
for (const field of ["active_vault_id", "active_workspace_id"]) {
|
|
2249
|
+
const v = o[field];
|
|
2250
|
+
if (v === null || isNonEmptyString(v)) out[field] = v;
|
|
2251
|
+
}
|
|
2252
|
+
return out;
|
|
2253
|
+
}
|
|
2254
|
+
function normalizePointers(raw) {
|
|
2255
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
2256
|
+
return {
|
|
2257
|
+
active_vault_id: isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null,
|
|
2258
|
+
active_workspace_id: isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
function registryFileSchemaOf(json) {
|
|
2262
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) return null;
|
|
2263
|
+
return json.schema_version === 2 ? 2 : 1;
|
|
2264
|
+
}
|
|
1643
2265
|
function parseVaultRegistryFile(json) {
|
|
1644
|
-
const
|
|
1645
|
-
if (
|
|
2266
|
+
const schema = registryFileSchemaOf(json);
|
|
2267
|
+
if (schema === null) return emptyVaultRegistry();
|
|
1646
2268
|
const o = json;
|
|
1647
2269
|
const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
|
|
1648
2270
|
const workspaces2 = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
|
|
1649
2271
|
const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
|
|
1650
2272
|
const vaults2 = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
|
|
1651
|
-
|
|
1652
|
-
|
|
2273
|
+
if (schema === 1) {
|
|
2274
|
+
const legacy = normalizePointers(o);
|
|
2275
|
+
const hasLegacyState = legacy.active_vault_id !== null || legacy.active_workspace_id !== null || workspaces2.length > 0 || vaults2.length > 0;
|
|
2276
|
+
return {
|
|
2277
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
2278
|
+
current_account_id: hasLegacyState ? LEGACY_REGISTRY_ACCOUNT_ID : null,
|
|
2279
|
+
accounts: hasLegacyState ? { [LEGACY_REGISTRY_ACCOUNT_ID]: legacy } : {},
|
|
2280
|
+
workspaces: workspaces2,
|
|
2281
|
+
vaults: vaults2
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
const accounts = {};
|
|
2285
|
+
if (o.accounts && typeof o.accounts === "object" && !Array.isArray(o.accounts)) {
|
|
2286
|
+
for (const [id, pointers] of Object.entries(o.accounts)) {
|
|
2287
|
+
if (isNonEmptyString(id)) accounts[id] = normalizeStoredPointers(pointers);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
1653
2290
|
return {
|
|
1654
2291
|
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
2292
|
+
current_account_id: isNonEmptyString(o.current_account_id) ? o.current_account_id : null,
|
|
2293
|
+
accounts,
|
|
1655
2294
|
workspaces: workspaces2,
|
|
1656
|
-
vaults: vaults2
|
|
1657
|
-
active_workspace_id,
|
|
1658
|
-
active_vault_id
|
|
2295
|
+
vaults: vaults2
|
|
1659
2296
|
};
|
|
1660
2297
|
}
|
|
1661
|
-
function
|
|
2298
|
+
function readRegistryJson(opts) {
|
|
1662
2299
|
const filePath = vaultRegistryFilePath(opts);
|
|
1663
|
-
if (!
|
|
2300
|
+
if (!existsSync4(filePath)) return { kind: "missing" };
|
|
1664
2301
|
try {
|
|
1665
|
-
const raw =
|
|
1666
|
-
|
|
2302
|
+
const raw = readFileSync6(filePath, "utf8");
|
|
2303
|
+
const json = JSON.parse(raw);
|
|
2304
|
+
if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
|
|
2305
|
+
return { kind: "ok", raw, json };
|
|
1667
2306
|
} catch {
|
|
1668
|
-
return
|
|
2307
|
+
return { kind: "unreadable" };
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
function inspectVaultRegistry(opts = {}) {
|
|
2311
|
+
const read = readRegistryJson(opts);
|
|
2312
|
+
if (read.kind !== "ok") {
|
|
2313
|
+
return {
|
|
2314
|
+
registry: emptyVaultRegistry(),
|
|
2315
|
+
file_schema_version: null,
|
|
2316
|
+
registry_unreadable: read.kind === "unreadable"
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
return {
|
|
2320
|
+
registry: parseVaultRegistryFile(read.json),
|
|
2321
|
+
file_schema_version: registryFileSchemaOf(read.json),
|
|
2322
|
+
registry_unreadable: false
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
function loadVaultRegistry(opts = {}) {
|
|
2326
|
+
return inspectVaultRegistry(opts).registry;
|
|
2327
|
+
}
|
|
2328
|
+
function ownsRegistryRows(registry, accountId2) {
|
|
2329
|
+
return registry.vaults.some((v) => v.account_id === accountId2) || registry.workspaces.some((w) => w.account_id === accountId2);
|
|
2330
|
+
}
|
|
2331
|
+
function accountPointersOf(registry, accountId2) {
|
|
2332
|
+
const own = accountId2 ? registry.accounts[accountId2] : void 0;
|
|
2333
|
+
const legacy = registry.accounts[LEGACY_REGISTRY_ACCOUNT_ID];
|
|
2334
|
+
const clearsCount = Boolean(accountId2) && ownsRegistryRows(registry, accountId2);
|
|
2335
|
+
const pick = (field) => {
|
|
2336
|
+
const mine = own?.[field];
|
|
2337
|
+
if (isNonEmptyString(mine)) return mine;
|
|
2338
|
+
if (mine === null && clearsCount && accountId2 !== LEGACY_REGISTRY_ACCOUNT_ID) return null;
|
|
2339
|
+
const fallback = legacy?.[field];
|
|
2340
|
+
return isNonEmptyString(fallback) ? fallback : null;
|
|
2341
|
+
};
|
|
2342
|
+
return {
|
|
2343
|
+
active_vault_id: pick("active_vault_id"),
|
|
2344
|
+
active_workspace_id: pick("active_workspace_id")
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
function serializeVaultRegistry(registry) {
|
|
2348
|
+
return `${JSON.stringify(registry, null, 2)}
|
|
2349
|
+
`;
|
|
2350
|
+
}
|
|
2351
|
+
function underTestRunner() {
|
|
2352
|
+
return Boolean(process.env.VITEST) || Boolean(process.env.NODE_TEST_CONTEXT);
|
|
2353
|
+
}
|
|
2354
|
+
function samePathLoose(a, b) {
|
|
2355
|
+
const norm = (p) => {
|
|
2356
|
+
const r = resolve4(p).replace(/[\\/]+$/, "");
|
|
2357
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
2358
|
+
};
|
|
2359
|
+
return norm(a) === norm(b);
|
|
2360
|
+
}
|
|
2361
|
+
function assertNotRealHomeRegistryUnderTest(filePath) {
|
|
2362
|
+
if (!underTestRunner()) return;
|
|
2363
|
+
if (samePathLoose(dirname4(filePath), join7(heyspexHome2(), ".heyspex"))) {
|
|
2364
|
+
throw new Error(
|
|
2365
|
+
`registry-file: refusing to write ${filePath} under a test runner \u2014 that is the real ~/.heyspex registry. Pass opts.registryDir (a temp dir).`
|
|
2366
|
+
);
|
|
1669
2367
|
}
|
|
1670
2368
|
}
|
|
1671
2369
|
function writeVaultRegistry(registry, opts = {}) {
|
|
1672
2370
|
const filePath = vaultRegistryFilePath(opts);
|
|
1673
|
-
|
|
2371
|
+
assertNotRealHomeRegistryUnderTest(filePath);
|
|
2372
|
+
mkdirSync3(dirname4(filePath), { recursive: true });
|
|
1674
2373
|
const tmpPath = `${filePath}.tmp`;
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
renameSync(tmpPath, filePath);
|
|
2374
|
+
writeFileSync3(tmpPath, serializeVaultRegistry(registry), "utf8");
|
|
2375
|
+
renameSync3(tmpPath, filePath);
|
|
1678
2376
|
}
|
|
1679
2377
|
function isValidId(id) {
|
|
1680
2378
|
if (!isNonEmptyString(id)) return false;
|
|
@@ -1682,10 +2380,17 @@ function isValidId(id) {
|
|
|
1682
2380
|
if (id === "." || id === "..") return false;
|
|
1683
2381
|
return true;
|
|
1684
2382
|
}
|
|
2383
|
+
function requireAccountId(fn, accountId2) {
|
|
2384
|
+
if (!isNonEmptyString(accountId2)) {
|
|
2385
|
+
throw new Error(`${fn}: account_id is required`);
|
|
2386
|
+
}
|
|
2387
|
+
return accountId2;
|
|
2388
|
+
}
|
|
1685
2389
|
function registerWorkspace(record, opts = {}) {
|
|
1686
2390
|
if (!isValidId(record.id)) {
|
|
1687
2391
|
throw new Error(`registerWorkspace: invalid workspace id "${record.id}"`);
|
|
1688
2392
|
}
|
|
2393
|
+
requireAccountId("registerWorkspace", record.account_id);
|
|
1689
2394
|
const registry = loadVaultRegistry(opts);
|
|
1690
2395
|
const idx = registry.workspaces.findIndex((w) => w.id === record.id);
|
|
1691
2396
|
if (idx >= 0) {
|
|
@@ -1696,30 +2401,921 @@ function registerWorkspace(record, opts = {}) {
|
|
|
1696
2401
|
writeVaultRegistry(registry, opts);
|
|
1697
2402
|
return registry;
|
|
1698
2403
|
}
|
|
1699
|
-
function
|
|
2404
|
+
function writeAccountPointer(fn, accountId2, field, value, opts) {
|
|
2405
|
+
requireAccountId(fn, accountId2);
|
|
2406
|
+
if (value !== null && !isNonEmptyString(value)) {
|
|
2407
|
+
throw new Error(`${fn}: ${field} must be a non-empty string or null`);
|
|
2408
|
+
}
|
|
2409
|
+
if (opts === null || typeof opts !== "object") {
|
|
2410
|
+
throw new Error(`${fn}: opts must be an object`);
|
|
2411
|
+
}
|
|
1700
2412
|
const registry = loadVaultRegistry(opts);
|
|
1701
|
-
registry.
|
|
2413
|
+
const row = { ...registry.accounts[accountId2] ?? {}, [field]: value };
|
|
2414
|
+
registry.accounts = { ...registry.accounts, [accountId2]: row };
|
|
2415
|
+
if (registry.current_account_id === null || registry.current_account_id === LEGACY_REGISTRY_ACCOUNT_ID && accountId2 !== LEGACY_REGISTRY_ACCOUNT_ID) {
|
|
2416
|
+
registry.current_account_id = accountId2;
|
|
2417
|
+
}
|
|
1702
2418
|
writeVaultRegistry(registry, opts);
|
|
1703
2419
|
return registry;
|
|
1704
2420
|
}
|
|
2421
|
+
function setActiveWorkspaceId(accountId2, workspaceId, opts = {}) {
|
|
2422
|
+
return writeAccountPointer(
|
|
2423
|
+
"setActiveWorkspaceId",
|
|
2424
|
+
accountId2,
|
|
2425
|
+
"active_workspace_id",
|
|
2426
|
+
workspaceId,
|
|
2427
|
+
opts
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
function clearPointerEverywhere(registry, field, id) {
|
|
2431
|
+
for (const [accountId2, pointers] of Object.entries(registry.accounts)) {
|
|
2432
|
+
if (pointers[field] === id) registry.accounts[accountId2] = { ...pointers, [field]: null };
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
1705
2435
|
function removeWorkspace(id, opts = {}) {
|
|
1706
2436
|
const registry = loadVaultRegistry(opts);
|
|
1707
2437
|
registry.workspaces = registry.workspaces.filter((w) => w.id !== id);
|
|
1708
|
-
|
|
1709
|
-
|
|
2438
|
+
clearPointerEverywhere(registry, "active_workspace_id", id);
|
|
2439
|
+
writeVaultRegistry(registry, opts);
|
|
2440
|
+
return registry;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
2444
|
+
var EXIT = {
|
|
2445
|
+
/** Success. */
|
|
2446
|
+
OK: 0,
|
|
2447
|
+
/**
|
|
2448
|
+
* A read-only CHECK ran correctly and its answer is "no" — `hs connect
|
|
2449
|
+
* --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
|
|
2450
|
+
* script can tell "you typed the command wrong" from "the command worked,
|
|
2451
|
+
* the repo is not connected". Both were `1` until 2026-09-18, which made
|
|
2452
|
+
* `--check` unusable in a conditional: every caller had to parse stdout to
|
|
2453
|
+
* find out which of the two had happened.
|
|
2454
|
+
*/
|
|
2455
|
+
CHECK_FAILED: 2,
|
|
2456
|
+
/** Usage error — unknown subcommand/flag, or a missing required argument. */
|
|
2457
|
+
USAGE: 1,
|
|
2458
|
+
/** Claim race lost — the spec is already claimed by another agent (409). */
|
|
2459
|
+
CONFLICT: 9,
|
|
2460
|
+
/** Target not found — release of an unheld claim, etc. (404). */
|
|
2461
|
+
NOT_FOUND: 10,
|
|
2462
|
+
/** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
|
|
2463
|
+
INVALID_ARG: 11,
|
|
2464
|
+
/** SpexConsole unreachable (network error reaching the dev server). */
|
|
2465
|
+
CONSOLE_UNREACHABLE: 12,
|
|
2466
|
+
/** Server failure — a non-OK response that isn't 400/402/404/409. */
|
|
2467
|
+
WRITE_FAILED: 13,
|
|
2468
|
+
/** Neon control plane absent — the endpoint returned 402 (upgrade required). */
|
|
2469
|
+
NEON_REQUIRED: 14,
|
|
2470
|
+
/**
|
|
2471
|
+
* A cross-account `--account <id>` override was refused — the caller is
|
|
2472
|
+
* not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
|
|
2473
|
+
* [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
|
|
2474
|
+
* from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
|
|
2475
|
+
* malformed command.
|
|
2476
|
+
*/
|
|
2477
|
+
FORBIDDEN: 15,
|
|
2478
|
+
/**
|
|
2479
|
+
* A CLOUD write refused because no account id could be resolved (no
|
|
2480
|
+
* `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
|
|
2481
|
+
* `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
|
|
2482
|
+
* ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
|
|
2483
|
+
* `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
|
|
2484
|
+
* authorized for this override".
|
|
2485
|
+
*/
|
|
2486
|
+
SIGN_IN_REQUIRED: 16,
|
|
2487
|
+
/**
|
|
2488
|
+
* Signed in, but the account a repo would be attached to was not confirmed:
|
|
2489
|
+
* a non-interactive install/connect with no matching `--account` / `--yes`,
|
|
2490
|
+
* an `account_id` that disagrees with the bearer's user, or the human
|
|
2491
|
+
* aborting the identity prompt (`install-identity.ts`,
|
|
2492
|
+
* [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
|
|
2493
|
+
*/
|
|
2494
|
+
IDENTITY_UNCONFIRMED: 17,
|
|
2495
|
+
/**
|
|
2496
|
+
* The repo this verb runs in is not registered on this machine (no
|
|
2497
|
+
* `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
|
|
2498
|
+
* cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
|
|
2499
|
+
* nothing was sent. Names `hs connect`
|
|
2500
|
+
* ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
|
|
2501
|
+
*/
|
|
2502
|
+
VAULT_NOT_REGISTERED: 18,
|
|
2503
|
+
/**
|
|
2504
|
+
* The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
|
|
2505
|
+
* *`) was written but the CLOUD half did not land -- the console refused
|
|
2506
|
+
* it or was never reached. The verb's receipt names the failed half and the
|
|
2507
|
+
* exact command to re-run
|
|
2508
|
+
* ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
|
|
2509
|
+
* Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
|
|
2510
|
+
* sync the cloud" from a generic server failure.
|
|
2511
|
+
*/
|
|
2512
|
+
CLOUD_WRITE_FAILED: 19,
|
|
2513
|
+
/**
|
|
2514
|
+
* The repo's committed `heyspex.yaml` records `account: <A>` but the machine
|
|
2515
|
+
* is signed in as another account, so `hs connect` refused and wrote
|
|
2516
|
+
* nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
|
|
2517
|
+
* ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
|
|
2518
|
+
*/
|
|
2519
|
+
ACCOUNT_MISMATCH: 20,
|
|
2520
|
+
/**
|
|
2521
|
+
* Both sides of a setup verb's receipt were read back and still DISAGREE
|
|
2522
|
+
* after a short bounded re-read of the cloud side (a briefly stale read is
|
|
2523
|
+
* absorbed first). The write may well have landed; what the verb cannot
|
|
2524
|
+
* claim is that this machine and the console now say the same thing. The
|
|
2525
|
+
* receipt prints the diffs and the command to re-run
|
|
2526
|
+
* ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
|
|
2527
|
+
* Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
|
|
2528
|
+
*/
|
|
2529
|
+
RECEIPT_DISAGREE: 21
|
|
2530
|
+
};
|
|
2531
|
+
var VAULT_ID_HEADER = "x-heyspex-vault-id";
|
|
2532
|
+
var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
|
|
2533
|
+
function readPairedBearer(opts = {}) {
|
|
2534
|
+
return readBearer(opts);
|
|
2535
|
+
}
|
|
2536
|
+
function readPairedToken(opts = {}) {
|
|
2537
|
+
return readPairedBearer(opts).token ?? void 0;
|
|
2538
|
+
}
|
|
2539
|
+
function scopeHeadersFor(scope, method, path4) {
|
|
2540
|
+
if (scope === "none") {
|
|
2541
|
+
if (!isVaultLessEndpoint(method, path4)) {
|
|
2542
|
+
throw new Error(
|
|
2543
|
+
`controlRequest: scope "none" is only allowed for the vault-less endpoints (${VAULT_LESS_ENDPOINTS.join(", ")}); ${method.toUpperCase()} ${path4} is vault-scoped and must name { accountId, vaultId }.`
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
return {};
|
|
2547
|
+
}
|
|
2548
|
+
return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
|
|
2549
|
+
}
|
|
2550
|
+
function cloudRequestScope(scope) {
|
|
2551
|
+
const named = { accountId: scope.accountId, vaultId: scope.vaultId };
|
|
2552
|
+
return {
|
|
2553
|
+
...named,
|
|
2554
|
+
headersFor: (method, path4) => isVaultLessEndpoint(method, path4) ? {} : scopeHeadersFor(named, method, path4)
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
function withCloudScope(deps, accountId2, vaultId) {
|
|
2558
|
+
if (!deps?.http || !vaultId || !accountId2) return deps;
|
|
2559
|
+
return { ...deps, http: { ...deps.http, scope: cloudRequestScope({ accountId: accountId2, vaultId }) } };
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// scripts/lib/console-url-resolver.mjs
|
|
2563
|
+
init_heyspex_home();
|
|
2564
|
+
init_dev_mode();
|
|
2565
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
2566
|
+
import { homedir as homedir8 } from "node:os";
|
|
2567
|
+
import { join as join9 } from "node:path";
|
|
2568
|
+
var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
|
|
2569
|
+
function trimTrailingSlash(url) {
|
|
2570
|
+
return url.replace(/\/+$/, "");
|
|
2571
|
+
}
|
|
2572
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
|
2573
|
+
function isLoopbackConsoleUrl(url) {
|
|
2574
|
+
if (typeof url !== "string" || !url) return false;
|
|
2575
|
+
try {
|
|
2576
|
+
return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
|
|
2577
|
+
} catch {
|
|
2578
|
+
return false;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
var staleLoopbackCredentialsWarned = false;
|
|
2582
|
+
function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
|
|
2583
|
+
if (staleLoopbackCredentialsWarned) return;
|
|
2584
|
+
staleLoopbackCredentialsWarned = true;
|
|
2585
|
+
try {
|
|
2586
|
+
log(
|
|
2587
|
+
`[heyspex] ignoring console_url ${credentialsUrl} from ~/.heyspex/credentials.yaml: the resolved reliance is cloud, and a loopback console cannot serve it \u2014 using ${derivedUrl} (set HEYSPEX_CONSOLE_URL to override explicitly, or run \`hs dev mode:local\` to go back to a local console).
|
|
2588
|
+
`
|
|
2589
|
+
);
|
|
2590
|
+
} catch {
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile: readFile2 = defaultReadFile2 } = {}) {
|
|
2594
|
+
try {
|
|
2595
|
+
const raw = readFile2(join9(homeDir, ".heyspex", "credentials.yaml"));
|
|
2596
|
+
const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
2597
|
+
const value = match?.[1]?.trim();
|
|
2598
|
+
return value && value.length > 0 ? trimTrailingSlash(value) : null;
|
|
2599
|
+
} catch {
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function defaultReadFile2(p) {
|
|
2604
|
+
return readFileSync8(p, "utf8");
|
|
2605
|
+
}
|
|
2606
|
+
function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
|
|
2607
|
+
if (!repoRoot) return null;
|
|
2608
|
+
try {
|
|
2609
|
+
const discovery = String(readDiscovery(join9(repoRoot, ".heyspex", "console-url")) ?? "").trim();
|
|
2610
|
+
if (discovery.length > 0) return trimTrailingSlash(discovery);
|
|
2611
|
+
} catch {
|
|
2612
|
+
}
|
|
2613
|
+
return null;
|
|
2614
|
+
}
|
|
2615
|
+
var ConsoleUrlUnresolvedError = class extends Error {
|
|
2616
|
+
constructor() {
|
|
2617
|
+
super(
|
|
2618
|
+
"resolveConsoleUrl: local reliance with no HEYSPEX_CONSOLE_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the localhost:3000 default in strict mode."
|
|
2619
|
+
);
|
|
2620
|
+
this.name = "ConsoleUrlUnresolvedError";
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
function resolveModeEnvelope({ env, homeDir }) {
|
|
2624
|
+
try {
|
|
2625
|
+
const resolved = resolveDevMode({ env, homeDir });
|
|
2626
|
+
return {
|
|
2627
|
+
reliance: resolved.reliance === "cloud" ? "cloud" : "local",
|
|
2628
|
+
consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
|
|
2629
|
+
};
|
|
2630
|
+
} catch {
|
|
2631
|
+
return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function resolveConsoleUrl({
|
|
2635
|
+
repoRoot,
|
|
2636
|
+
env = process.env,
|
|
2637
|
+
homeDir = heyspexHome(),
|
|
2638
|
+
strict = false,
|
|
2639
|
+
devMode,
|
|
2640
|
+
log = (line) => process.stderr.write(line),
|
|
2641
|
+
readFile: readFile2 = defaultReadFile2,
|
|
2642
|
+
readDiscovery = defaultReadFile2
|
|
2643
|
+
} = {}) {
|
|
2644
|
+
const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
|
|
2645
|
+
if (envUrl) return trimTrailingSlash(envUrl);
|
|
2646
|
+
const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
|
|
2647
|
+
const cloud = mode.reliance === "cloud";
|
|
2648
|
+
const derivedUrl = trimTrailingSlash(
|
|
2649
|
+
cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
|
|
2650
|
+
);
|
|
2651
|
+
const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile: readFile2 });
|
|
2652
|
+
if (credentialsUrl) {
|
|
2653
|
+
if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
|
|
2654
|
+
warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
|
|
2655
|
+
} else {
|
|
2656
|
+
return credentialsUrl;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
if (!cloud) {
|
|
2660
|
+
const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery });
|
|
2661
|
+
if (discoveryUrl) return discoveryUrl;
|
|
2662
|
+
if (strict) throw new ConsoleUrlUnresolvedError();
|
|
2663
|
+
}
|
|
2664
|
+
return derivedUrl;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// spex-console/src/lib/cli/defer.ts
|
|
2668
|
+
function resolveSpexConsoleUrl(vaultRoot, readDiscovery, opts = {}) {
|
|
2669
|
+
return resolveConsoleUrl({
|
|
2670
|
+
repoRoot: vaultRoot,
|
|
2671
|
+
env: opts.env,
|
|
2672
|
+
homeDir: opts.homeDir,
|
|
2673
|
+
readFile: opts.readFile,
|
|
2674
|
+
readDiscovery,
|
|
2675
|
+
log: opts.log
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
var HELP = [
|
|
2679
|
+
"Usage:",
|
|
2680
|
+
" hs defer <spec-id> [--until <when>] [--json] [--no-daemon]",
|
|
2681
|
+
" hs undefer <spec-id> [--json] [--no-daemon]",
|
|
2682
|
+
"",
|
|
2683
|
+
"Time-shift a spec via its `defer_until` frontmatter field. A deferred spec is",
|
|
2684
|
+
"scheduler-invisible until the date passes, then auto-revives.",
|
|
2685
|
+
"",
|
|
2686
|
+
"Arguments:",
|
|
2687
|
+
" <spec-id> The spec to defer/undefer (e.g. HS-TASK-EWL5).",
|
|
2688
|
+
"",
|
|
2689
|
+
"Flags:",
|
|
2690
|
+
" --until <when> ISO date, or relative offset +Nd / +Nw / +Nm / +Ny.",
|
|
2691
|
+
" Defaults to +1y (matches the console Defer button).",
|
|
2692
|
+
" Ignored by `undefer` (which always clears the field).",
|
|
2693
|
+
" --json Emit a machine-readable JSON result.",
|
|
2694
|
+
" --no-daemon Write straight to the vault (no console needed; CI path).",
|
|
2695
|
+
" --help, -h Show this help.",
|
|
2696
|
+
"",
|
|
2697
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
|
|
2698
|
+
].join("\n");
|
|
2699
|
+
|
|
2700
|
+
// spex-console/src/lib/cli/install-identity.ts
|
|
2701
|
+
import { spawnSync } from "node:child_process";
|
|
2702
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
2703
|
+
import { join as join10 } from "node:path";
|
|
2704
|
+
import { createInterface } from "node:readline/promises";
|
|
2705
|
+
init_credentials();
|
|
2706
|
+
|
|
2707
|
+
// vendor/heyspex-mcp/whoami.mjs
|
|
2708
|
+
init_heyspex_home();
|
|
2709
|
+
init_credentials();
|
|
2710
|
+
import { homedir as homedir9 } from "node:os";
|
|
2711
|
+
var SESSION_PATH = "/api/mcp/session";
|
|
2712
|
+
async function probeCredentialIdentity({ consoleUrl, token, fetchImpl = fetch } = {}) {
|
|
2713
|
+
if (!token) return { kind: "no_credential" };
|
|
2714
|
+
const base = String(consoleUrl ?? "").trim().replace(/\/+$/, "");
|
|
2715
|
+
let res;
|
|
2716
|
+
try {
|
|
2717
|
+
res = await fetchImpl(`${base}${SESSION_PATH}`, {
|
|
2718
|
+
method: "GET",
|
|
2719
|
+
headers: { authorization: `Bearer ${token}` },
|
|
2720
|
+
cache: "no-store"
|
|
2721
|
+
});
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
return { kind: "unreachable", message: err?.message ?? String(err) };
|
|
2724
|
+
}
|
|
2725
|
+
if (res.status === 401 || res.status === 403) return { kind: "rejected", status: res.status };
|
|
2726
|
+
if (res.status === 404 || res.status === 405) return { kind: "unsupported", status: res.status };
|
|
2727
|
+
let body = null;
|
|
2728
|
+
try {
|
|
2729
|
+
body = await res.json();
|
|
2730
|
+
} catch {
|
|
2731
|
+
body = null;
|
|
2732
|
+
}
|
|
2733
|
+
if (!res.ok || !body || typeof body.userId !== "string" || !body.userId.trim()) {
|
|
2734
|
+
return { kind: "unreachable", message: `console returned ${res.status}` };
|
|
2735
|
+
}
|
|
2736
|
+
return {
|
|
2737
|
+
kind: "identified",
|
|
2738
|
+
accountId: body.userId.trim(),
|
|
2739
|
+
email: typeof body.email === "string" && body.email.trim() ? body.email.trim() : null,
|
|
2740
|
+
displayName: typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : null,
|
|
2741
|
+
// How the console authenticated this caller (the route's `mode`, e.g. a
|
|
2742
|
+
// bearer); the `hs login` receipt shows it ([[HS-CRNE-WW9R-TASK-give-hs-install-and-hs-login-the]]).
|
|
2743
|
+
mode: typeof body.mode === "string" && body.mode.trim() ? body.mode.trim() : null,
|
|
2744
|
+
consoleUrl: base
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function formatIdentity({ email, accountId: accountId2 }) {
|
|
2748
|
+
return email ? `${email} (${accountId2})` : accountId2;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// spex-console/src/lib/cli/init.ts
|
|
2752
|
+
import { randomUUID } from "node:crypto";
|
|
2753
|
+
var HELP2 = [
|
|
2754
|
+
"Usage:",
|
|
2755
|
+
" hs init [--console-url <url>] [--vault-root <path>] [--force] [--json]",
|
|
2756
|
+
" hs init --customer-vault <dir> [--console-url <url>] [--principles <all|none|a,b>]",
|
|
2757
|
+
" [--vault-name <name>] [--git] [--json]",
|
|
2758
|
+
"",
|
|
2759
|
+
"Write the top-level heyspex.yaml vault marker (schema_version, console_url,",
|
|
2760
|
+
"vault_root, vault_id; `hs connect` also records `account:`, the account id the",
|
|
2761
|
+
"repo was connected under) \u2014 the committed file the console / web companion read",
|
|
2762
|
+
"to bootstrap a vault. Idempotent: an existing marker is left untouched unless",
|
|
2763
|
+
"--force, EXCEPT a missing vault_id is backfilled in place either way (backward",
|
|
2764
|
+
"compatible \u2014 no schema_version bump). vault_id is reused exactly when already",
|
|
2765
|
+
"present; a fresh one is minted only when absent.",
|
|
2766
|
+
"Also scaffolds the vault_root folder and seeds .heyspex/config.yaml (the",
|
|
2767
|
+
"HeySpex workflow settings, ADR-0009) from the shipped template",
|
|
2768
|
+
"(vendor/heyspex-mcp/config-seed.yaml) when that file is absent. An existing",
|
|
2769
|
+
".heyspex/config.yaml is never rewritten \u2014 not even with --force; change its",
|
|
2770
|
+
"values with `hs settings set <key> <value>`.",
|
|
2771
|
+
"",
|
|
2772
|
+
"--customer-vault <dir> scaffolds a REAL customer vault at <dir> instead: the",
|
|
2773
|
+
"specs/ folder tree, heyspex.yaml, the seeded .heyspex/config.yaml, and every",
|
|
2774
|
+
"row of the template shipping manifest",
|
|
2775
|
+
"(spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
|
|
2776
|
+
"copied verbatim, template-shape docs written as author-prompt skeletons \u2014",
|
|
2777
|
+
"plus a generated CLAUDE.md, the kept-principles index, and the template_version",
|
|
2778
|
+
"pin; with --git, one initial `git init` + commit. Rerunnable: a <dir> that already",
|
|
2779
|
+
"holds heyspex.yaml and an empty/placeholder-only specs/ is accepted, existing",
|
|
2780
|
+
"files are never overwritten (an existing CLAUDE.md gets a CLAUDE.md.template",
|
|
2781
|
+
"beside it), and the result lists each manifest row as created or kept. Refuses a",
|
|
2782
|
+
"non-empty <dir> without heyspex.yaml, or a specs/ holding managed specs.",
|
|
2783
|
+
"See init.customer-vault.ts.",
|
|
2784
|
+
"",
|
|
2785
|
+
"Flags:",
|
|
2786
|
+
" --console-url <url> console_url value (default: the mode-aware console --",
|
|
2787
|
+
" $HEYSPEX_CONSOLE_URL, else the signed-in console, else",
|
|
2788
|
+
" https://app.heyspex.com in prod mode).",
|
|
2789
|
+
" --vault-root <path> vault_root value (default: specs/).",
|
|
2790
|
+
" --customer-vault <dir> Scaffold a new customer vault at <dir> (see above).",
|
|
2791
|
+
" --principles <all|none|a,b> Which universal principle docs to ship",
|
|
2792
|
+
" (--customer-vault only). Omitted + a TTY: interactive",
|
|
2793
|
+
" keep/skip prompt per principle. Omitted + no TTY: all.",
|
|
2794
|
+
" --vault-name <name> Display name for the scaffolded vault (--customer-vault",
|
|
2795
|
+
" only; default: <dir>'s basename).",
|
|
2796
|
+
" --git Also run git init + one initial commit (--customer-vault only;",
|
|
2797
|
+
" default: no git). --no-git is accepted as a no-op.",
|
|
2798
|
+
" --force Overwrite an existing heyspex.yaml (never .heyspex/config.yaml).",
|
|
2799
|
+
" --json Emit a machine-readable JSON result.",
|
|
2800
|
+
" --help, -h Show this help.",
|
|
2801
|
+
"",
|
|
2802
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed"
|
|
2803
|
+
].join("\n");
|
|
2804
|
+
|
|
2805
|
+
// spex-console/src/lib/cli/satellite.ts
|
|
2806
|
+
import { resolve as resolveNodePath } from "node:path";
|
|
2807
|
+
import process2 from "node:process";
|
|
2808
|
+
|
|
2809
|
+
// spex-console/src/lib/cli/install-identity.ts
|
|
2810
|
+
var SWITCH_HINT = "Sign in as another account with `hs login --switch-account` (or create one with `hs login --create-account`).";
|
|
2811
|
+
var ATTACH_WORDING = {
|
|
2812
|
+
willBe: "This repo will be attached to",
|
|
2813
|
+
wouldBe: "It would be attached to",
|
|
2814
|
+
act: "attach this repo",
|
|
2815
|
+
choose: "Choose the account this repo should belong to.",
|
|
2816
|
+
noAccount: "there is no account to attach this repo to",
|
|
2817
|
+
whichAccount: "which account this repo would be attached to",
|
|
2818
|
+
accountFlag: "--account",
|
|
2819
|
+
yesFlag: "--yes"
|
|
2820
|
+
};
|
|
2821
|
+
var WRITE_WORDING = {
|
|
2822
|
+
willBe: "This change will be made as",
|
|
2823
|
+
wouldBe: "It would be made as",
|
|
2824
|
+
act: "make this change",
|
|
2825
|
+
choose: "Choose the account this change should be made as.",
|
|
2826
|
+
noAccount: "there is no account to make this change as",
|
|
2827
|
+
whichAccount: "which account this change would be made as",
|
|
2828
|
+
accountFlag: "--as",
|
|
2829
|
+
yesFlag: "--confirm-account"
|
|
2830
|
+
};
|
|
2831
|
+
function extractWriteIdentityFlags(argv) {
|
|
2832
|
+
const flags = { yes: false };
|
|
2833
|
+
const rest = [];
|
|
2834
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2835
|
+
const arg = argv[i];
|
|
2836
|
+
if (arg === "--as" || arg.startsWith("--as=")) {
|
|
2837
|
+
const v = arg === "--as" ? argv[++i] : arg.slice("--as=".length);
|
|
2838
|
+
if (v === void 0 || v.trim() === "" || v.trim().startsWith("-")) {
|
|
2839
|
+
return { flags, rest, error: "--as requires the signed-in account's email or account id" };
|
|
2840
|
+
}
|
|
2841
|
+
flags.account = v.trim();
|
|
2842
|
+
} else if (arg === "--confirm-account") {
|
|
2843
|
+
flags.yes = true;
|
|
2844
|
+
} else {
|
|
2845
|
+
rest.push(arg);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
return { flags, rest };
|
|
2849
|
+
}
|
|
2850
|
+
function matchesAccount(identity, wanted) {
|
|
2851
|
+
const w = wanted.trim().toLowerCase();
|
|
2852
|
+
return identity.accountId.toLowerCase() === w || (identity.email?.toLowerCase() ?? "") === w;
|
|
2853
|
+
}
|
|
2854
|
+
function formatShownIdentity(identity) {
|
|
2855
|
+
return formatIdentity(identity) + (identity.unverified ? " [unverified - console not yet updated]" : "") + (identity.tokenSource ? ` [token: ${describeBearerSource(identity.tokenSource)}]` : "");
|
|
2856
|
+
}
|
|
2857
|
+
async function probeBearerIdentity(opts) {
|
|
2858
|
+
const probe = await probeCredentialIdentity({
|
|
2859
|
+
consoleUrl: opts.consoleUrl,
|
|
2860
|
+
token: opts.bearer.token,
|
|
2861
|
+
fetchImpl: opts.fetchImpl ?? fetch
|
|
2862
|
+
});
|
|
2863
|
+
if (probe.kind === "identified" && opts.bearer.source) {
|
|
2864
|
+
return { ...probe, tokenSource: opts.bearer.source };
|
|
2865
|
+
}
|
|
2866
|
+
return probe;
|
|
2867
|
+
}
|
|
2868
|
+
function decideIdentity(input) {
|
|
2869
|
+
const { probe, configAccountId, flags, interactive, consoleUrl, verb } = input;
|
|
2870
|
+
const w = input.wording ?? ATTACH_WORDING;
|
|
2871
|
+
if (isLoopbackConsoleUrl(consoleUrl)) {
|
|
2872
|
+
return {
|
|
2873
|
+
action: "proceed",
|
|
2874
|
+
identity: null,
|
|
2875
|
+
note: `Local console (${consoleUrl}): no cloud account is bound by this run.`
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
if (probe.kind === "no_credential" || probe.kind === "rejected") {
|
|
2879
|
+
const why = probe.kind === "no_credential" ? "This machine is not signed in to HeySpex" : `The stored sign-in was rejected by ${consoleUrl} (HTTP ${probe.status}) - revoked, or from another console`;
|
|
2880
|
+
if (interactive) {
|
|
2881
|
+
return {
|
|
2882
|
+
action: "prompt",
|
|
2883
|
+
identity: null,
|
|
2884
|
+
message: `${why}. ${w.choose}`,
|
|
2885
|
+
choices: ["switch", "create", "quit"]
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
return {
|
|
2889
|
+
action: "refuse",
|
|
2890
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
2891
|
+
message: `${verb}: refusing -- ${why.charAt(0).toLowerCase()}${why.slice(1)}, so ${w.noAccount}. Run \`hs login\` (or \`hs login --create-account\`) first; nothing was written.`
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
if (probe.kind === "unreachable") {
|
|
2895
|
+
if (flags.yes) {
|
|
2896
|
+
return {
|
|
2897
|
+
action: "proceed",
|
|
2898
|
+
identity: null,
|
|
2899
|
+
note: `Could not reach ${consoleUrl} to confirm the account (${probe.message}); continuing because ${w.yesFlag} was given.`
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
action: "refuse",
|
|
2904
|
+
code: EXIT.CONSOLE_UNREACHABLE,
|
|
2905
|
+
message: `${verb}: refusing -- could not reach ${consoleUrl} to confirm ${w.whichAccount} (${probe.message}). Retry when online, or pass ${w.yesFlag} to continue without confirming. Nothing was written.`
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
let identity;
|
|
2909
|
+
if (probe.kind === "unsupported") {
|
|
2910
|
+
if (!configAccountId) {
|
|
2911
|
+
return {
|
|
2912
|
+
action: "refuse",
|
|
2913
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
2914
|
+
message: `${verb}: refusing -- ${consoleUrl} cannot confirm whose sign-in this is yet, and no current account is recorded in ~/.heyspex/credentials.yaml.Run \`hs login\` first; nothing was written.`
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
identity = { accountId: configAccountId, email: null, unverified: true };
|
|
2918
|
+
} else {
|
|
2919
|
+
identity = {
|
|
2920
|
+
accountId: probe.accountId,
|
|
2921
|
+
email: probe.email,
|
|
2922
|
+
...probe.tokenSource ? { tokenSource: probe.tokenSource } : {}
|
|
2923
|
+
};
|
|
2924
|
+
if (configAccountId && configAccountId !== probe.accountId) {
|
|
2925
|
+
const message2 = `The sign-in on this machine belongs to ${formatIdentity(identity)}, but this machine's current account (~/.heyspex/credentials.yaml, or HS_ACCOUNT_ID) is ${configAccountId}. A repo bound now would be registered under one and written under the other.`;
|
|
2926
|
+
if (interactive) {
|
|
2927
|
+
return { action: "prompt", identity, message: message2, choices: ["switch", "create", "quit"] };
|
|
2928
|
+
}
|
|
2929
|
+
return {
|
|
2930
|
+
action: "refuse",
|
|
2931
|
+
code: EXIT.IDENTITY_UNCONFIRMED,
|
|
2932
|
+
message: `${verb}: refusing -- ${message2} ${SWITCH_HINT} Nothing was written.`
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
const shown = formatShownIdentity(identity);
|
|
2937
|
+
if (flags.account !== void 0) {
|
|
2938
|
+
if (matchesAccount(identity, flags.account)) {
|
|
2939
|
+
return { action: "proceed", identity, note: `${w.willBe} ${shown}.` };
|
|
2940
|
+
}
|
|
2941
|
+
return {
|
|
2942
|
+
action: "refuse",
|
|
2943
|
+
code: EXIT.IDENTITY_UNCONFIRMED,
|
|
2944
|
+
message: `${verb}: refusing -- ${w.accountFlag} ${flags.account} does not match the signed-in account ${shown}. ${SWITCH_HINT} Nothing was written.`
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
if (flags.yes) {
|
|
2948
|
+
return { action: "proceed", identity, note: `${w.willBe} ${shown} (${w.yesFlag}).` };
|
|
2949
|
+
}
|
|
2950
|
+
if (interactive) {
|
|
2951
|
+
return {
|
|
2952
|
+
action: "prompt",
|
|
2953
|
+
identity,
|
|
2954
|
+
message: `${w.willBe} ${shown} on ${consoleUrl}.`,
|
|
2955
|
+
choices: ["continue", "switch", "create", "quit"]
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
return {
|
|
2959
|
+
action: "refuse",
|
|
2960
|
+
code: EXIT.IDENTITY_UNCONFIRMED,
|
|
2961
|
+
message: `${verb}: refusing to ${w.act} without confirming the account. ${w.wouldBe} ${shown} on ${consoleUrl}. Confirm with ${w.accountFlag} ${identity.email ?? identity.accountId} (or ${w.yesFlag}), or ${SWITCH_HINT.charAt(0).toLowerCase()}${SWITCH_HINT.slice(1)} An agent should ask the user which account to use. Nothing was written.`
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
function renderChoices(choices, signedIn) {
|
|
2965
|
+
const label = {
|
|
2966
|
+
continue: "[Enter] continue",
|
|
2967
|
+
switch: signedIn ? "[s] sign in with a different account" : "[s] sign in",
|
|
2968
|
+
create: "[n] create a new account",
|
|
2969
|
+
quit: "[q] abort"
|
|
2970
|
+
};
|
|
2971
|
+
return choices.map((c) => label[c]).join(" - ");
|
|
2972
|
+
}
|
|
2973
|
+
function parseChoice(answer, choices) {
|
|
2974
|
+
const a = answer.trim().toLowerCase();
|
|
2975
|
+
const picked = a === "" || a === "c" || a === "continue" || a === "y" || a === "yes" ? "continue" : a === "s" || a === "switch" || a === "sign in" ? "switch" : a === "n" || a === "new" || a === "create" ? "create" : a === "q" || a === "quit" || a === "abort" ? "quit" : null;
|
|
2976
|
+
return picked && choices.includes(picked) ? picked : null;
|
|
2977
|
+
}
|
|
2978
|
+
function refusal(code, message2, reason) {
|
|
2979
|
+
return {
|
|
2980
|
+
ok: false,
|
|
2981
|
+
result: { code, message: message2, payload: { ok: false, error: reason, message: message2 }, isError: true }
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
var MAX_ROUNDS = 4;
|
|
2985
|
+
async function runIdentityGate(io, flags, verb, wording = ATTACH_WORDING) {
|
|
2986
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
2987
|
+
const probe = await io.probe();
|
|
2988
|
+
const decision = decideIdentity({
|
|
2989
|
+
probe,
|
|
2990
|
+
configAccountId: io.readConfigAccountId(),
|
|
2991
|
+
flags,
|
|
2992
|
+
interactive: io.interactive,
|
|
2993
|
+
consoleUrl: io.consoleUrl,
|
|
2994
|
+
verb,
|
|
2995
|
+
wording
|
|
2996
|
+
});
|
|
2997
|
+
if (decision.action === "proceed") {
|
|
2998
|
+
io.print(decision.note);
|
|
2999
|
+
return { ok: true, identity: decision.identity, note: decision.note };
|
|
3000
|
+
}
|
|
3001
|
+
if (decision.action === "refuse") {
|
|
3002
|
+
return refusal(decision.code, decision.message, "identity_unconfirmed");
|
|
3003
|
+
}
|
|
3004
|
+
io.print(decision.message);
|
|
3005
|
+
const menu = renderChoices(decision.choices, decision.identity !== null);
|
|
3006
|
+
let choice = null;
|
|
3007
|
+
while (choice === null) {
|
|
3008
|
+
choice = parseChoice(await io.ask(`${menu}: `), decision.choices);
|
|
3009
|
+
}
|
|
3010
|
+
if (choice === "continue") {
|
|
3011
|
+
const note = `${wording.willBe} ${formatShownIdentity(decision.identity)}.`;
|
|
3012
|
+
return { ok: true, identity: decision.identity, note };
|
|
3013
|
+
}
|
|
3014
|
+
if (choice === "quit") {
|
|
3015
|
+
return refusal(
|
|
3016
|
+
EXIT.IDENTITY_UNCONFIRMED,
|
|
3017
|
+
`${verb}: aborted - nothing was written.`,
|
|
3018
|
+
"aborted"
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
const code = await io.runLogin(choice === "create" ? "create" : "select_account");
|
|
3022
|
+
if (code !== 0) {
|
|
3023
|
+
io.print(`hs login exited ${code}; the account was not changed.`);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
return refusal(
|
|
3027
|
+
EXIT.IDENTITY_UNCONFIRMED,
|
|
3028
|
+
`${verb}: refusing -- the account was still not confirmed after several sign-in attempts. Nothing was written.`,
|
|
3029
|
+
"identity_unconfirmed"
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
3032
|
+
var WORKSPACE_WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
3033
|
+
"create",
|
|
3034
|
+
"rename",
|
|
3035
|
+
"unregister",
|
|
3036
|
+
"archive",
|
|
3037
|
+
"restore"
|
|
3038
|
+
]);
|
|
3039
|
+
async function gateWriteVerb(opts) {
|
|
3040
|
+
if (!opts.writeVerbs.has(opts.sub)) return null;
|
|
3041
|
+
return runIdentityGate(opts.io, opts.flags, `${opts.namespace} ${opts.sub}`, WRITE_WORDING);
|
|
3042
|
+
}
|
|
3043
|
+
function readConfiguredAccountId(env = process.env, homeDir = heyspexHome2()) {
|
|
3044
|
+
return resolveSessionOwnerUncached({ env, homeDir });
|
|
3045
|
+
}
|
|
3046
|
+
function isInteractiveTerminal() {
|
|
3047
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY) && process.env.CI !== "true";
|
|
3048
|
+
}
|
|
3049
|
+
function defaultIdentityGateIo(opts) {
|
|
3050
|
+
const consoleUrl = opts.consoleUrl.trim().replace(/\/+$/, "");
|
|
3051
|
+
const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join10(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync5(p));
|
|
3052
|
+
return {
|
|
3053
|
+
consoleUrl,
|
|
3054
|
+
interactive: opts.interactive ?? isInteractiveTerminal(),
|
|
3055
|
+
probe: () => probeBearerIdentity({ consoleUrl, bearer: readPairedBearer(), fetchImpl: fetch }),
|
|
3056
|
+
readConfigAccountId: () => readConfiguredAccountId(),
|
|
3057
|
+
ask: async (question) => {
|
|
3058
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3059
|
+
try {
|
|
3060
|
+
return await rl.question(question);
|
|
3061
|
+
} finally {
|
|
3062
|
+
rl.close();
|
|
3063
|
+
}
|
|
3064
|
+
},
|
|
3065
|
+
runLogin: async (prompt) => {
|
|
3066
|
+
const flag = prompt === "create" ? "--create-account" : "--switch-account";
|
|
3067
|
+
if (!loginCli) {
|
|
3068
|
+
process.stderr.write(
|
|
3069
|
+
`Run \`hs login ${flag}\` in another terminal, then come back here.
|
|
3070
|
+
`
|
|
3071
|
+
);
|
|
3072
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3073
|
+
try {
|
|
3074
|
+
await rl.question("Press Enter when the sign-in has finished: ");
|
|
3075
|
+
} finally {
|
|
3076
|
+
rl.close();
|
|
3077
|
+
}
|
|
3078
|
+
return 0;
|
|
3079
|
+
}
|
|
3080
|
+
const res = spawnSync(
|
|
3081
|
+
process.execPath,
|
|
3082
|
+
[loginCli, "login", flag, "--console-url", consoleUrl],
|
|
3083
|
+
{
|
|
3084
|
+
stdio: "inherit",
|
|
3085
|
+
env: { ...process.env, HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1" }
|
|
3086
|
+
}
|
|
3087
|
+
);
|
|
3088
|
+
return res.status ?? 1;
|
|
3089
|
+
},
|
|
3090
|
+
print: (line) => process.stderr.write(`${line}
|
|
3091
|
+
`)
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// spex-console/src/lib/vault-registry/account-pointer.ts
|
|
3096
|
+
init_sql_orm_inert_cli_shim();
|
|
3097
|
+
|
|
3098
|
+
// spex-console/src/lib/cli/vault-workspace-shared.ts
|
|
3099
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3100
|
+
|
|
3101
|
+
// spex-console/src/lib/cli/default-workspace.ts
|
|
3102
|
+
function defaultWorkspaceId(accountId2) {
|
|
3103
|
+
const sanitized = accountId2.replace(/[/\\.\s]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3104
|
+
return `ws-${sanitized || "account"}`;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// spex-console/src/lib/cli/vault-workspace-shared.ts
|
|
3108
|
+
function resolveConfiguredAccountId(opts = {}) {
|
|
3109
|
+
return resolveSessionOwnerUncached({
|
|
3110
|
+
env: opts.env ?? process.env,
|
|
3111
|
+
homeDir: opts.homeDir ?? heyspexHome2(),
|
|
3112
|
+
readFile: opts.readFile ?? ((p) => readFileSync9(p, "utf-8"))
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
var CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE = "refusing -- no signed-in account (no HS_ACCOUNT_ID env var, and no account_id: in ~/.heyspex/config.yaml). Run `hs login` first; nothing was written.";
|
|
3116
|
+
function resolveCloudAccountIdOrRefuse(opts = {}) {
|
|
3117
|
+
const accountId2 = resolveConfiguredAccountId(opts);
|
|
3118
|
+
return accountId2 ? { ok: true, accountId: accountId2 } : { ok: false, message: CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE };
|
|
3119
|
+
}
|
|
3120
|
+
var VAULT_OWNED_BY_OTHER_ACCOUNT = "vault_owned_by_other_account";
|
|
3121
|
+
var WORKSPACE_OWNED_BY_OTHER_ACCOUNT = "workspace_owned_by_other_account";
|
|
3122
|
+
function workspaceNotOwnedByCallerMessage(id) {
|
|
3123
|
+
return `workspace "${id}" belongs to another HeySpex account, so nothing was changed. Only the account that owns a workspace can rename, archive, restore or unregister it (\`hs login --switch-account\` to act as that account).`;
|
|
3124
|
+
}
|
|
3125
|
+
function workspaceOwnedByOtherAccountMessage(id) {
|
|
3126
|
+
return `workspace "${id}" belongs to another HeySpex account, so nothing was changed. Pick a different workspace id (e.g. \`--workspace <new-id>\`), or sign in as the account that owns it (\`hs login --switch-account\`). Transferring a workspace between accounts is not supported.`;
|
|
3127
|
+
}
|
|
3128
|
+
function classifyHttpFailureReason(res) {
|
|
3129
|
+
if (res.status === 0) return "network-error";
|
|
3130
|
+
if (res.status === 401) return "unauthorized";
|
|
3131
|
+
return "server-error";
|
|
3132
|
+
}
|
|
3133
|
+
async function cloudHttpRequest(http, method, path4, body) {
|
|
3134
|
+
const scopeHeaders = http.scope ? http.scope.headersFor(method, path4) : {};
|
|
3135
|
+
try {
|
|
3136
|
+
const res = await http.fetchImpl(`${http.consoleUrl}${path4}`, {
|
|
3137
|
+
method,
|
|
3138
|
+
headers: {
|
|
3139
|
+
"content-type": "application/json",
|
|
3140
|
+
...http.token ? { authorization: `Bearer ${http.token}` } : {},
|
|
3141
|
+
...scopeHeaders
|
|
3142
|
+
},
|
|
3143
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
3144
|
+
});
|
|
3145
|
+
let json = null;
|
|
3146
|
+
try {
|
|
3147
|
+
json = await res.json();
|
|
3148
|
+
} catch {
|
|
3149
|
+
}
|
|
3150
|
+
return { ok: res.ok, status: res.status, json };
|
|
3151
|
+
} catch {
|
|
3152
|
+
return { ok: false, status: 0, json: null };
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
function isCloudWriteFailure(value) {
|
|
3156
|
+
return typeof value === "object" && value !== null && value.kind === "cloud_write_failed";
|
|
3157
|
+
}
|
|
3158
|
+
function cloudWriteFailure(http, res) {
|
|
3159
|
+
const where = http.consoleUrl;
|
|
3160
|
+
let message2;
|
|
3161
|
+
if (res.status === 0) {
|
|
3162
|
+
message2 = `could not reach the console at ${where} -- nothing was changed; check the connection and re-run the command to retry`;
|
|
3163
|
+
} else if (res.status === 401) {
|
|
3164
|
+
message2 = `the console at ${where} refused the sign-in token (HTTP 401) -- nothing was changed; run \`hs login\` and re-run the command`;
|
|
3165
|
+
} else {
|
|
3166
|
+
const code = errorCodeOf(res.json);
|
|
3167
|
+
message2 = `the console at ${where} answered HTTP ${res.status}${code ? ` (${code})` : ""} -- nothing was changed; re-run the command to retry`;
|
|
3168
|
+
}
|
|
3169
|
+
return { kind: "cloud_write_failed", status: res.status, message: message2 };
|
|
3170
|
+
}
|
|
3171
|
+
function errorCodeOf(json) {
|
|
3172
|
+
if (!json || typeof json !== "object") return null;
|
|
3173
|
+
const v = json.error;
|
|
3174
|
+
return typeof v === "string" ? v : null;
|
|
3175
|
+
}
|
|
3176
|
+
async function classifyWorkspaceConflictHttp(id, conflictJson, http) {
|
|
3177
|
+
if (errorCodeOf(conflictJson) === WORKSPACE_OWNED_BY_OTHER_ACCOUNT) {
|
|
3178
|
+
return { written: false, reason: "owned-by-other-account" };
|
|
3179
|
+
}
|
|
3180
|
+
if (conflictJson && typeof conflictJson === "object" && conflictJson.owned_by_caller === true) {
|
|
3181
|
+
return { written: true };
|
|
3182
|
+
}
|
|
3183
|
+
const list = await cloudHttpRequest(http, "GET", "/api/workspaces?include_archived=true");
|
|
3184
|
+
if (!list.ok) return { written: false, reason: classifyHttpFailureReason(list) };
|
|
3185
|
+
const rows = list.json?.workspaces;
|
|
3186
|
+
if (!Array.isArray(rows)) return { written: false, reason: "server-error" };
|
|
3187
|
+
return rows.some((w) => w.id === id) ? { written: true } : { written: false, reason: "owned-by-other-account" };
|
|
3188
|
+
}
|
|
3189
|
+
async function mutateHttp(http, method, path4, body, landed) {
|
|
3190
|
+
const res = await cloudHttpRequest(http, method, path4, body);
|
|
3191
|
+
if (res.ok) return landed;
|
|
3192
|
+
if (res.status === 404) return "not_found";
|
|
3193
|
+
if (res.status === 403 && isOwnedByOtherAccountCode(errorCodeOf(res.json))) {
|
|
3194
|
+
return "owned_by_other_account";
|
|
3195
|
+
}
|
|
3196
|
+
return cloudWriteFailure(http, res);
|
|
3197
|
+
}
|
|
3198
|
+
function isOwnedByOtherAccountCode(code) {
|
|
3199
|
+
return code === VAULT_OWNED_BY_OTHER_ACCOUNT || code === WORKSPACE_OWNED_BY_OTHER_ACCOUNT;
|
|
3200
|
+
}
|
|
3201
|
+
function manageVerbPath(kind, id) {
|
|
3202
|
+
return `/api/${kind}/${encodeURIComponent(id)}`;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
// spex-console/src/lib/vault-registry/account-pointer.ts
|
|
3206
|
+
init_postgres_client_cli_shim();
|
|
3207
|
+
init_postgres();
|
|
3208
|
+
|
|
3209
|
+
// spex-console/src/lib/vault-registry/neon-probe.ts
|
|
3210
|
+
var NEON_PROBE_TIMEOUT_MS = 1500;
|
|
3211
|
+
var SUSPENSION_ELAPSED_FACTOR = 3;
|
|
3212
|
+
var SUSPENSION_MIN_OVERSHOOT_MS = 5e3;
|
|
3213
|
+
var MAX_SUSPENSION_RETRIES = 2;
|
|
3214
|
+
function withTimeout(promise, ms, label) {
|
|
3215
|
+
return new Promise((resolve5, reject) => {
|
|
3216
|
+
const timer = setTimeout(() => reject(new NeonProbeTimeoutError(label, ms)), ms);
|
|
3217
|
+
promise.then(
|
|
3218
|
+
(value) => {
|
|
3219
|
+
clearTimeout(timer);
|
|
3220
|
+
resolve5(value);
|
|
3221
|
+
},
|
|
3222
|
+
(err) => {
|
|
3223
|
+
clearTimeout(timer);
|
|
3224
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
3225
|
+
}
|
|
3226
|
+
);
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
var NeonProbeTimeoutError = class _NeonProbeTimeoutError extends Error {
|
|
3230
|
+
timeoutMs;
|
|
3231
|
+
constructor(label, timeoutMs) {
|
|
3232
|
+
super(`${label} timed out after ${timeoutMs}ms`);
|
|
3233
|
+
this.name = "NeonProbeTimeoutError";
|
|
3234
|
+
this.timeoutMs = timeoutMs;
|
|
3235
|
+
Object.setPrototypeOf(this, _NeonProbeTimeoutError.prototype);
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
function sleep(ms) {
|
|
3239
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3240
|
+
}
|
|
3241
|
+
function emitProbeFailure(options, failure) {
|
|
3242
|
+
try {
|
|
3243
|
+
options?.onFailure?.(failure);
|
|
3244
|
+
} catch {
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
async function runNeonReachabilityProbe(attempt, options = {}, label = "Neon reachability probe") {
|
|
3248
|
+
const timeoutMs = options.timeoutMs ?? NEON_PROBE_TIMEOUT_MS;
|
|
3249
|
+
const backoffMs = options.backoffMs ?? [];
|
|
3250
|
+
const deadlineMs = options.deadlineMs ?? Number.POSITIVE_INFINITY;
|
|
3251
|
+
const attempts = backoffMs.length + 1;
|
|
3252
|
+
let startedAt = Date.now();
|
|
3253
|
+
let suspensions = 0;
|
|
3254
|
+
for (let i = 0; i < attempts; i++) {
|
|
3255
|
+
const attemptStartedAt = Date.now();
|
|
3256
|
+
try {
|
|
3257
|
+
await withTimeout(Promise.resolve().then(attempt), timeoutMs, label);
|
|
3258
|
+
return true;
|
|
3259
|
+
} catch (err) {
|
|
3260
|
+
const elapsedMs = Date.now() - attemptStartedAt;
|
|
3261
|
+
const suspended = elapsedMs >= timeoutMs * SUSPENSION_ELAPSED_FACTOR && elapsedMs - timeoutMs >= SUSPENSION_MIN_OVERSHOOT_MS;
|
|
3262
|
+
if (suspended && suspensions < MAX_SUSPENSION_RETRIES) {
|
|
3263
|
+
suspensions += 1;
|
|
3264
|
+
emitProbeFailure(options, {
|
|
3265
|
+
reason: "suspended",
|
|
3266
|
+
attempt: i + 1,
|
|
3267
|
+
attempts,
|
|
3268
|
+
timeoutMs,
|
|
3269
|
+
elapsedMs,
|
|
3270
|
+
willRetry: true,
|
|
3271
|
+
error: err
|
|
3272
|
+
});
|
|
3273
|
+
startedAt = Date.now();
|
|
3274
|
+
i -= 1;
|
|
3275
|
+
continue;
|
|
3276
|
+
}
|
|
3277
|
+
const isLast = i === attempts - 1;
|
|
3278
|
+
const pastDeadline = Date.now() - startedAt >= deadlineMs;
|
|
3279
|
+
const willRetry = !isLast && !pastDeadline;
|
|
3280
|
+
emitProbeFailure(options, {
|
|
3281
|
+
reason: err instanceof NeonProbeTimeoutError ? "timeout" : "error",
|
|
3282
|
+
attempt: i + 1,
|
|
3283
|
+
attempts,
|
|
3284
|
+
timeoutMs,
|
|
3285
|
+
elapsedMs,
|
|
3286
|
+
willRetry,
|
|
3287
|
+
error: err
|
|
3288
|
+
});
|
|
3289
|
+
if (!willRetry) return false;
|
|
3290
|
+
await sleep(backoffMs[i]);
|
|
3291
|
+
}
|
|
1710
3292
|
}
|
|
1711
|
-
|
|
1712
|
-
return registry;
|
|
3293
|
+
return false;
|
|
1713
3294
|
}
|
|
1714
|
-
var init_registry_file = __esm({
|
|
1715
|
-
"spex-console/src/lib/vault-registry/registry-file.ts"() {
|
|
1716
|
-
"use strict";
|
|
1717
|
-
init_types();
|
|
1718
|
-
init_types();
|
|
1719
|
-
}
|
|
1720
|
-
});
|
|
1721
3295
|
|
|
1722
3296
|
// spex-console/src/lib/vault-registry/account-pointer.ts
|
|
3297
|
+
var UnknownWorkspaceIdError = class extends Error {
|
|
3298
|
+
workspaceId;
|
|
3299
|
+
constructor(workspaceId) {
|
|
3300
|
+
super(
|
|
3301
|
+
`setActiveWorkspace: unknown workspace id "${workspaceId}" \u2014 no matching row in Neon "workspaces" or registry.json's workspaces[] list. Nothing was written.`
|
|
3302
|
+
);
|
|
3303
|
+
this.name = "UnknownWorkspaceIdError";
|
|
3304
|
+
this.workspaceId = workspaceId;
|
|
3305
|
+
}
|
|
3306
|
+
};
|
|
3307
|
+
var CloudPointerWriteError = class extends Error {
|
|
3308
|
+
status;
|
|
3309
|
+
consoleUrl;
|
|
3310
|
+
constructor(consoleUrl, status, code) {
|
|
3311
|
+
super(
|
|
3312
|
+
status === 0 ? `could not reach the console at ${consoleUrl} to write the active pointer` : status === 401 ? `the console at ${consoleUrl} refused the sign-in token (HTTP 401) -- run \`hs login\`` : `the console at ${consoleUrl} answered HTTP ${status}${code ? ` (${code})` : ""} to the active-pointer write`
|
|
3313
|
+
);
|
|
3314
|
+
this.name = "CloudPointerWriteError";
|
|
3315
|
+
this.status = status;
|
|
3316
|
+
this.consoleUrl = consoleUrl;
|
|
3317
|
+
}
|
|
3318
|
+
};
|
|
1723
3319
|
async function isNeonReachable(deps) {
|
|
1724
3320
|
if (deps.neonReachable !== void 0) return deps.neonReachable;
|
|
1725
3321
|
const usingInjectedDb = deps.db !== void 0;
|
|
@@ -1742,6 +3338,19 @@ async function isNeonReachable(deps) {
|
|
|
1742
3338
|
await db.execute(sql`select 1`);
|
|
1743
3339
|
}, deps.probe);
|
|
1744
3340
|
}
|
|
3341
|
+
function shouldUseHttp(deps) {
|
|
3342
|
+
return deps.db === void 0 && deps.http !== void 0;
|
|
3343
|
+
}
|
|
3344
|
+
async function readPointerHttp(http) {
|
|
3345
|
+
const res = await cloudHttpRequest(http, "GET", "/api/vaults/active");
|
|
3346
|
+
if (!res.ok || !res.json || typeof res.json !== "object") return null;
|
|
3347
|
+
return res.json;
|
|
3348
|
+
}
|
|
3349
|
+
function errorCodeOf2(json) {
|
|
3350
|
+
if (!json || typeof json !== "object") return null;
|
|
3351
|
+
const v = json.error;
|
|
3352
|
+
return typeof v === "string" ? v : null;
|
|
3353
|
+
}
|
|
1745
3354
|
async function accountSettingsRow(accountId2, db) {
|
|
1746
3355
|
const rows = await db.select().from(accountSettings).where(eq(accountSettings.accountId, accountId2)).limit(1);
|
|
1747
3356
|
return rows[0] ?? null;
|
|
@@ -1761,14 +3370,30 @@ function workspaceExistsInRegistry(workspaceId, opts) {
|
|
|
1761
3370
|
return loadVaultRegistry(opts).workspaces.some((w) => w.id === workspaceId);
|
|
1762
3371
|
}
|
|
1763
3372
|
async function getActiveWorkspace(accountId2, deps = {}) {
|
|
1764
|
-
if (
|
|
3373
|
+
if (shouldUseHttp(deps)) {
|
|
3374
|
+
const body = await readPointerHttp(deps.http);
|
|
3375
|
+
if (body) {
|
|
3376
|
+
return typeof body.workspace_id === "string" && body.workspace_id ? body.workspace_id : null;
|
|
3377
|
+
}
|
|
3378
|
+
} else if (await isNeonReachable(deps)) {
|
|
1765
3379
|
const db = deps.db ?? getPostgresDb();
|
|
1766
3380
|
const row = await accountSettingsRow(accountId2, db);
|
|
1767
3381
|
return row?.activeWorkspaceId ?? null;
|
|
1768
3382
|
}
|
|
1769
|
-
return loadVaultRegistry(deps.registryOpts).active_workspace_id;
|
|
3383
|
+
return accountPointersOf(loadVaultRegistry(deps.registryOpts), accountId2).active_workspace_id;
|
|
1770
3384
|
}
|
|
1771
3385
|
async function setActiveWorkspace(accountId2, workspaceId, deps = {}) {
|
|
3386
|
+
if (shouldUseHttp(deps)) {
|
|
3387
|
+
const res = await cloudHttpRequest(deps.http, "POST", "/api/vaults/active", {
|
|
3388
|
+
workspace_id: workspaceId
|
|
3389
|
+
});
|
|
3390
|
+
if (res.ok) return;
|
|
3391
|
+
const code = errorCodeOf2(res.json);
|
|
3392
|
+
if (res.status === 404 && code === "unknown_workspace") {
|
|
3393
|
+
throw new UnknownWorkspaceIdError(workspaceId);
|
|
3394
|
+
}
|
|
3395
|
+
throw new CloudPointerWriteError(deps.http.consoleUrl, res.status, code);
|
|
3396
|
+
}
|
|
1772
3397
|
if (await isNeonReachable(deps)) {
|
|
1773
3398
|
const db = deps.db ?? getPostgresDb();
|
|
1774
3399
|
if (!await workspaceExistsInNeon(accountId2, workspaceId, db)) {
|
|
@@ -1781,108 +3406,50 @@ async function setActiveWorkspace(accountId2, workspaceId, deps = {}) {
|
|
|
1781
3406
|
if (!workspaceExistsInRegistry(workspaceId, registryOpts)) {
|
|
1782
3407
|
throw new UnknownWorkspaceIdError(workspaceId);
|
|
1783
3408
|
}
|
|
1784
|
-
setActiveWorkspaceId(workspaceId, registryOpts);
|
|
3409
|
+
setActiveWorkspaceId(accountId2, workspaceId, registryOpts);
|
|
1785
3410
|
}
|
|
1786
3411
|
async function clearActiveWorkspace(accountId2, deps = {}) {
|
|
1787
|
-
if (await isNeonReachable(deps)) {
|
|
3412
|
+
if (!shouldUseHttp(deps) && await isNeonReachable(deps)) {
|
|
1788
3413
|
const db = deps.db ?? getPostgresDb();
|
|
1789
3414
|
await upsertAccountSettings(accountId2, { activeWorkspaceId: null }, db);
|
|
1790
3415
|
return;
|
|
1791
3416
|
}
|
|
1792
|
-
setActiveWorkspaceId(null, deps.registryOpts ?? {});
|
|
3417
|
+
setActiveWorkspaceId(accountId2, null, deps.registryOpts ?? {});
|
|
1793
3418
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
init_postgres();
|
|
1801
|
-
init_neon_probe();
|
|
1802
|
-
init_registry_file();
|
|
1803
|
-
UnknownWorkspaceIdError = class extends Error {
|
|
1804
|
-
workspaceId;
|
|
1805
|
-
constructor(workspaceId) {
|
|
1806
|
-
super(
|
|
1807
|
-
`setActiveWorkspace: unknown workspace id "${workspaceId}" \u2014 no matching row in Neon "workspaces" or registry.json's workspaces[] list. Nothing was written.`
|
|
1808
|
-
);
|
|
1809
|
-
this.name = "UnknownWorkspaceIdError";
|
|
1810
|
-
this.workspaceId = workspaceId;
|
|
1811
|
-
}
|
|
1812
|
-
};
|
|
3419
|
+
|
|
3420
|
+
// dist/cli/._admin-scope-inert.cli-shim.mjs
|
|
3421
|
+
async function resolveScopedAccountId(callerAccountId, requestedAccountId) {
|
|
3422
|
+
const target = requestedAccountId == null ? "" : String(requestedAccountId).trim();
|
|
3423
|
+
if (!target || target === callerAccountId) {
|
|
3424
|
+
return { ok: true, accountId: callerAccountId };
|
|
1813
3425
|
}
|
|
1814
|
-
|
|
3426
|
+
return {
|
|
3427
|
+
ok: false,
|
|
3428
|
+
code: "forbidden_account_override",
|
|
3429
|
+
message: "Only a builder/admin may act on another account's workspaces or vaults."
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
// spex-console/src/lib/vault-registry/vault-workspace-store.ts
|
|
3434
|
+
init_sql_orm_inert_cli_shim();
|
|
3435
|
+
init_postgres_client_cli_shim();
|
|
3436
|
+
init_postgres();
|
|
1815
3437
|
|
|
1816
3438
|
// spex-console/src/lib/vault-registry/repo-identity.ts
|
|
1817
3439
|
import path2 from "node:path";
|
|
1818
|
-
var init_repo_identity = __esm({
|
|
1819
|
-
"spex-console/src/lib/vault-registry/repo-identity.ts"() {
|
|
1820
|
-
"use strict";
|
|
1821
|
-
}
|
|
1822
|
-
});
|
|
1823
3440
|
|
|
1824
|
-
// spex-console/src/lib/
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
import path3 from "node:path";
|
|
1828
|
-
function resolveCliAccountId(opts = {}) {
|
|
1829
|
-
const env = opts.env ?? process.env;
|
|
1830
|
-
const fromEnv = env.HS_ACCOUNT_ID?.trim();
|
|
1831
|
-
if (fromEnv) return fromEnv;
|
|
1832
|
-
try {
|
|
1833
|
-
const homeDir = opts.homeDir ?? os2.homedir();
|
|
1834
|
-
const readFile2 = opts.readFile ?? ((p) => readFileSync5(p, "utf-8"));
|
|
1835
|
-
const config = readFile2(path3.join(homeDir, ".heyspex", "config.yaml"));
|
|
1836
|
-
const match = config.match(/^account_id:\s*["']?([^"'\r\n#]+)/m);
|
|
1837
|
-
const value = match?.[1]?.trim();
|
|
1838
|
-
if (value && value.length > 0) return value;
|
|
1839
|
-
} catch {
|
|
1840
|
-
}
|
|
1841
|
-
return DEFAULT_ACCOUNT_ID;
|
|
1842
|
-
}
|
|
1843
|
-
async function cloudHttpRequest(http, method, path5, body) {
|
|
1844
|
-
try {
|
|
1845
|
-
const res = await http.fetchImpl(`${http.consoleUrl}${path5}`, {
|
|
1846
|
-
method,
|
|
1847
|
-
headers: {
|
|
1848
|
-
"content-type": "application/json",
|
|
1849
|
-
...http.token ? { authorization: `Bearer ${http.token}` } : {}
|
|
1850
|
-
},
|
|
1851
|
-
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
1852
|
-
});
|
|
1853
|
-
let json = null;
|
|
1854
|
-
try {
|
|
1855
|
-
json = await res.json();
|
|
1856
|
-
} catch {
|
|
1857
|
-
}
|
|
1858
|
-
return { ok: res.ok, status: res.status, json };
|
|
1859
|
-
} catch {
|
|
1860
|
-
return { ok: false, status: 0, json: null };
|
|
1861
|
-
}
|
|
3441
|
+
// spex-console/src/lib/vault-registry/vault-workspace-store.ts
|
|
3442
|
+
function mapProbeFailureReason(reason) {
|
|
3443
|
+
return reason === "not-configured" ? "not-configured" : "network-error";
|
|
1862
3444
|
}
|
|
1863
3445
|
function shouldUseCloudHttp(deps) {
|
|
1864
|
-
return deps.db === void 0 && deps.http !== void 0
|
|
3446
|
+
return deps.db === void 0 && deps.http !== void 0;
|
|
1865
3447
|
}
|
|
1866
|
-
async function
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
});
|
|
1872
|
-
return res.status === 201 || res.status === 409;
|
|
1873
|
-
}
|
|
1874
|
-
async function insertWorkspaceStrictHttp(record, http) {
|
|
1875
|
-
const res = await cloudHttpRequest(http, "POST", "/api/workspaces", {
|
|
1876
|
-
id: record.id,
|
|
1877
|
-
display_name: record.displayName,
|
|
1878
|
-
...record.githubOrg ? { github_org: record.githubOrg } : {}
|
|
1879
|
-
});
|
|
1880
|
-
if (res.status === 201) return "inserted";
|
|
1881
|
-
if (res.status === 409) return "conflict";
|
|
1882
|
-
return "unreachable";
|
|
1883
|
-
}
|
|
1884
|
-
async function isNeonReachableForWrite(deps) {
|
|
1885
|
-
if (deps.neonReachable !== void 0) return deps.neonReachable;
|
|
3448
|
+
async function isNeonReachableForWriteWithReason(deps) {
|
|
3449
|
+
if (shouldUseCloudHttp(deps)) return { reachable: false, reason: "not-configured" };
|
|
3450
|
+
if (deps.neonReachable !== void 0) {
|
|
3451
|
+
return deps.neonReachable ? { reachable: true } : { reachable: false, reason: "network-error" };
|
|
3452
|
+
}
|
|
1886
3453
|
const usingInjectedDb = deps.db !== void 0;
|
|
1887
3454
|
if (!usingInjectedDb && !isPostgresConfigured()) {
|
|
1888
3455
|
emitProbeFailure(deps.probe, {
|
|
@@ -1896,19 +3463,46 @@ async function isNeonReachableForWrite(deps) {
|
|
|
1896
3463
|
"isPostgresConfigured() is false \u2014 no DATABASE_URL env var and no usable ~/.heyspex/credentials.yaml. Neon was never contacted."
|
|
1897
3464
|
)
|
|
1898
3465
|
});
|
|
1899
|
-
return false;
|
|
3466
|
+
return { reachable: false, reason: "not-configured" };
|
|
1900
3467
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
3468
|
+
let reason = "network-error";
|
|
3469
|
+
const reachable = await runNeonReachabilityProbe(
|
|
3470
|
+
async () => {
|
|
3471
|
+
const db = deps.db ?? getPostgresDb();
|
|
3472
|
+
await db.execute(sql`select 1`);
|
|
3473
|
+
},
|
|
3474
|
+
{
|
|
3475
|
+
...deps.probe,
|
|
3476
|
+
onFailure: (failure) => {
|
|
3477
|
+
reason = mapProbeFailureReason(failure.reason);
|
|
3478
|
+
deps.probe?.onFailure?.(failure);
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
);
|
|
3482
|
+
return reachable ? { reachable: true } : { reachable: false, reason };
|
|
3483
|
+
}
|
|
3484
|
+
async function isNeonReachableForWrite(deps) {
|
|
3485
|
+
return (await isNeonReachableForWriteWithReason(deps)).reachable;
|
|
1905
3486
|
}
|
|
1906
|
-
async function
|
|
1907
|
-
if (
|
|
1908
|
-
|
|
3487
|
+
async function upsertWorkspaceNeonReasoned(record, accountId2, deps = {}) {
|
|
3488
|
+
if (isReservedForOtherAccountsDefault(record.id, accountId2)) {
|
|
3489
|
+
return { written: false, reason: "owned-by-other-account" };
|
|
3490
|
+
}
|
|
3491
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3492
|
+
const res = await cloudHttpRequest(deps.http, "POST", "/api/workspaces", {
|
|
3493
|
+
id: record.id,
|
|
3494
|
+
display_name: record.displayName,
|
|
3495
|
+
...record.githubOrg ? { github_org: record.githubOrg } : {}
|
|
3496
|
+
});
|
|
3497
|
+
if (res.status === 201) return { written: true };
|
|
3498
|
+
if (res.status === 409) return classifyWorkspaceConflictHttp(record.id, res.json, deps.http);
|
|
3499
|
+
return { written: false, reason: classifyHttpFailureReason(res) };
|
|
3500
|
+
}
|
|
3501
|
+
const reach = await isNeonReachableForWriteWithReason(deps);
|
|
3502
|
+
if (!reach.reachable) return { written: false, reason: reach.reason };
|
|
1909
3503
|
try {
|
|
1910
3504
|
const db = deps.db ?? getPostgresDb();
|
|
1911
|
-
await db.insert(workspaces).values({
|
|
3505
|
+
const rows = await db.insert(workspaces).values({
|
|
1912
3506
|
id: record.id,
|
|
1913
3507
|
accountId: accountId2,
|
|
1914
3508
|
displayName: record.displayName,
|
|
@@ -1918,16 +3512,27 @@ async function upsertWorkspaceNeon(record, accountId2, deps = {}) {
|
|
|
1918
3512
|
set: {
|
|
1919
3513
|
displayName: record.displayName,
|
|
1920
3514
|
...record.githubOrg ? { githubOrg: record.githubOrg } : {}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
|
|
3515
|
+
},
|
|
3516
|
+
setWhere: eq(workspaces.accountId, accountId2)
|
|
3517
|
+
}).returning({ id: workspaces.id });
|
|
3518
|
+
return rows.length > 0 ? { written: true } : { written: false, reason: "owned-by-other-account" };
|
|
1924
3519
|
} catch {
|
|
1925
|
-
return false;
|
|
3520
|
+
return { written: false, reason: "server-error" };
|
|
1926
3521
|
}
|
|
1927
3522
|
}
|
|
1928
|
-
async function
|
|
1929
|
-
if (shouldUseCloudHttp(deps))
|
|
1930
|
-
|
|
3523
|
+
async function insertWorkspaceNeonStrictReasoned(record, accountId2, deps = {}) {
|
|
3524
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3525
|
+
const res = await cloudHttpRequest(deps.http, "POST", "/api/workspaces", {
|
|
3526
|
+
id: record.id,
|
|
3527
|
+
display_name: record.displayName,
|
|
3528
|
+
...record.githubOrg ? { github_org: record.githubOrg } : {}
|
|
3529
|
+
});
|
|
3530
|
+
if (res.status === 201) return { result: "inserted" };
|
|
3531
|
+
if (res.status === 409) return { result: "conflict" };
|
|
3532
|
+
return { result: "unreachable", reason: classifyHttpFailureReason(res) };
|
|
3533
|
+
}
|
|
3534
|
+
const reach = await isNeonReachableForWriteWithReason(deps);
|
|
3535
|
+
if (!reach.reachable) return { result: "unreachable", reason: reach.reason };
|
|
1931
3536
|
try {
|
|
1932
3537
|
const db = deps.db ?? getPostgresDb();
|
|
1933
3538
|
const rows = await db.insert(workspaces).values({
|
|
@@ -1936,9 +3541,9 @@ async function insertWorkspaceNeonStrict(record, accountId2, deps = {}) {
|
|
|
1936
3541
|
displayName: record.displayName,
|
|
1937
3542
|
...record.githubOrg ? { githubOrg: record.githubOrg } : {}
|
|
1938
3543
|
}).onConflictDoNothing({ target: workspaces.id }).returning({ id: workspaces.id });
|
|
1939
|
-
return rows.length > 0 ? "inserted" : "conflict";
|
|
3544
|
+
return { result: rows.length > 0 ? "inserted" : "conflict" };
|
|
1940
3545
|
} catch {
|
|
1941
|
-
return "unreachable";
|
|
3546
|
+
return { result: "unreachable", reason: "server-error" };
|
|
1942
3547
|
}
|
|
1943
3548
|
}
|
|
1944
3549
|
async function listWorkspacesNeon(accountId2, deps = {}) {
|
|
@@ -1949,188 +3554,98 @@ async function listWorkspacesNeon(accountId2, deps = {}) {
|
|
|
1949
3554
|
id: workspaces.id,
|
|
1950
3555
|
displayName: workspaces.displayName,
|
|
1951
3556
|
githubOrg: workspaces.githubOrg,
|
|
1952
|
-
createdAt: workspaces.createdAt
|
|
3557
|
+
createdAt: workspaces.createdAt,
|
|
3558
|
+
archived: workspaces.archived
|
|
1953
3559
|
}).from(workspaces).where(eq(workspaces.accountId, accountId2));
|
|
1954
3560
|
return rows.map((r) => ({
|
|
1955
3561
|
id: r.id,
|
|
1956
3562
|
display_name: r.displayName,
|
|
1957
3563
|
created_at: (r.createdAt instanceof Date ? r.createdAt : /* @__PURE__ */ new Date()).toISOString(),
|
|
3564
|
+
// The flag `setWorkspaceArchivedNeon` writes; `list.ts` filters on it.
|
|
3565
|
+
// On a cloud host this row IS the roster (no registry.json to carry the
|
|
3566
|
+
// flag), so leaving it out made an archived workspace list forever.
|
|
3567
|
+
archived: r.archived,
|
|
1958
3568
|
...r.githubOrg ? { github_org: r.githubOrg } : {}
|
|
1959
3569
|
}));
|
|
1960
3570
|
} catch {
|
|
1961
3571
|
return null;
|
|
1962
3572
|
}
|
|
1963
3573
|
}
|
|
3574
|
+
async function classifyZeroRowWorkspaceWrite(db, id, accountId2) {
|
|
3575
|
+
try {
|
|
3576
|
+
const rows = await db.select({ accountId: workspaces.accountId }).from(workspaces).where(eq(workspaces.id, id)).limit(1);
|
|
3577
|
+
return rows[0] && rows[0].accountId !== accountId2 ? "owned_by_other_account" : "not_found";
|
|
3578
|
+
} catch {
|
|
3579
|
+
return "not_found";
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
1964
3582
|
async function renameWorkspaceNeon(id, displayName, accountId2, deps = {}) {
|
|
3583
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3584
|
+
return mutateHttp(
|
|
3585
|
+
deps.http,
|
|
3586
|
+
"PATCH",
|
|
3587
|
+
manageVerbPath("workspaces", id),
|
|
3588
|
+
{ display_name: displayName },
|
|
3589
|
+
"updated"
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
1965
3592
|
if (!await isNeonReachableForWrite(deps)) return "unreachable";
|
|
1966
3593
|
try {
|
|
1967
3594
|
const db = deps.db ?? getPostgresDb();
|
|
1968
3595
|
const rows = await db.update(workspaces).set({ displayName }).where(and(eq(workspaces.id, id), eq(workspaces.accountId, accountId2))).returning({ id: workspaces.id });
|
|
1969
|
-
return rows.length > 0 ? "updated" :
|
|
3596
|
+
return rows.length > 0 ? "updated" : classifyZeroRowWorkspaceWrite(db, id, accountId2);
|
|
1970
3597
|
} catch {
|
|
1971
3598
|
return "unreachable";
|
|
1972
3599
|
}
|
|
1973
3600
|
}
|
|
1974
3601
|
async function setWorkspaceArchivedNeon(id, archived, accountId2, deps = {}) {
|
|
3602
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3603
|
+
return mutateHttp(
|
|
3604
|
+
deps.http,
|
|
3605
|
+
"PATCH",
|
|
3606
|
+
manageVerbPath("workspaces", id),
|
|
3607
|
+
{ archived },
|
|
3608
|
+
"updated"
|
|
3609
|
+
);
|
|
3610
|
+
}
|
|
1975
3611
|
if (!await isNeonReachableForWrite(deps)) return "unreachable";
|
|
1976
3612
|
try {
|
|
1977
3613
|
const db = deps.db ?? getPostgresDb();
|
|
1978
3614
|
const rows = await db.update(workspaces).set({ archived }).where(and(eq(workspaces.id, id), eq(workspaces.accountId, accountId2))).returning({ id: workspaces.id });
|
|
1979
|
-
return rows.length > 0 ? "updated" :
|
|
3615
|
+
return rows.length > 0 ? "updated" : classifyZeroRowWorkspaceWrite(db, id, accountId2);
|
|
1980
3616
|
} catch {
|
|
1981
3617
|
return "unreachable";
|
|
1982
3618
|
}
|
|
1983
3619
|
}
|
|
1984
3620
|
async function deleteWorkspaceNeon(id, accountId2, deps = {}) {
|
|
3621
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3622
|
+
return mutateHttp(deps.http, "DELETE", manageVerbPath("workspaces", id), void 0, "deleted");
|
|
3623
|
+
}
|
|
1985
3624
|
if (!await isNeonReachableForWrite(deps)) return "unreachable";
|
|
1986
3625
|
try {
|
|
1987
3626
|
const db = deps.db ?? getPostgresDb();
|
|
1988
3627
|
const rows = await db.delete(workspaces).where(and(eq(workspaces.id, id), eq(workspaces.accountId, accountId2))).returning({ id: workspaces.id });
|
|
1989
|
-
return rows.length > 0 ? "deleted" :
|
|
3628
|
+
return rows.length > 0 ? "deleted" : classifyZeroRowWorkspaceWrite(db, id, accountId2);
|
|
1990
3629
|
} catch {
|
|
1991
3630
|
return "unreachable";
|
|
1992
3631
|
}
|
|
1993
3632
|
}
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
"use strict";
|
|
1998
|
-
init_sql_orm_inert_cli_shim();
|
|
1999
|
-
init_postgres_client_cli_shim();
|
|
2000
|
-
init_interfaces();
|
|
2001
|
-
init_postgres();
|
|
2002
|
-
init_neon_probe();
|
|
2003
|
-
init_repo_identity();
|
|
2004
|
-
DEFAULT_ACCOUNT_ID = SINGLE_USER_ID;
|
|
2005
|
-
}
|
|
2006
|
-
});
|
|
2007
|
-
|
|
2008
|
-
// spex-console/bin/hs-workspace.ts
|
|
2009
|
-
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2010
|
-
import path4 from "node:path";
|
|
2011
|
-
import process2 from "node:process";
|
|
2012
|
-
import { createInterface } from "node:readline/promises";
|
|
2013
|
-
|
|
2014
|
-
// spex-console/src/lib/cli/control-shared.ts
|
|
2015
|
-
import { readFileSync } from "node:fs";
|
|
2016
|
-
import { homedir } from "node:os";
|
|
2017
|
-
import { join } from "node:path";
|
|
2018
|
-
var EXIT = {
|
|
2019
|
-
/** Success. */
|
|
2020
|
-
OK: 0,
|
|
2021
|
-
/** Usage error — unknown subcommand/flag, or a missing required argument. */
|
|
2022
|
-
USAGE: 1,
|
|
2023
|
-
/** Claim race lost — the spec is already claimed by another agent (409). */
|
|
2024
|
-
CONFLICT: 9,
|
|
2025
|
-
/** Target not found — release of an unheld claim, etc. (404). */
|
|
2026
|
-
NOT_FOUND: 10,
|
|
2027
|
-
/** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
|
|
2028
|
-
INVALID_ARG: 11,
|
|
2029
|
-
/** SpexConsole unreachable (network error reaching the dev server). */
|
|
2030
|
-
CONSOLE_UNREACHABLE: 12,
|
|
2031
|
-
/** Server failure — a non-OK response that isn't 400/402/404/409. */
|
|
2032
|
-
WRITE_FAILED: 13,
|
|
2033
|
-
/** Neon control plane absent — the endpoint returned 402 (upgrade required). */
|
|
2034
|
-
NEON_REQUIRED: 14,
|
|
2035
|
-
/**
|
|
2036
|
-
* A cross-account `--account <id>` override was refused — the caller is
|
|
2037
|
-
* not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
|
|
2038
|
-
* [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
|
|
2039
|
-
* from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
|
|
2040
|
-
* malformed command.
|
|
2041
|
-
*/
|
|
2042
|
-
FORBIDDEN: 15
|
|
2043
|
-
};
|
|
2044
|
-
function readPairedToken() {
|
|
2045
|
-
try {
|
|
2046
|
-
const raw = readFileSync(join(homedir(), ".heyspex", "credentials.yaml"), "utf-8");
|
|
2047
|
-
const match = raw.match(/\b(hsmcp_[A-Za-z0-9_-]+|hscmp_[A-Za-z0-9_-]+)\b/);
|
|
2048
|
-
return match?.[1];
|
|
2049
|
-
} catch {
|
|
2050
|
-
return void 0;
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
// spex-console/src/lib/cli/defer.ts
|
|
2055
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2056
|
-
import os from "node:os";
|
|
2057
|
-
import path from "node:path";
|
|
2058
|
-
|
|
2059
|
-
// scripts/lib/console-url-resolver.mjs
|
|
2060
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2061
|
-
import { homedir as homedir2 } from "node:os";
|
|
2062
|
-
import { join as join2 } from "node:path";
|
|
2063
|
-
var DEFAULT_CONSOLE_URL = "https://app.heyspex.com";
|
|
2064
|
-
|
|
2065
|
-
// spex-console/src/lib/cli/defer.ts
|
|
2066
|
-
function resolveSpexConsoleUrl(vaultRoot, readDiscovery, opts = {}) {
|
|
2067
|
-
const env = opts.env ?? process.env;
|
|
2068
|
-
const raw = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
|
|
2069
|
-
if (raw && raw.length > 0) return raw.replace(/\/+$/, "");
|
|
3633
|
+
async function findWorkspaceForAccountNeon(workspaceId, accountId2, deps = {}) {
|
|
3634
|
+
if (!await isNeonReachableForWrite(deps)) return "unreachable";
|
|
3635
|
+
const db = deps.db ?? getPostgresDb();
|
|
2070
3636
|
try {
|
|
2071
|
-
const
|
|
2072
|
-
const
|
|
2073
|
-
|
|
2074
|
-
const match = credentials.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
2075
|
-
const value = match?.[1]?.trim();
|
|
2076
|
-
if (value && value.length > 0) return value.replace(/\/+$/, "");
|
|
3637
|
+
const rows = await db.select({ id: workspaces.id, displayName: workspaces.displayName }).from(workspaces).where(and(eq(workspaces.id, workspaceId), eq(workspaces.accountId, accountId2))).limit(1);
|
|
3638
|
+
const row = rows[0];
|
|
3639
|
+
return row ? { id: row.id, displayName: row.displayName } : null;
|
|
2077
3640
|
} catch {
|
|
3641
|
+
return "unreachable";
|
|
2078
3642
|
}
|
|
2079
|
-
for (const name of ["console-url", "cockpit-url"]) {
|
|
2080
|
-
try {
|
|
2081
|
-
const discovery = readDiscovery(`${vaultRoot}/.heyspex/${name}`);
|
|
2082
|
-
if (discovery && discovery.trim().length > 0) {
|
|
2083
|
-
return discovery.trim().replace(/\/+$/, "");
|
|
2084
|
-
}
|
|
2085
|
-
} catch {
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return DEFAULT_CONSOLE_URL;
|
|
2089
3643
|
}
|
|
2090
|
-
|
|
2091
|
-
"
|
|
2092
|
-
" hs defer <spec-id> [--until <when>] [--json] [--no-daemon]",
|
|
2093
|
-
" hs undefer <spec-id> [--json] [--no-daemon]",
|
|
2094
|
-
"",
|
|
2095
|
-
"Time-shift a spec via its `defer_until` frontmatter field. A deferred spec is",
|
|
2096
|
-
"scheduler-invisible until the date passes, then auto-revives.",
|
|
2097
|
-
"",
|
|
2098
|
-
"Arguments:",
|
|
2099
|
-
" <spec-id> The spec to defer/undefer (e.g. HS-TASK-EWL5).",
|
|
2100
|
-
"",
|
|
2101
|
-
"Flags:",
|
|
2102
|
-
" --until <when> ISO date, or relative offset +Nd / +Nw / +Nm / +Ny.",
|
|
2103
|
-
" Defaults to +1y (matches the console Defer button).",
|
|
2104
|
-
" Ignored by `undefer` (which always clears the field).",
|
|
2105
|
-
" --json Emit a machine-readable JSON result.",
|
|
2106
|
-
" --no-daemon Write straight to the vault (no console needed; CI path).",
|
|
2107
|
-
" --help, -h Show this help.",
|
|
2108
|
-
"",
|
|
2109
|
-
"Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
|
|
2110
|
-
].join("\n");
|
|
2111
|
-
|
|
2112
|
-
// spex-console/src/lib/cli/workspace.ts
|
|
2113
|
-
init_account_pointer();
|
|
2114
|
-
|
|
2115
|
-
// dist/cli/._admin-scope-inert.cli-shim.mjs
|
|
2116
|
-
async function resolveScopedAccountId(callerAccountId, requestedAccountId) {
|
|
2117
|
-
const target = requestedAccountId == null ? "" : String(requestedAccountId).trim();
|
|
2118
|
-
if (!target || target === callerAccountId) {
|
|
2119
|
-
return { ok: true, accountId: callerAccountId };
|
|
2120
|
-
}
|
|
2121
|
-
return {
|
|
2122
|
-
ok: false,
|
|
2123
|
-
code: "forbidden_account_override",
|
|
2124
|
-
message: "Only a builder/admin may act on another account's workspaces or vaults."
|
|
2125
|
-
};
|
|
3644
|
+
function isReservedForOtherAccountsDefault(workspaceId, accountId2) {
|
|
3645
|
+
return workspaceId.startsWith("ws-user_") && workspaceId !== defaultWorkspaceId(accountId2);
|
|
2126
3646
|
}
|
|
2127
3647
|
|
|
2128
|
-
// spex-console/src/lib/vault-registry/archive.ts
|
|
2129
|
-
init_vault_workspace_shared();
|
|
2130
|
-
|
|
2131
3648
|
// spex-console/src/lib/vault-registry/active-repoint.ts
|
|
2132
|
-
init_account_pointer();
|
|
2133
|
-
init_registry_file();
|
|
2134
3649
|
function pointerDeps(deps) {
|
|
2135
3650
|
return deps.accountPointer ?? { registryOpts: deps.registryOpts };
|
|
2136
3651
|
}
|
|
@@ -2150,15 +3665,57 @@ async function repointActiveWorkspaceOnRemoval(accountId2, removedWorkspaceId, d
|
|
|
2150
3665
|
return { repointed: true, newActiveId: candidate.id };
|
|
2151
3666
|
}
|
|
2152
3667
|
|
|
3668
|
+
// spex-console/src/lib/vault-registry/side-effects.ts
|
|
3669
|
+
function isLocalRegistryHost() {
|
|
3670
|
+
const override = process.env.HEYSPEX_VAULT_WRITE_MODE?.trim().toLowerCase();
|
|
3671
|
+
if (override === "cloud") return false;
|
|
3672
|
+
if (override === "local") return true;
|
|
3673
|
+
return process.env.VERCEL !== "1";
|
|
3674
|
+
}
|
|
3675
|
+
function sideEffectErrorMessage(err) {
|
|
3676
|
+
return err instanceof Error ? err.message : String(err);
|
|
3677
|
+
}
|
|
3678
|
+
function runRegistrySideEffect(write) {
|
|
3679
|
+
const step = "registry_json";
|
|
3680
|
+
if (!isLocalRegistryHost()) return { step, ok: true, skipped: true };
|
|
3681
|
+
try {
|
|
3682
|
+
write();
|
|
3683
|
+
return { step, ok: true };
|
|
3684
|
+
} catch (err) {
|
|
3685
|
+
const error = sideEffectErrorMessage(err);
|
|
3686
|
+
console.error(`[vault-registry] post-write side step "${step}" failed: ${error}`);
|
|
3687
|
+
return { step, ok: false, error };
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
function registryWritten(effect) {
|
|
3691
|
+
return effect.ok && effect.skipped !== true;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
2153
3694
|
// spex-console/src/lib/vault-registry/archive.ts
|
|
2154
|
-
|
|
3695
|
+
function cloudWriteFailed(failure) {
|
|
3696
|
+
return { ok: false, code: "cloud_write_failed", message: failure.message };
|
|
3697
|
+
}
|
|
3698
|
+
function neonUnwritten(kind, id) {
|
|
3699
|
+
return {
|
|
3700
|
+
ok: false,
|
|
3701
|
+
code: "neon_unwritten",
|
|
3702
|
+
message: `${kind} "${id}" was not written to the cloud registry (database unreachable); nothing was changed. Retry once the console can reach its database.`
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
2155
3705
|
function workspaceNotFound(id) {
|
|
2156
3706
|
return { ok: false, code: "workspace_not_found", message: `workspace "${id}" not found` };
|
|
2157
3707
|
}
|
|
3708
|
+
function workspaceNotOwned(id) {
|
|
3709
|
+
return {
|
|
3710
|
+
ok: false,
|
|
3711
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
3712
|
+
message: workspaceNotOwnedByCallerMessage(id)
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
2158
3715
|
async function setWorkspaceArchivedState(input, archived, deps = {}) {
|
|
2159
|
-
const
|
|
2160
|
-
const existing =
|
|
2161
|
-
if (!existing) return workspaceNotFound(input.id);
|
|
3716
|
+
const local = isLocalRegistryHost();
|
|
3717
|
+
const existing = local ? loadVaultRegistry(deps.registryOpts).workspaces.find((w) => w.id === input.id) : void 0;
|
|
3718
|
+
if (local && !existing) return workspaceNotFound(input.id);
|
|
2162
3719
|
const neonResult = await setWorkspaceArchivedNeon(
|
|
2163
3720
|
input.id,
|
|
2164
3721
|
archived,
|
|
@@ -2166,20 +3723,23 @@ async function setWorkspaceArchivedState(input, archived, deps = {}) {
|
|
|
2166
3723
|
deps.neon ?? {}
|
|
2167
3724
|
);
|
|
2168
3725
|
if (neonResult === "not_found") return workspaceNotFound(input.id);
|
|
3726
|
+
if (neonResult === "owned_by_other_account") return workspaceNotOwned(input.id);
|
|
3727
|
+
if (isCloudWriteFailure(neonResult)) return cloudWriteFailed(neonResult);
|
|
3728
|
+
if (neonResult === "unreachable" && !existing) return neonUnwritten("workspace", input.id);
|
|
2169
3729
|
if (archived) {
|
|
2170
3730
|
await repointActiveWorkspaceOnRemoval(input.accountId, input.id, {
|
|
2171
3731
|
registryOpts: deps.registryOpts,
|
|
2172
3732
|
accountPointer: { ...deps.neon, registryOpts: deps.registryOpts }
|
|
2173
3733
|
});
|
|
2174
3734
|
}
|
|
2175
|
-
registerWorkspace({ ...existing, archived }, deps.registryOpts);
|
|
3735
|
+
if (existing) registerWorkspace({ ...existing, archived }, deps.registryOpts);
|
|
2176
3736
|
return {
|
|
2177
3737
|
ok: true,
|
|
2178
3738
|
workspace: {
|
|
2179
3739
|
id: input.id,
|
|
2180
3740
|
archived,
|
|
2181
3741
|
neon_written: neonResult === "updated",
|
|
2182
|
-
registry_written:
|
|
3742
|
+
registry_written: existing !== void 0
|
|
2183
3743
|
}
|
|
2184
3744
|
};
|
|
2185
3745
|
}
|
|
@@ -2218,24 +3778,40 @@ function buildCloseNotDestroyConfirm(kind, verb, displayName) {
|
|
|
2218
3778
|
}
|
|
2219
3779
|
|
|
2220
3780
|
// spex-console/src/lib/vault-registry/list.ts
|
|
2221
|
-
|
|
2222
|
-
init_account_pointer();
|
|
2223
|
-
init_registry_file();
|
|
2224
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
3781
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
2225
3782
|
function pointerDeps2(deps) {
|
|
2226
3783
|
return deps.accountPointer ?? { registryOpts: deps.registryOpts };
|
|
2227
3784
|
}
|
|
3785
|
+
async function resolveSharedNeonReachable(deps, pDeps) {
|
|
3786
|
+
if (pDeps.neonReachable !== void 0) return pDeps.neonReachable;
|
|
3787
|
+
if (!isLocalRegistryHost()) return true;
|
|
3788
|
+
const probe = deps.neonReachableProbe ?? ((d) => isNeonReachableForWrite({ db: d.db, probe: d.probe }));
|
|
3789
|
+
return probe(pDeps);
|
|
3790
|
+
}
|
|
2228
3791
|
function localWorkspaceRows(deps) {
|
|
2229
3792
|
return loadVaultRegistry(deps.registryOpts).workspaces;
|
|
2230
3793
|
}
|
|
3794
|
+
function originTaggedWorkspaceRows(neonRows, deps) {
|
|
3795
|
+
if (neonRows === null) {
|
|
3796
|
+
const rows = deps.allowLocalRegistryFallback ? localWorkspaceRows(deps) : [];
|
|
3797
|
+
return rows.map((w) => ({ ...w, origin: "registry-only" }));
|
|
3798
|
+
}
|
|
3799
|
+
const cloudRows = neonRows.map((w) => ({ ...w, origin: "cloud-backed" }));
|
|
3800
|
+
if (!deps.allowLocalRegistryFallback) return cloudRows;
|
|
3801
|
+
const cloudIds = new Set(cloudRows.map((w) => w.id));
|
|
3802
|
+
const localOnlyRows = localWorkspaceRows(deps).filter((w) => !cloudIds.has(w.id)).map((w) => ({ ...w, origin: "registry-only" }));
|
|
3803
|
+
return [...cloudRows, ...localOnlyRows];
|
|
3804
|
+
}
|
|
2231
3805
|
async function listWorkspaces(accountId2, deps = {}) {
|
|
2232
3806
|
const pDeps = pointerDeps2(deps);
|
|
3807
|
+
const neonReachable = await resolveSharedNeonReachable(deps, pDeps);
|
|
3808
|
+
const sharedPointerDeps = { ...pDeps, neonReachable };
|
|
2233
3809
|
const neonRows = await (deps.neonListWorkspaces ?? listWorkspacesNeon)(accountId2, {
|
|
2234
3810
|
db: pDeps.db,
|
|
2235
|
-
neonReachable
|
|
3811
|
+
neonReachable
|
|
2236
3812
|
});
|
|
2237
|
-
const rows = neonRows
|
|
2238
|
-
const activeId = await getActiveWorkspace(accountId2,
|
|
3813
|
+
const rows = originTaggedWorkspaceRows(neonRows, deps);
|
|
3814
|
+
const activeId = await getActiveWorkspace(accountId2, sharedPointerDeps);
|
|
2239
3815
|
const scoped = deps.includeArchived ? rows : rows.filter((w) => !w.archived);
|
|
2240
3816
|
return {
|
|
2241
3817
|
workspaces: scoped.map((w) => ({ ...w, active: w.id === activeId })),
|
|
@@ -2243,12 +3819,7 @@ async function listWorkspaces(accountId2, deps = {}) {
|
|
|
2243
3819
|
};
|
|
2244
3820
|
}
|
|
2245
3821
|
|
|
2246
|
-
// spex-console/src/lib/cli/workspace.ts
|
|
2247
|
-
init_registry_file();
|
|
2248
|
-
|
|
2249
3822
|
// spex-console/src/lib/vault-registry/workspace-create.ts
|
|
2250
|
-
init_vault_workspace_shared();
|
|
2251
|
-
init_registry_file();
|
|
2252
3823
|
var MAX_DISPLAY_NAME_LEN = 60;
|
|
2253
3824
|
var MAX_SLUG_LEN = 40;
|
|
2254
3825
|
var MAX_SLUG_SUFFIX = 9;
|
|
@@ -2285,7 +3856,7 @@ function exhaustedSlugError(trimmedName, base) {
|
|
|
2285
3856
|
message: `could not find a free slug for "${trimmedName}" -- ${base} through ${base}-${MAX_SLUG_SUFFIX} are all taken`
|
|
2286
3857
|
};
|
|
2287
3858
|
}
|
|
2288
|
-
function createOffline(input, trimmedName, now, deps) {
|
|
3859
|
+
function createOffline(input, trimmedName, now, deps, neonWriteReason) {
|
|
2289
3860
|
const existingIds = new Set(loadVaultRegistry(deps.registryOpts).workspaces.map((w) => w.id));
|
|
2290
3861
|
let id;
|
|
2291
3862
|
if (input.id !== void 0) {
|
|
@@ -2303,10 +3874,22 @@ function createOffline(input, trimmedName, now, deps) {
|
|
|
2303
3874
|
if (!free) return exhaustedSlugError(trimmedName, base);
|
|
2304
3875
|
id = free;
|
|
2305
3876
|
}
|
|
2306
|
-
|
|
3877
|
+
const registry = runRegistrySideEffect(
|
|
3878
|
+
() => registerWorkspace(
|
|
3879
|
+
{ id, display_name: trimmedName, created_at: now, account_id: input.accountId },
|
|
3880
|
+
deps.registryOpts
|
|
3881
|
+
)
|
|
3882
|
+
);
|
|
2307
3883
|
return {
|
|
2308
3884
|
ok: true,
|
|
2309
|
-
workspace: {
|
|
3885
|
+
workspace: {
|
|
3886
|
+
id,
|
|
3887
|
+
display_name: trimmedName,
|
|
3888
|
+
neon_written: false,
|
|
3889
|
+
...neonWriteReason ? { neon_write_reason: neonWriteReason } : {},
|
|
3890
|
+
registry_written: registryWritten(registry),
|
|
3891
|
+
side_effects: [registry]
|
|
3892
|
+
}
|
|
2310
3893
|
};
|
|
2311
3894
|
}
|
|
2312
3895
|
async function createWorkspace(input, deps = {}) {
|
|
@@ -2328,19 +3911,40 @@ async function createWorkspace(input, deps = {}) {
|
|
|
2328
3911
|
};
|
|
2329
3912
|
}
|
|
2330
3913
|
const id = input.id;
|
|
2331
|
-
|
|
2332
|
-
|
|
3914
|
+
if (isReservedForOtherAccountsDefault(id, input.accountId)) {
|
|
3915
|
+
return {
|
|
3916
|
+
ok: false,
|
|
3917
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
3918
|
+
message: workspaceOwnedByOtherAccountMessage(id)
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
const upserted = await upsertWorkspaceNeonReasoned(
|
|
2333
3922
|
{ id, displayName: trimmedName },
|
|
2334
3923
|
input.accountId,
|
|
2335
3924
|
deps.neon
|
|
2336
3925
|
);
|
|
3926
|
+
if (upserted.reason === "owned-by-other-account") {
|
|
3927
|
+
return {
|
|
3928
|
+
ok: false,
|
|
3929
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
3930
|
+
message: workspaceOwnedByOtherAccountMessage(id)
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
const registry = runRegistrySideEffect(
|
|
3934
|
+
() => registerWorkspace(
|
|
3935
|
+
{ id, display_name: trimmedName, created_at: now, account_id: input.accountId },
|
|
3936
|
+
deps.registryOpts
|
|
3937
|
+
)
|
|
3938
|
+
);
|
|
2337
3939
|
return {
|
|
2338
3940
|
ok: true,
|
|
2339
3941
|
workspace: {
|
|
2340
3942
|
id,
|
|
2341
3943
|
display_name: trimmedName,
|
|
2342
|
-
neon_written:
|
|
2343
|
-
|
|
3944
|
+
neon_written: upserted.written,
|
|
3945
|
+
...upserted.reason ? { neon_write_reason: upserted.reason } : {},
|
|
3946
|
+
registry_written: registryWritten(registry),
|
|
3947
|
+
side_effects: [registry]
|
|
2344
3948
|
}
|
|
2345
3949
|
};
|
|
2346
3950
|
}
|
|
@@ -2351,17 +3955,31 @@ async function createWorkspace(input, deps = {}) {
|
|
|
2351
3955
|
message: `invalid workspace id "${input.id}" -- must be non-empty with no path separators`
|
|
2352
3956
|
};
|
|
2353
3957
|
}
|
|
3958
|
+
if (input.id !== void 0 && isReservedForOtherAccountsDefault(input.id, input.accountId)) {
|
|
3959
|
+
return {
|
|
3960
|
+
ok: false,
|
|
3961
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
3962
|
+
message: workspaceOwnedByOtherAccountMessage(input.id)
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
2354
3965
|
const { base, candidates } = input.id !== void 0 ? { base: input.id, candidates: [input.id] } : derivedCandidates(trimmedName);
|
|
2355
3966
|
for (const candidate of candidates) {
|
|
2356
|
-
const result = await
|
|
3967
|
+
const { result, reason } = await insertWorkspaceNeonStrictReasoned(
|
|
2357
3968
|
{ id: candidate, displayName: trimmedName },
|
|
2358
3969
|
input.accountId,
|
|
2359
3970
|
deps.neon ?? {}
|
|
2360
3971
|
);
|
|
2361
3972
|
if (result === "inserted") {
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
3973
|
+
const registry = runRegistrySideEffect(
|
|
3974
|
+
() => registerWorkspace(
|
|
3975
|
+
{
|
|
3976
|
+
id: candidate,
|
|
3977
|
+
display_name: trimmedName,
|
|
3978
|
+
created_at: now,
|
|
3979
|
+
account_id: input.accountId
|
|
3980
|
+
},
|
|
3981
|
+
deps.registryOpts
|
|
3982
|
+
)
|
|
2365
3983
|
);
|
|
2366
3984
|
return {
|
|
2367
3985
|
ok: true,
|
|
@@ -2369,18 +3987,28 @@ async function createWorkspace(input, deps = {}) {
|
|
|
2369
3987
|
id: candidate,
|
|
2370
3988
|
display_name: trimmedName,
|
|
2371
3989
|
neon_written: true,
|
|
2372
|
-
registry_written:
|
|
3990
|
+
registry_written: registryWritten(registry),
|
|
3991
|
+
side_effects: [registry]
|
|
2373
3992
|
}
|
|
2374
3993
|
};
|
|
2375
3994
|
}
|
|
2376
3995
|
if (result === "unreachable") {
|
|
2377
|
-
return createOffline(input, trimmedName, now, deps);
|
|
3996
|
+
return createOffline(input, trimmedName, now, deps, reason);
|
|
2378
3997
|
}
|
|
2379
3998
|
if (input.id !== void 0) {
|
|
3999
|
+
const owner = await findWorkspaceForAccountNeon(input.id, input.accountId, deps.neon ?? {});
|
|
4000
|
+
if (owner === null) {
|
|
4001
|
+
return {
|
|
4002
|
+
ok: false,
|
|
4003
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
4004
|
+
message: workspaceOwnedByOtherAccountMessage(input.id)
|
|
4005
|
+
};
|
|
4006
|
+
}
|
|
2380
4007
|
return {
|
|
2381
4008
|
ok: false,
|
|
2382
4009
|
code: "duplicate_workspace",
|
|
2383
|
-
message: `workspace "${input.id}" already exists
|
|
4010
|
+
message: `workspace "${input.id}" already exists`,
|
|
4011
|
+
...owner !== "unreachable" ? { owned_by_caller: true } : {}
|
|
2384
4012
|
};
|
|
2385
4013
|
}
|
|
2386
4014
|
}
|
|
@@ -2388,12 +4016,32 @@ async function createWorkspace(input, deps = {}) {
|
|
|
2388
4016
|
}
|
|
2389
4017
|
|
|
2390
4018
|
// spex-console/src/lib/vault-registry/workspace-manage.ts
|
|
2391
|
-
init_vault_workspace_shared();
|
|
2392
|
-
init_registry_file();
|
|
2393
4019
|
var MAX_DISPLAY_NAME_LEN2 = 60;
|
|
2394
4020
|
function notFound(id) {
|
|
2395
4021
|
return { ok: false, code: "workspace_not_found", message: `workspace "${id}" not found` };
|
|
2396
4022
|
}
|
|
4023
|
+
function notOwned(id) {
|
|
4024
|
+
return {
|
|
4025
|
+
ok: false,
|
|
4026
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
4027
|
+
message: workspaceNotOwnedByCallerMessage(id)
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
function neonUnwritten2(id) {
|
|
4031
|
+
return {
|
|
4032
|
+
ok: false,
|
|
4033
|
+
code: "neon_unwritten",
|
|
4034
|
+
message: `workspace "${id}" was not written to the cloud registry (database unreachable); nothing was changed. Retry once the console can reach its database.`
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
function localRow(id, registryOpts) {
|
|
4038
|
+
const local = isLocalRegistryHost();
|
|
4039
|
+
const existing = local ? loadVaultRegistry(registryOpts).workspaces.find((w) => w.id === id) : void 0;
|
|
4040
|
+
return { local, existing };
|
|
4041
|
+
}
|
|
4042
|
+
function cloudWriteFailed2(failure) {
|
|
4043
|
+
return { ok: false, code: "cloud_write_failed", message: failure.message };
|
|
4044
|
+
}
|
|
2397
4045
|
async function renameWorkspace(input, deps = {}) {
|
|
2398
4046
|
const trimmedName = input.displayName.trim();
|
|
2399
4047
|
if (trimmedName.length < 1 || trimmedName.length > MAX_DISPLAY_NAME_LEN2) {
|
|
@@ -2403,9 +4051,8 @@ async function renameWorkspace(input, deps = {}) {
|
|
|
2403
4051
|
message: `display name must be 1-${MAX_DISPLAY_NAME_LEN2} characters (trimmed)`
|
|
2404
4052
|
};
|
|
2405
4053
|
}
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
if (!existing) return notFound(input.id);
|
|
4054
|
+
const { local, existing } = localRow(input.id, deps.registryOpts);
|
|
4055
|
+
if (local && !existing) return notFound(input.id);
|
|
2409
4056
|
const neonResult = await renameWorkspaceNeon(
|
|
2410
4057
|
input.id,
|
|
2411
4058
|
trimmedName,
|
|
@@ -2413,46 +4060,351 @@ async function renameWorkspace(input, deps = {}) {
|
|
|
2413
4060
|
deps.neon ?? {}
|
|
2414
4061
|
);
|
|
2415
4062
|
if (neonResult === "not_found") return notFound(input.id);
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
},
|
|
2421
|
-
|
|
2422
|
-
);
|
|
4063
|
+
if (neonResult === "owned_by_other_account") return notOwned(input.id);
|
|
4064
|
+
if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
|
|
4065
|
+
if (neonResult === "unreachable" && !existing) return neonUnwritten2(input.id);
|
|
4066
|
+
if (existing) {
|
|
4067
|
+
registerWorkspace({ ...existing, display_name: trimmedName }, deps.registryOpts);
|
|
4068
|
+
}
|
|
2423
4069
|
return {
|
|
2424
4070
|
ok: true,
|
|
2425
4071
|
workspace: {
|
|
2426
4072
|
id: input.id,
|
|
2427
4073
|
display_name: trimmedName,
|
|
2428
4074
|
neon_written: neonResult === "updated",
|
|
2429
|
-
registry_written:
|
|
4075
|
+
registry_written: existing !== void 0
|
|
2430
4076
|
}
|
|
2431
4077
|
};
|
|
2432
4078
|
}
|
|
2433
4079
|
async function unregisterWorkspace(input, deps = {}) {
|
|
2434
|
-
const
|
|
2435
|
-
|
|
2436
|
-
if (!existing) return notFound(input.id);
|
|
4080
|
+
const { local, existing } = localRow(input.id, deps.registryOpts);
|
|
4081
|
+
if (local && !existing) return notFound(input.id);
|
|
2437
4082
|
const neonResult = await deleteWorkspaceNeon(input.id, input.accountId, deps.neon ?? {});
|
|
2438
4083
|
if (neonResult === "not_found") return notFound(input.id);
|
|
4084
|
+
if (neonResult === "owned_by_other_account") return notOwned(input.id);
|
|
4085
|
+
if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
|
|
4086
|
+
if (neonResult === "unreachable" && !existing) return neonUnwritten2(input.id);
|
|
2439
4087
|
await repointActiveWorkspaceOnRemoval(input.accountId, input.id, {
|
|
2440
4088
|
registryOpts: deps.registryOpts,
|
|
2441
4089
|
accountPointer: { ...deps.neon, registryOpts: deps.registryOpts }
|
|
2442
4090
|
});
|
|
2443
|
-
removeWorkspace(input.id, deps.registryOpts);
|
|
4091
|
+
if (existing) removeWorkspace(input.id, deps.registryOpts);
|
|
2444
4092
|
return {
|
|
2445
4093
|
ok: true,
|
|
2446
4094
|
workspace: {
|
|
2447
4095
|
id: input.id,
|
|
2448
4096
|
neon_written: neonResult === "deleted",
|
|
2449
|
-
registry_written:
|
|
4097
|
+
registry_written: existing !== void 0
|
|
4098
|
+
}
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
// spex-console/src/lib/cli/receipt.ts
|
|
4103
|
+
var RECEIPT_AGREEMENT_FIELDS = [
|
|
4104
|
+
"vault_id",
|
|
4105
|
+
"workspace_id",
|
|
4106
|
+
"account_id",
|
|
4107
|
+
"active_vault_id"
|
|
4108
|
+
];
|
|
4109
|
+
var ALL_FIELDS = [
|
|
4110
|
+
...RECEIPT_AGREEMENT_FIELDS,
|
|
4111
|
+
"display_name",
|
|
4112
|
+
"archived"
|
|
4113
|
+
];
|
|
4114
|
+
function unreadSide(reason) {
|
|
4115
|
+
return {
|
|
4116
|
+
read: false,
|
|
4117
|
+
reason,
|
|
4118
|
+
vault_id: null,
|
|
4119
|
+
workspace_id: null,
|
|
4120
|
+
account_id: null,
|
|
4121
|
+
active_vault_id: null
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
function valueOf(side, field) {
|
|
4125
|
+
const v = side[field];
|
|
4126
|
+
return v === void 0 ? null : v;
|
|
4127
|
+
}
|
|
4128
|
+
function comparable(field, a, b) {
|
|
4129
|
+
return field !== "account_id" || a !== null && b !== null;
|
|
4130
|
+
}
|
|
4131
|
+
function buildReceipt(input) {
|
|
4132
|
+
const local = input.local ?? unreadSide("not read back");
|
|
4133
|
+
const cloud = input.cloud ?? unreadSide("not read back");
|
|
4134
|
+
if (input.failed?.reason) {
|
|
4135
|
+
const target = input.failed.side === "cloud" ? cloud : local;
|
|
4136
|
+
target.reason = input.failed.reason;
|
|
4137
|
+
}
|
|
4138
|
+
const verified = local.read && cloud.read;
|
|
4139
|
+
const expected = input.expected ?? {};
|
|
4140
|
+
const fields = ALL_FIELDS.filter(
|
|
4141
|
+
(f) => RECEIPT_AGREEMENT_FIELDS.includes(f) || Object.hasOwn(expected, f)
|
|
4142
|
+
);
|
|
4143
|
+
const diffs = [];
|
|
4144
|
+
if (verified) {
|
|
4145
|
+
for (const field of fields) {
|
|
4146
|
+
const l = valueOf(local, field);
|
|
4147
|
+
const c = valueOf(cloud, field);
|
|
4148
|
+
const hasExpected = Object.hasOwn(expected, field);
|
|
4149
|
+
const want = hasExpected ? expected[field] ?? null : void 0;
|
|
4150
|
+
const sidesDiffer = comparable(field, l, c) && l !== c;
|
|
4151
|
+
const offTarget = hasExpected && (l !== want || c !== want);
|
|
4152
|
+
if (sidesDiffer || offTarget) {
|
|
4153
|
+
diffs.push({ field, local: l, cloud: c, ...hasExpected ? { expected: want } : {} });
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
const changed = [];
|
|
4158
|
+
for (const side of ["local", "cloud"]) {
|
|
4159
|
+
const before = input.before?.[side];
|
|
4160
|
+
const after = side === "local" ? local : cloud;
|
|
4161
|
+
if (!before?.read || !after.read) continue;
|
|
4162
|
+
for (const field of ALL_FIELDS) {
|
|
4163
|
+
const from = valueOf(before, field);
|
|
4164
|
+
const to = valueOf(after, field);
|
|
4165
|
+
if (from !== to) changed.push({ side, field, from, to });
|
|
2450
4166
|
}
|
|
4167
|
+
}
|
|
4168
|
+
return {
|
|
4169
|
+
local,
|
|
4170
|
+
cloud,
|
|
4171
|
+
verified,
|
|
4172
|
+
agree: verified && diffs.length === 0,
|
|
4173
|
+
diffs,
|
|
4174
|
+
changed,
|
|
4175
|
+
...input.failed ? { failed: input.failed.side } : {},
|
|
4176
|
+
...input.retry ? { retry: input.retry } : {}
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
function withReceipt(result, receipt) {
|
|
4180
|
+
return {
|
|
4181
|
+
...result,
|
|
4182
|
+
message: [result.message, formatReceipt(receipt)].filter(Boolean).join("\n"),
|
|
4183
|
+
payload: { ...result.payload, receipt }
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
function shellArg(value) {
|
|
4187
|
+
return /^[\w@%+=:,./\\-]+$/.test(value) ? value : JSON.stringify(value);
|
|
4188
|
+
}
|
|
4189
|
+
function accountFlag(account) {
|
|
4190
|
+
return account ? ` --account ${shellArg(account)}` : "";
|
|
4191
|
+
}
|
|
4192
|
+
function receiptStatus(receipt) {
|
|
4193
|
+
if (receipt.failed) return "failed";
|
|
4194
|
+
if (!receipt.verified) return "unverified";
|
|
4195
|
+
if (!receipt.agree) return "disagree";
|
|
4196
|
+
return "verified";
|
|
4197
|
+
}
|
|
4198
|
+
var LABELS = {
|
|
4199
|
+
vault_id: "vault",
|
|
4200
|
+
workspace_id: "workspace",
|
|
4201
|
+
account_id: "account",
|
|
4202
|
+
active_vault_id: "active vault",
|
|
4203
|
+
display_name: "name",
|
|
4204
|
+
archived: "archived"
|
|
4205
|
+
};
|
|
4206
|
+
function cell(side, field) {
|
|
4207
|
+
if (!side.read) return "(not read)";
|
|
4208
|
+
const v = valueOf(side, field);
|
|
4209
|
+
if (v === null) return "(none)";
|
|
4210
|
+
if (typeof v === "boolean") return v ? "yes" : "no";
|
|
4211
|
+
return v;
|
|
4212
|
+
}
|
|
4213
|
+
function formatReceipt(receipt) {
|
|
4214
|
+
const shown = ALL_FIELDS.filter(
|
|
4215
|
+
(f) => RECEIPT_AGREEMENT_FIELDS.includes(f) || receipt.local[f] !== void 0 || receipt.cloud[f] !== void 0
|
|
4216
|
+
);
|
|
4217
|
+
const rows = [
|
|
4218
|
+
["", "local (this machine)", "cloud (console)"],
|
|
4219
|
+
...shown.map((f) => [
|
|
4220
|
+
LABELS[f],
|
|
4221
|
+
cell(receipt.local, f),
|
|
4222
|
+
cell(receipt.cloud, f)
|
|
4223
|
+
])
|
|
4224
|
+
];
|
|
4225
|
+
const w0 = Math.max(...rows.map((r) => r[0].length));
|
|
4226
|
+
const w1 = Math.max(...rows.map((r) => r[1].length));
|
|
4227
|
+
const lines = [
|
|
4228
|
+
"Read back after the write:",
|
|
4229
|
+
...rows.map(([a, b, c]) => ` ${a.padEnd(w0)} ${b.padEnd(w1)} ${c}`.trimEnd())
|
|
4230
|
+
];
|
|
4231
|
+
const status = receiptStatus(receipt);
|
|
4232
|
+
if (status === "failed") {
|
|
4233
|
+
const side = receipt.failed;
|
|
4234
|
+
const reason = receipt[side].reason;
|
|
4235
|
+
lines.push(
|
|
4236
|
+
`Result: failed -- the ${side} half did not land${reason ? ` (${reason})` : ""}.` + (receipt.retry ? ` Retry: \`${receipt.retry}\`` : "")
|
|
4237
|
+
);
|
|
4238
|
+
} else if (status === "unverified") {
|
|
4239
|
+
const unread = ["local", "cloud"].filter((s) => !receipt[s].read);
|
|
4240
|
+
const why = unread.map((s) => `${s}${receipt[s].reason ? `: ${receipt[s].reason}` : ""}`).join("; ");
|
|
4241
|
+
lines.push(
|
|
4242
|
+
`Result: unverified -- the write was made, but the ${unread.join(" and ")} side was not read back (${why}).`
|
|
4243
|
+
);
|
|
4244
|
+
} else if (status === "disagree") {
|
|
4245
|
+
const fields = receipt.diffs.map((d) => LABELS[d.field]).join(", ");
|
|
4246
|
+
const reads = receipt.cloud_reads && receipt.cloud_reads > 1 ? ` after ${receipt.cloud_reads} reads of the cloud side` : "";
|
|
4247
|
+
const show = (v) => v === void 0 || v === null ? "(none)" : typeof v === "boolean" ? v ? "yes" : "no" : v;
|
|
4248
|
+
lines.push(
|
|
4249
|
+
`Result: the two sides disagree on ${fields}${reads}.` + (receipt.retry ? ` Retry: \`${receipt.retry}\`` : ""),
|
|
4250
|
+
...receipt.diffs.map(
|
|
4251
|
+
(d) => ` ${LABELS[d.field]}: local ${show(d.local)} | cloud ${show(d.cloud)}` + (d.expected !== void 0 ? ` | expected ${show(d.expected)}` : "")
|
|
4252
|
+
)
|
|
4253
|
+
);
|
|
4254
|
+
} else {
|
|
4255
|
+
lines.push("Result: verified -- both sides were read back and agree.");
|
|
4256
|
+
}
|
|
4257
|
+
return lines.join("\n");
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
// spex-console/src/lib/cli/receipt-readback.ts
|
|
4261
|
+
var RECEIPT_CLOUD_READS = 3;
|
|
4262
|
+
var RECEIPT_REREAD_DELAY_MS = 1e3;
|
|
4263
|
+
var realSleep = (ms) => new Promise((resolve5) => {
|
|
4264
|
+
setTimeout(resolve5, ms);
|
|
4265
|
+
});
|
|
4266
|
+
function message(err) {
|
|
4267
|
+
return err instanceof Error ? err.message : String(err);
|
|
4268
|
+
}
|
|
4269
|
+
function readLocalReceiptSide(subject, registryOpts = {}) {
|
|
4270
|
+
try {
|
|
4271
|
+
const registry = loadVaultRegistry(registryOpts);
|
|
4272
|
+
const pointer = accountPointersOf(registry, subject.accountId);
|
|
4273
|
+
const ownerOf = (account) => account && account !== LEGACY_REGISTRY_ACCOUNT_ID ? account : null;
|
|
4274
|
+
if (subject.vaultId) {
|
|
4275
|
+
const row2 = registry.vaults.find((v) => v.id === subject.vaultId);
|
|
4276
|
+
return {
|
|
4277
|
+
read: true,
|
|
4278
|
+
vault_id: row2?.id ?? null,
|
|
4279
|
+
workspace_id: row2?.workspace_id ?? null,
|
|
4280
|
+
account_id: ownerOf(row2?.account_id),
|
|
4281
|
+
active_vault_id: pointer.active_vault_id ?? null,
|
|
4282
|
+
display_name: row2?.display_name ?? null,
|
|
4283
|
+
archived: row2 ? row2.archived ?? false : null
|
|
4284
|
+
};
|
|
4285
|
+
}
|
|
4286
|
+
const row = subject.workspaceId ? registry.workspaces.find((w) => w.id === subject.workspaceId) : void 0;
|
|
4287
|
+
return {
|
|
4288
|
+
read: true,
|
|
4289
|
+
vault_id: null,
|
|
4290
|
+
workspace_id: row?.id ?? null,
|
|
4291
|
+
account_id: ownerOf(row?.account_id),
|
|
4292
|
+
active_vault_id: pointer.active_vault_id ?? null,
|
|
4293
|
+
...subject.workspaceId ? {
|
|
4294
|
+
display_name: row?.display_name ?? null,
|
|
4295
|
+
archived: row ? row.archived ?? false : null
|
|
4296
|
+
} : {}
|
|
4297
|
+
};
|
|
4298
|
+
} catch (err) {
|
|
4299
|
+
return unreadSide(`registry.json could not be read: ${message(err)}`);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
async function readList(http, path4, key) {
|
|
4303
|
+
const res = await cloudHttpRequest(http, "GET", path4);
|
|
4304
|
+
const rows = res.json?.[key];
|
|
4305
|
+
if (!res.ok || !Array.isArray(rows)) {
|
|
4306
|
+
return { reason: `GET ${path4.split("?")[0]} answered HTTP ${res.status}` };
|
|
4307
|
+
}
|
|
4308
|
+
return { rows: rows.filter((r) => Boolean(r) && typeof r === "object") };
|
|
4309
|
+
}
|
|
4310
|
+
var str = (v) => typeof v === "string" && v ? v : null;
|
|
4311
|
+
async function readCloudReceiptSide(subject, http) {
|
|
4312
|
+
if (!http) return unreadSide("no cloud transport on this call");
|
|
4313
|
+
const active = await cloudHttpRequest(http, "GET", "/api/vaults/active");
|
|
4314
|
+
if (!active.ok || !active.json || typeof active.json !== "object") {
|
|
4315
|
+
return unreadSide(`GET /api/vaults/active answered HTTP ${active.status}`);
|
|
4316
|
+
}
|
|
4317
|
+
const activeVaultId = str(active.json.vault_id);
|
|
4318
|
+
if (subject.vaultId) {
|
|
4319
|
+
const list2 = await readList(http, "/api/vaults?include_archived=true", "vaults");
|
|
4320
|
+
if ("reason" in list2) return unreadSide(list2.reason);
|
|
4321
|
+
const row2 = list2.rows.find((r) => r.id === subject.vaultId);
|
|
4322
|
+
return {
|
|
4323
|
+
read: true,
|
|
4324
|
+
vault_id: row2 ? subject.vaultId : null,
|
|
4325
|
+
workspace_id: str(row2?.workspace_id),
|
|
4326
|
+
account_id: row2 && row2.granted !== true ? subject.accountId : null,
|
|
4327
|
+
active_vault_id: activeVaultId,
|
|
4328
|
+
display_name: str(row2?.display_name),
|
|
4329
|
+
archived: row2 ? row2.archived === true : null
|
|
4330
|
+
};
|
|
4331
|
+
}
|
|
4332
|
+
if (!subject.workspaceId) {
|
|
4333
|
+
return {
|
|
4334
|
+
read: true,
|
|
4335
|
+
vault_id: null,
|
|
4336
|
+
workspace_id: null,
|
|
4337
|
+
account_id: null,
|
|
4338
|
+
active_vault_id: activeVaultId
|
|
4339
|
+
};
|
|
4340
|
+
}
|
|
4341
|
+
const list = await readList(http, "/api/workspaces?include_archived=true", "workspaces");
|
|
4342
|
+
if ("reason" in list) return unreadSide(list.reason);
|
|
4343
|
+
const row = list.rows.find((r) => r.id === subject.workspaceId);
|
|
4344
|
+
return {
|
|
4345
|
+
read: true,
|
|
4346
|
+
vault_id: null,
|
|
4347
|
+
workspace_id: row ? subject.workspaceId : null,
|
|
4348
|
+
account_id: row ? subject.accountId : null,
|
|
4349
|
+
active_vault_id: activeVaultId,
|
|
4350
|
+
display_name: str(row?.display_name),
|
|
4351
|
+
archived: row ? row.archived === true : null
|
|
2451
4352
|
};
|
|
2452
4353
|
}
|
|
4354
|
+
async function readSettledReceipt(subject, deps, build) {
|
|
4355
|
+
const sides = await readReceiptSides(subject, deps);
|
|
4356
|
+
let receipt = build(sides);
|
|
4357
|
+
const maxReads = Math.max(1, deps.cloudReads ?? RECEIPT_CLOUD_READS);
|
|
4358
|
+
const sleep2 = deps.sleep ?? realSleep;
|
|
4359
|
+
let reads = 1;
|
|
4360
|
+
while (!receipt.failed && receipt.verified && !receipt.agree && reads < maxReads) {
|
|
4361
|
+
await sleep2(RECEIPT_REREAD_DELAY_MS);
|
|
4362
|
+
const cloud = await readCloudReceiptSide(subject, deps.http);
|
|
4363
|
+
reads += 1;
|
|
4364
|
+
receipt = build({ local: sides.local, cloud });
|
|
4365
|
+
}
|
|
4366
|
+
return { ...receipt, cloud_reads: reads };
|
|
4367
|
+
}
|
|
4368
|
+
function receiptDisagrees(receipt) {
|
|
4369
|
+
return !receipt.failed && receipt.verified && !receipt.agree;
|
|
4370
|
+
}
|
|
4371
|
+
async function finishWithReceipt(result, deps, subject, opts) {
|
|
4372
|
+
const failed = opts.failedReason !== void 0;
|
|
4373
|
+
const receipt = await readSettledReceipt(
|
|
4374
|
+
subject,
|
|
4375
|
+
deps,
|
|
4376
|
+
(sides) => buildReceipt({
|
|
4377
|
+
...sides,
|
|
4378
|
+
...opts.before ? { before: opts.before } : {},
|
|
4379
|
+
...opts.expected && !failed ? { expected: opts.expected } : {},
|
|
4380
|
+
...failed ? { failed: { side: "cloud", reason: opts.failedReason } } : {},
|
|
4381
|
+
...failed && opts.retry ? { retry: opts.retry } : {}
|
|
4382
|
+
})
|
|
4383
|
+
);
|
|
4384
|
+
if (failed) {
|
|
4385
|
+
return withReceipt({ ...result, code: EXIT.CLOUD_WRITE_FAILED, isError: true }, receipt);
|
|
4386
|
+
}
|
|
4387
|
+
if (receiptDisagrees(receipt)) {
|
|
4388
|
+
if (opts.retry) receipt.retry = opts.retry;
|
|
4389
|
+
return withReceipt(
|
|
4390
|
+
{
|
|
4391
|
+
...result,
|
|
4392
|
+
code: EXIT.RECEIPT_DISAGREE,
|
|
4393
|
+
isError: true,
|
|
4394
|
+
payload: { ...result.payload, ok: false, error: "receipt_disagree" }
|
|
4395
|
+
},
|
|
4396
|
+
receipt
|
|
4397
|
+
);
|
|
4398
|
+
}
|
|
4399
|
+
return withReceipt(result, receipt);
|
|
4400
|
+
}
|
|
4401
|
+
async function readReceiptSides(subject, deps) {
|
|
4402
|
+
const local = readLocalReceiptSide(subject, deps.registryOpts);
|
|
4403
|
+
const cloud = await readCloudReceiptSide(subject, deps.http);
|
|
4404
|
+
return { local, cloud };
|
|
4405
|
+
}
|
|
2453
4406
|
|
|
2454
4407
|
// spex-console/src/lib/cli/workspace.ts
|
|
2455
|
-
init_vault_workspace_shared();
|
|
2456
4408
|
function parseWorkspaceArgs(argv) {
|
|
2457
4409
|
const subRaw = argv[0];
|
|
2458
4410
|
if (subRaw === "--help" || subRaw === "-h" || subRaw === void 0) {
|
|
@@ -2564,8 +4516,78 @@ function parseWorkspaceArgs(argv) {
|
|
|
2564
4516
|
function accountPointerDeps(deps) {
|
|
2565
4517
|
return { registryOpts: deps.registryOpts, ...deps.neon ?? {} };
|
|
2566
4518
|
}
|
|
4519
|
+
function scopedToActiveVault(deps) {
|
|
4520
|
+
const acct = accountId(deps);
|
|
4521
|
+
let vaultId = null;
|
|
4522
|
+
try {
|
|
4523
|
+
vaultId = accountPointersOf(loadVaultRegistry(deps.registryOpts), acct).active_vault_id ?? null;
|
|
4524
|
+
} catch {
|
|
4525
|
+
}
|
|
4526
|
+
return { ...deps, neon: withCloudScope(deps.neon, acct, vaultId) };
|
|
4527
|
+
}
|
|
4528
|
+
function readWorkspaceSides(deps, subject) {
|
|
4529
|
+
return readReceiptSides(subject, { registryOpts: deps.registryOpts, http: deps.neon?.http });
|
|
4530
|
+
}
|
|
4531
|
+
function finishWorkspaceWrite(result, deps, subject, opts) {
|
|
4532
|
+
return finishWithReceipt(
|
|
4533
|
+
result,
|
|
4534
|
+
{ registryOpts: deps.registryOpts, http: deps.neon?.http, sleep: deps.receiptSleep },
|
|
4535
|
+
subject,
|
|
4536
|
+
opts
|
|
4537
|
+
);
|
|
4538
|
+
}
|
|
4539
|
+
async function manageWithReceipt(verb, workspaceId, effectiveAccountId, deps, retry, write, success, expected) {
|
|
4540
|
+
deps = scopedToActiveVault(deps);
|
|
4541
|
+
const subject = { accountId: effectiveAccountId, workspaceId };
|
|
4542
|
+
const before = await readWorkspaceSides(deps, subject);
|
|
4543
|
+
const result = await write(deps);
|
|
4544
|
+
if (!result.ok) {
|
|
4545
|
+
if (result.code === "cloud_write_failed") {
|
|
4546
|
+
return finishWorkspaceWrite(
|
|
4547
|
+
{
|
|
4548
|
+
code: EXIT.CLOUD_WRITE_FAILED,
|
|
4549
|
+
message: `hs workspace ${verb}: the cloud half failed -- ${result.message} Retry: \`${retry}\``,
|
|
4550
|
+
payload: { ok: false, error: result.code, message: result.message, retry },
|
|
4551
|
+
isError: true
|
|
4552
|
+
},
|
|
4553
|
+
deps,
|
|
4554
|
+
subject,
|
|
4555
|
+
{ before, failedReason: result.message, retry }
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
4558
|
+
return {
|
|
4559
|
+
code: statusForWorkspaceManageError(result.code),
|
|
4560
|
+
message: `hs workspace ${verb}: ${result.message}`,
|
|
4561
|
+
payload: { ok: false, error: result.code, message: result.message },
|
|
4562
|
+
isError: true
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
4565
|
+
const selection = await activeWorkspaceSelection(deps, effectiveAccountId);
|
|
4566
|
+
const done = success(result.workspace, selection);
|
|
4567
|
+
return finishWorkspaceWrite(
|
|
4568
|
+
{ code: EXIT.OK, message: done.message, payload: done.payload, isError: false },
|
|
4569
|
+
deps,
|
|
4570
|
+
subject,
|
|
4571
|
+
{ before, expected, retry }
|
|
4572
|
+
);
|
|
4573
|
+
}
|
|
2567
4574
|
function accountId(deps) {
|
|
2568
|
-
|
|
4575
|
+
if (deps.accountId) return deps.accountId;
|
|
4576
|
+
const strict = resolveCloudAccountIdOrRefuse(deps.accountIdOpts);
|
|
4577
|
+
if (strict.ok) return strict.accountId;
|
|
4578
|
+
throw new Error(`hs workspace: ${strict.message}`);
|
|
4579
|
+
}
|
|
4580
|
+
function strictAccountId(deps) {
|
|
4581
|
+
if (deps.accountId) return { ok: true, accountId: deps.accountId };
|
|
4582
|
+
return resolveCloudAccountIdOrRefuse(deps.accountIdOpts);
|
|
4583
|
+
}
|
|
4584
|
+
function signInRequiredResult(verb, message2) {
|
|
4585
|
+
return {
|
|
4586
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
4587
|
+
message: `hs workspace ${verb}: ${message2}`,
|
|
4588
|
+
payload: { ok: false, error: "sign_in_required", message: message2 },
|
|
4589
|
+
isError: true
|
|
4590
|
+
};
|
|
2569
4591
|
}
|
|
2570
4592
|
async function resolveEffectiveAccountId(deps, requestedAccount, verb) {
|
|
2571
4593
|
const scope = await resolveScopedAccountId(accountId(deps), requestedAccount);
|
|
@@ -2604,7 +4626,9 @@ async function runList(args, deps) {
|
|
|
2604
4626
|
isError: false
|
|
2605
4627
|
};
|
|
2606
4628
|
}
|
|
2607
|
-
const lines = workspaces2.length ? workspaces2.map(
|
|
4629
|
+
const lines = workspaces2.length ? workspaces2.map(
|
|
4630
|
+
(w) => `${w.active ? "* " : " "}${w.id} ${w.display_name}` + (w.origin === "registry-only" ? " (this machine only)" : "")
|
|
4631
|
+
).join("\n") : ' (no workspaces registered -- run `hs workspace create --id <slug> --name "..."`)';
|
|
2608
4632
|
return {
|
|
2609
4633
|
code: EXIT.OK,
|
|
2610
4634
|
message: `${workspaces2.length} registered workspace(s):
|
|
@@ -2613,13 +4637,58 @@ ${lines}`,
|
|
|
2613
4637
|
isError: false
|
|
2614
4638
|
};
|
|
2615
4639
|
}
|
|
4640
|
+
function describeNeonWriteFailure(reason) {
|
|
4641
|
+
switch (reason) {
|
|
4642
|
+
case "not-configured":
|
|
4643
|
+
return "Neon is not configured on this machine";
|
|
4644
|
+
case "unauthorized":
|
|
4645
|
+
return "the cloud write was unauthorized -- run `hs login`";
|
|
4646
|
+
case "network-error":
|
|
4647
|
+
return "the cloud console could not be reached";
|
|
4648
|
+
case "server-error":
|
|
4649
|
+
return "the cloud console rejected the write";
|
|
4650
|
+
case "owned-by-other-account":
|
|
4651
|
+
return "that id belongs to another account";
|
|
4652
|
+
case "workspace-owned-by-other-account":
|
|
4653
|
+
return "that workspace belongs to another account";
|
|
4654
|
+
default:
|
|
4655
|
+
return "Neon unreachable";
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
2616
4658
|
async function runCreate(args, deps) {
|
|
4659
|
+
const strict = strictAccountId(deps);
|
|
4660
|
+
if (!strict.ok) return signInRequiredResult("create", strict.message);
|
|
4661
|
+
deps = scopedToActiveVault(deps);
|
|
4662
|
+
const subject = { accountId: strict.accountId, workspaceId: args.id };
|
|
4663
|
+
const before = await readWorkspaceSides(deps, subject);
|
|
4664
|
+
const retry = `hs workspace create --id ${shellArg(args.id)} --name ${shellArg(args.name)}`;
|
|
4665
|
+
const holds = (s) => s.read && s.workspace_id === args.id && s.display_name === args.name && s.archived !== true;
|
|
4666
|
+
if (deps.neon?.http && holds(before.local) && holds(before.cloud)) {
|
|
4667
|
+
return finishWorkspaceWrite(
|
|
4668
|
+
{
|
|
4669
|
+
code: EXIT.OK,
|
|
4670
|
+
message: `Workspace "${args.id}" (${args.name}) already exists on both sides -- nothing to change.`,
|
|
4671
|
+
payload: {
|
|
4672
|
+
ok: true,
|
|
4673
|
+
id: args.id,
|
|
4674
|
+
display_name: args.name,
|
|
4675
|
+
unchanged: true,
|
|
4676
|
+
neon_written: false,
|
|
4677
|
+
registry_written: false
|
|
4678
|
+
},
|
|
4679
|
+
isError: false
|
|
4680
|
+
},
|
|
4681
|
+
deps,
|
|
4682
|
+
subject,
|
|
4683
|
+
{ before, expected: { workspace_id: args.id, display_name: args.name }, retry }
|
|
4684
|
+
);
|
|
4685
|
+
}
|
|
2617
4686
|
const result = await createWorkspace(
|
|
2618
|
-
{ accountId: accountId
|
|
4687
|
+
{ accountId: strict.accountId, id: args.id, displayName: args.name },
|
|
2619
4688
|
{ registryOpts: deps.registryOpts, neon: deps.neon, now: deps.now, allowIdUpsert: true }
|
|
2620
4689
|
);
|
|
2621
4690
|
if (!result.ok) {
|
|
2622
|
-
const code = result.code === "duplicate_workspace" ? EXIT.CONFLICT : EXIT.INVALID_ARG;
|
|
4691
|
+
const code = result.code === "duplicate_workspace" || result.code === "workspace_owned_by_other_account" ? EXIT.CONFLICT : EXIT.INVALID_ARG;
|
|
2623
4692
|
return {
|
|
2624
4693
|
code,
|
|
2625
4694
|
message: `hs workspace create: ${result.message}`,
|
|
@@ -2627,15 +4696,36 @@ async function runCreate(args, deps) {
|
|
|
2627
4696
|
isError: true
|
|
2628
4697
|
};
|
|
2629
4698
|
}
|
|
2630
|
-
const { id, display_name, neon_written, registry_written } = result.workspace;
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
4699
|
+
const { id, display_name, neon_written, neon_write_reason, registry_written } = result.workspace;
|
|
4700
|
+
const cloudFailed = !neon_written && deps.neon?.http !== void 0;
|
|
4701
|
+
const failureText = describeNeonWriteFailure(neon_write_reason);
|
|
4702
|
+
return finishWorkspaceWrite(
|
|
4703
|
+
{
|
|
4704
|
+
code: EXIT.OK,
|
|
4705
|
+
message: cloudFailed ? `hs workspace create: created workspace "${id}" (${display_name}) in registry.json only -- the cloud half failed (${failureText}). Retry: \`${retry}\`` : `Created workspace "${id}" (${display_name})` + (neon_written ? " in Neon + registry.json." : ` in registry.json only (${failureText}).`),
|
|
4706
|
+
payload: {
|
|
4707
|
+
ok: !cloudFailed,
|
|
4708
|
+
...cloudFailed ? { error: "cloud_write_failed", retry } : {},
|
|
4709
|
+
id,
|
|
4710
|
+
display_name,
|
|
4711
|
+
neon_written,
|
|
4712
|
+
...neon_write_reason ? { neon_write_reason } : {},
|
|
4713
|
+
registry_written
|
|
4714
|
+
},
|
|
4715
|
+
isError: false
|
|
4716
|
+
},
|
|
4717
|
+
deps,
|
|
4718
|
+
{ ...subject, workspaceId: id },
|
|
4719
|
+
{
|
|
4720
|
+
before,
|
|
4721
|
+
expected: { workspace_id: id, display_name },
|
|
4722
|
+
...cloudFailed ? { failedReason: neon_write_reason ?? "server-error" } : {},
|
|
4723
|
+
retry
|
|
4724
|
+
}
|
|
4725
|
+
);
|
|
2637
4726
|
}
|
|
2638
4727
|
async function runSwitch(args, deps) {
|
|
4728
|
+
deps = scopedToActiveVault(deps);
|
|
2639
4729
|
try {
|
|
2640
4730
|
await setActiveWorkspace(accountId(deps), args.id, accountPointerDeps(deps));
|
|
2641
4731
|
} catch (err) {
|
|
@@ -2647,6 +4737,21 @@ async function runSwitch(args, deps) {
|
|
|
2647
4737
|
isError: true
|
|
2648
4738
|
};
|
|
2649
4739
|
}
|
|
4740
|
+
if (err instanceof CloudPointerWriteError) {
|
|
4741
|
+
const message2 = `hs workspace switch: active workspace pointer not updated in the cloud (${err.message}) -- re-run \`hs workspace switch ${args.id}\` to retry.`;
|
|
4742
|
+
return {
|
|
4743
|
+
code: EXIT.CLOUD_WRITE_FAILED,
|
|
4744
|
+
message: message2,
|
|
4745
|
+
payload: {
|
|
4746
|
+
ok: false,
|
|
4747
|
+
error: "cloud_pointer_failed",
|
|
4748
|
+
workspace_id: args.id,
|
|
4749
|
+
http_status: err.status,
|
|
4750
|
+
message: message2
|
|
4751
|
+
},
|
|
4752
|
+
isError: true
|
|
4753
|
+
};
|
|
4754
|
+
}
|
|
2650
4755
|
throw err;
|
|
2651
4756
|
}
|
|
2652
4757
|
return {
|
|
@@ -2661,36 +4766,38 @@ async function activeWorkspaceSelection(deps, effectiveAccountId = accountId(dep
|
|
|
2661
4766
|
return { line: `Active workspace: ${workspaceId ?? "(none)"}`, workspaceId };
|
|
2662
4767
|
}
|
|
2663
4768
|
function statusForWorkspaceManageError(code) {
|
|
2664
|
-
|
|
4769
|
+
if (code === "workspace_not_found" || code === "vault_not_found") return EXIT.NOT_FOUND;
|
|
4770
|
+
if (code === "workspace_owned_by_other_account" || code === "vault_owned_by_other_account") {
|
|
4771
|
+
return EXIT.CONFLICT;
|
|
4772
|
+
}
|
|
4773
|
+
if (code === "cloud_write_failed" || code === "neon_unwritten") return EXIT.CLOUD_WRITE_FAILED;
|
|
4774
|
+
return EXIT.INVALID_ARG;
|
|
2665
4775
|
}
|
|
2666
4776
|
async function runRename(args, deps) {
|
|
2667
4777
|
const scoped = await resolveEffectiveAccountId(deps, args.account, "rename");
|
|
2668
4778
|
if (!scoped.ok) return scoped.result;
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
const selection = await activeWorkspaceSelection(deps, scoped.accountId);
|
|
2682
|
-
return {
|
|
2683
|
-
code: EXIT.OK,
|
|
2684
|
-
message: `Renamed workspace "${result.workspace.id}" to "${result.workspace.display_name}".
|
|
4779
|
+
return manageWithReceipt(
|
|
4780
|
+
"rename",
|
|
4781
|
+
args.id,
|
|
4782
|
+
scoped.accountId,
|
|
4783
|
+
deps,
|
|
4784
|
+
`hs workspace rename ${shellArg(args.id)} --name ${shellArg(args.name)}${accountFlag(args.account)}`,
|
|
4785
|
+
(d) => renameWorkspace(
|
|
4786
|
+
{ accountId: scoped.accountId, id: args.id, displayName: args.name },
|
|
4787
|
+
{ registryOpts: d.registryOpts, neon: d.neon }
|
|
4788
|
+
),
|
|
4789
|
+
(workspace, selection) => ({
|
|
4790
|
+
message: `Renamed workspace "${workspace.id}" to "${workspace.display_name}".
|
|
2685
4791
|
${selection.line}`,
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
4792
|
+
payload: {
|
|
4793
|
+
ok: true,
|
|
4794
|
+
id: workspace.id,
|
|
4795
|
+
display_name: workspace.display_name,
|
|
4796
|
+
active_workspace_id: selection.workspaceId
|
|
4797
|
+
}
|
|
4798
|
+
}),
|
|
4799
|
+
{ display_name: args.name.trim() }
|
|
4800
|
+
);
|
|
2694
4801
|
}
|
|
2695
4802
|
async function confirmRemoval(deps, displayName, verb, yes) {
|
|
2696
4803
|
if (yes) return true;
|
|
@@ -2714,6 +4821,7 @@ async function runUnregister(args, deps) {
|
|
|
2714
4821
|
const displayName = existing?.display_name ?? args.id;
|
|
2715
4822
|
const proceed = await confirmRemoval(deps, displayName, "unregister", args.yes);
|
|
2716
4823
|
if (!proceed) return cancelledResult("unregister");
|
|
4824
|
+
deps = scopedToActiveVault(deps);
|
|
2717
4825
|
const result = await unregisterWorkspace(
|
|
2718
4826
|
{ accountId: scoped.accountId, id: args.id },
|
|
2719
4827
|
{ registryOpts: deps.registryOpts, neon: deps.neon }
|
|
@@ -2747,62 +4855,59 @@ async function runArchive(args, deps) {
|
|
|
2747
4855
|
const displayName = existing?.display_name ?? args.id;
|
|
2748
4856
|
const proceed = await confirmRemoval(deps, displayName, "archive", args.yes);
|
|
2749
4857
|
if (!proceed) return cancelledResult("archive");
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
const selection = await activeWorkspaceSelection(deps, scoped.accountId);
|
|
2763
|
-
return {
|
|
2764
|
-
code: EXIT.OK,
|
|
2765
|
-
message: `Archived workspace "${result.workspace.id}". ${CLOSE_NOT_DESTROY_STATEMENT}
|
|
4858
|
+
return manageWithReceipt(
|
|
4859
|
+
"archive",
|
|
4860
|
+
args.id,
|
|
4861
|
+
scoped.accountId,
|
|
4862
|
+
deps,
|
|
4863
|
+
`hs workspace archive ${shellArg(args.id)} --yes${accountFlag(args.account)}`,
|
|
4864
|
+
(d) => archiveWorkspace(
|
|
4865
|
+
{ accountId: scoped.accountId, id: args.id },
|
|
4866
|
+
{ registryOpts: d.registryOpts, neon: d.neon }
|
|
4867
|
+
),
|
|
4868
|
+
(workspace, selection) => ({
|
|
4869
|
+
message: `Archived workspace "${workspace.id}". ${CLOSE_NOT_DESTROY_STATEMENT}
|
|
2766
4870
|
${selection.line}`,
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
4871
|
+
payload: {
|
|
4872
|
+
ok: true,
|
|
4873
|
+
id: workspace.id,
|
|
4874
|
+
archived: workspace.archived,
|
|
4875
|
+
active_workspace_id: selection.workspaceId
|
|
4876
|
+
}
|
|
4877
|
+
}),
|
|
4878
|
+
{ archived: true }
|
|
4879
|
+
);
|
|
2775
4880
|
}
|
|
2776
4881
|
async function runRestore(args, deps) {
|
|
2777
4882
|
const scoped = await resolveEffectiveAccountId(deps, args.account, "restore");
|
|
2778
4883
|
if (!scoped.ok) return scoped.result;
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
const selection = await activeWorkspaceSelection(deps, scoped.accountId);
|
|
2792
|
-
return {
|
|
2793
|
-
code: EXIT.OK,
|
|
2794
|
-
message: `Restored workspace "${result.workspace.id}".
|
|
4884
|
+
return manageWithReceipt(
|
|
4885
|
+
"restore",
|
|
4886
|
+
args.id,
|
|
4887
|
+
scoped.accountId,
|
|
4888
|
+
deps,
|
|
4889
|
+
`hs workspace restore ${shellArg(args.id)}${accountFlag(args.account)}`,
|
|
4890
|
+
(d) => restoreWorkspace(
|
|
4891
|
+
{ accountId: scoped.accountId, id: args.id },
|
|
4892
|
+
{ registryOpts: d.registryOpts, neon: d.neon }
|
|
4893
|
+
),
|
|
4894
|
+
(workspace, selection) => ({
|
|
4895
|
+
message: `Restored workspace "${workspace.id}".
|
|
2795
4896
|
${selection.line}`,
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
4897
|
+
payload: {
|
|
4898
|
+
ok: true,
|
|
4899
|
+
id: workspace.id,
|
|
4900
|
+
archived: workspace.archived,
|
|
4901
|
+
active_workspace_id: selection.workspaceId
|
|
4902
|
+
}
|
|
4903
|
+
}),
|
|
4904
|
+
{ archived: false }
|
|
4905
|
+
);
|
|
2804
4906
|
}
|
|
2805
4907
|
async function runWorkspace(args, deps) {
|
|
4908
|
+
const strict = strictAccountId(deps);
|
|
4909
|
+
if (!strict.ok) return signInRequiredResult(args.sub, strict.message);
|
|
4910
|
+
deps = { ...deps, accountId: strict.accountId };
|
|
2806
4911
|
if (args.sub === "list") return runList(args, deps);
|
|
2807
4912
|
if (args.sub === "create") return runCreate(args, deps);
|
|
2808
4913
|
if (args.sub === "switch") return runSwitch(args, deps);
|
|
@@ -2811,7 +4916,7 @@ async function runWorkspace(args, deps) {
|
|
|
2811
4916
|
if (args.sub === "archive") return runArchive(args, deps);
|
|
2812
4917
|
return runRestore(args, deps);
|
|
2813
4918
|
}
|
|
2814
|
-
var
|
|
4919
|
+
var HELP3 = [
|
|
2815
4920
|
"Usage:",
|
|
2816
4921
|
" hs workspace list [--json] List registered workspaces, mark active",
|
|
2817
4922
|
' hs workspace create --id <slug> --name "<name>" Create a workspace (Neon + registry.json)',
|
|
@@ -2841,24 +4946,64 @@ var HELP2 = [
|
|
|
2841
4946
|
" --account <id> Act on a DIFFERENT account's workspace instead of your own",
|
|
2842
4947
|
" (list/rename/unregister/archive/restore only) -- builder/admin-gated",
|
|
2843
4948
|
" server-side; refused for a non-builder caller (exit 15).",
|
|
4949
|
+
" --as <email|id> Confirm the SIGNED-IN account this write is made as (create/rename/",
|
|
4950
|
+
" unregister/archive/restore). Must match the account this machine is",
|
|
4951
|
+
" signed in as, or the command refuses. Not the same as --account:",
|
|
4952
|
+
" --account picks the TARGET account, --as confirms who is acting.",
|
|
4953
|
+
" --confirm-account Confirm whichever account is signed in, without naming it (--as is safer).",
|
|
4954
|
+
" A terminal is asked instead; with no terminal, a write verb needs --as or",
|
|
4955
|
+
" --confirm-account (exit 17 otherwise). A machine whose config.yaml",
|
|
4956
|
+
" account_id disagrees with its sign-in is always refused.",
|
|
2844
4957
|
" --json Emit a machine-readable JSON result.",
|
|
2845
4958
|
" --help, -h Show this help.",
|
|
2846
4959
|
"",
|
|
2847
|
-
"
|
|
4960
|
+
"A workspace id that belongs to ANOTHER account is never overwritten: create refuses (exit 9).",
|
|
4961
|
+
"",
|
|
4962
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 9 conflict \xB7 10 not-found \xB7 11 bad-arg \xB7 15 forbidden (--account) \xB7",
|
|
4963
|
+
"12 console unreachable (account check) \xB7 16 sign-in-required (create -- run `hs login`) \xB7",
|
|
4964
|
+
"17 account not confirmed (--as / --confirm-account)"
|
|
2848
4965
|
].join("\n");
|
|
2849
4966
|
|
|
4967
|
+
// spex-console/bin/run-entry.ts
|
|
4968
|
+
import process3 from "node:process";
|
|
4969
|
+
var BinExit = class extends Error {
|
|
4970
|
+
code;
|
|
4971
|
+
constructor(code) {
|
|
4972
|
+
super(`exit ${code}`);
|
|
4973
|
+
this.name = "BinExit";
|
|
4974
|
+
this.code = code;
|
|
4975
|
+
}
|
|
4976
|
+
};
|
|
4977
|
+
function runEntry(name, main2, options = {}) {
|
|
4978
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
|
|
4979
|
+
void (async () => {
|
|
4980
|
+
try {
|
|
4981
|
+
await main2();
|
|
4982
|
+
} catch (err) {
|
|
4983
|
+
if (err instanceof BinExit) {
|
|
4984
|
+
process3.exitCode = err.code;
|
|
4985
|
+
return;
|
|
4986
|
+
}
|
|
4987
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
4988
|
+
process3.stderr.write(`${name}: ${unhandledLabel}: ${message2}
|
|
4989
|
+
`);
|
|
4990
|
+
process3.exitCode = fallbackCode;
|
|
4991
|
+
}
|
|
4992
|
+
})();
|
|
4993
|
+
}
|
|
4994
|
+
|
|
2850
4995
|
// spex-console/bin/hs-workspace.ts
|
|
2851
4996
|
function readFile(p) {
|
|
2852
4997
|
try {
|
|
2853
|
-
return
|
|
4998
|
+
return readFileSync10(p, "utf8");
|
|
2854
4999
|
} catch {
|
|
2855
5000
|
return null;
|
|
2856
5001
|
}
|
|
2857
5002
|
}
|
|
2858
|
-
var INTERACTIVE =
|
|
5003
|
+
var INTERACTIVE = process4.stdin.isTTY === true;
|
|
2859
5004
|
async function confirm(question) {
|
|
2860
5005
|
if (!INTERACTIVE) return false;
|
|
2861
|
-
const rl =
|
|
5006
|
+
const rl = createInterface2({ input: process4.stdin, output: process4.stdout });
|
|
2862
5007
|
try {
|
|
2863
5008
|
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
2864
5009
|
return answer === "y" || answer === "yes" || answer === "proceed";
|
|
@@ -2867,32 +5012,51 @@ async function confirm(question) {
|
|
|
2867
5012
|
}
|
|
2868
5013
|
}
|
|
2869
5014
|
async function main() {
|
|
2870
|
-
const
|
|
5015
|
+
const identityArgv = extractWriteIdentityFlags(process4.argv.slice(3));
|
|
5016
|
+
if (identityArgv.error) {
|
|
5017
|
+
process4.stderr.write(`hs workspace: ${identityArgv.error}
|
|
5018
|
+
`);
|
|
5019
|
+
process4.exitCode = EXIT.INVALID_ARG;
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
5022
|
+
const parsed = parseWorkspaceArgs(identityArgv.rest);
|
|
2871
5023
|
if (parsed.help) {
|
|
2872
|
-
|
|
5024
|
+
process4.stdout.write(`${HELP3}
|
|
2873
5025
|
`);
|
|
2874
|
-
|
|
5026
|
+
process4.exitCode = EXIT.OK;
|
|
5027
|
+
return;
|
|
2875
5028
|
}
|
|
2876
5029
|
if (parsed.error) {
|
|
2877
|
-
|
|
5030
|
+
process4.stderr.write(`hs workspace: ${parsed.error.message}
|
|
2878
5031
|
`);
|
|
2879
|
-
|
|
5032
|
+
process4.exitCode = parsed.error.code;
|
|
5033
|
+
return;
|
|
2880
5034
|
}
|
|
2881
|
-
const vaultRoot =
|
|
5035
|
+
const vaultRoot = process4.env.HS_VAULT_ROOT ? path3.resolve(process4.env.HS_VAULT_ROOT) : process4.cwd();
|
|
2882
5036
|
const consoleUrl = resolveSpexConsoleUrl(vaultRoot, readFile);
|
|
5037
|
+
const gate = await gateWriteVerb({
|
|
5038
|
+
namespace: "hs workspace",
|
|
5039
|
+
sub: parsed.args.sub,
|
|
5040
|
+
writeVerbs: WORKSPACE_WRITE_VERBS,
|
|
5041
|
+
flags: identityArgv.flags,
|
|
5042
|
+
io: defaultIdentityGateIo({ consoleUrl, platformRoot: path3.resolve(process4.cwd(), "..") })
|
|
5043
|
+
});
|
|
5044
|
+
if (gate && !gate.ok) {
|
|
5045
|
+
process4.stderr.write(
|
|
5046
|
+
`${parsed.args.json ? JSON.stringify(gate.result.payload) : gate.result.message}
|
|
5047
|
+
`
|
|
5048
|
+
);
|
|
5049
|
+
process4.exitCode = gate.result.code;
|
|
5050
|
+
return;
|
|
5051
|
+
}
|
|
2883
5052
|
const result = await runWorkspace(parsed.args, {
|
|
2884
5053
|
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
2885
5054
|
confirm,
|
|
2886
5055
|
neon: { http: { fetchImpl: fetch, consoleUrl, token: readPairedToken() } }
|
|
2887
5056
|
});
|
|
2888
|
-
const sink = result.isError ?
|
|
5057
|
+
const sink = result.isError ? process4.stderr : process4.stdout;
|
|
2889
5058
|
sink.write(`${parsed.args.json ? JSON.stringify(result.payload) : result.message}
|
|
2890
5059
|
`);
|
|
2891
|
-
|
|
5060
|
+
process4.exitCode = result.code;
|
|
2892
5061
|
}
|
|
2893
|
-
|
|
2894
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2895
|
-
process2.stderr.write(`hs workspace: unhandled: ${message}
|
|
2896
|
-
`);
|
|
2897
|
-
process2.exit(EXIT.WRITE_FAILED);
|
|
2898
|
-
});
|
|
5062
|
+
runEntry("hs workspace", main, { fallbackCode: EXIT.WRITE_FAILED });
|