heyspex 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/bin/hs.mjs +931 -98
- 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 +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- 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 +28 -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 +26 -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 +31 -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 +30 -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/mcp-client-config.md +259 -6
- package/package.json +231 -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/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -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 +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -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-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 +575 -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/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 +66 -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 +247 -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 +449 -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/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -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 +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -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 +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -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 +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- 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/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 +441 -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/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 +274 -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/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -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 +819 -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 +95 -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 +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -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 +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -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 +124 -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 +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -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 +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -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 +906 -21
- 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 +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -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 +660 -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 +29 -9
- 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 +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -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/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 +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -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 +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -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 +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -5,8 +5,290 @@ var __esm = (fn, res) => function __init() {
|
|
|
5
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
// vendor/heyspex-mcp/credentials.mjs
|
|
9
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
import os, { homedir } from "node:os";
|
|
11
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
12
|
+
function credentialsFilePath(homeDir = homedir()) {
|
|
13
|
+
return join2(homeDir, ".heyspex", "credentials.yaml");
|
|
14
|
+
}
|
|
15
|
+
function parseMcpToken(raw) {
|
|
16
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
17
|
+
const token = raw.match(MCP_TOKEN_LINE_RE)?.[1]?.trim();
|
|
18
|
+
return token ? token : null;
|
|
19
|
+
}
|
|
20
|
+
function isValidAccountId(id) {
|
|
21
|
+
return typeof id === "string" && ACCOUNT_ID_RE.test(id);
|
|
22
|
+
}
|
|
23
|
+
function topLevelScalar(raw, key) {
|
|
24
|
+
const re = new RegExp(`^${key}:[ \\t]*["']?([^"'\\r\\n#]+)`, "m");
|
|
25
|
+
const value = raw.match(re)?.[1]?.trim();
|
|
26
|
+
return value ? value : null;
|
|
27
|
+
}
|
|
28
|
+
function decodeScalar(text2) {
|
|
29
|
+
const t = text2.trim();
|
|
30
|
+
if (t === "" || t === "null" || t === "~") return null;
|
|
31
|
+
if (t.startsWith('"')) {
|
|
32
|
+
try {
|
|
33
|
+
const v = JSON.parse(t);
|
|
34
|
+
return typeof v === "string" && v ? v : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return t.replace(/^"|"$/g, "") || null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (t.startsWith("'")) return t.replace(/^'|'$/g, "") || null;
|
|
40
|
+
const bare = t.replace(/\s+#.*$/, "").trim();
|
|
41
|
+
return bare || null;
|
|
42
|
+
}
|
|
43
|
+
function emptyAccount() {
|
|
44
|
+
return { mcp_token: null, console_url: null, email: null, paired_at: null };
|
|
45
|
+
}
|
|
46
|
+
function findAccountsBlock(lines) {
|
|
47
|
+
const start = lines.findIndex((l) => /^accounts:[ \t]*(#.*)?$/.test(l));
|
|
48
|
+
if (start < 0) return null;
|
|
49
|
+
let end = start + 1;
|
|
50
|
+
while (end < lines.length && (lines[end].trim() === "" || /^[ \t]/.test(lines[end]))) end += 1;
|
|
51
|
+
while (end > start + 1 && lines[end - 1].trim() === "") end -= 1;
|
|
52
|
+
return { start, end };
|
|
53
|
+
}
|
|
54
|
+
function parseAccountsBlock(lines) {
|
|
55
|
+
const accounts = {};
|
|
56
|
+
const block = findAccountsBlock(lines);
|
|
57
|
+
if (!block) return accounts;
|
|
58
|
+
let current = null;
|
|
59
|
+
for (const line of lines.slice(block.start + 1, block.end)) {
|
|
60
|
+
const header = line.match(/^ {2}([^\s:#][^:]*):[ \t]*(#.*)?$/);
|
|
61
|
+
if (header) {
|
|
62
|
+
const id = decodeScalar(header[1]);
|
|
63
|
+
current = isValidAccountId(id) ? id : null;
|
|
64
|
+
if (current) accounts[current] = emptyAccount();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const field = line.match(/^ {4}([a-z_]+):(.*)$/);
|
|
68
|
+
if (field && current && ACCOUNT_FIELDS.includes(field[1])) {
|
|
69
|
+
accounts[current][field[1]] = decodeScalar(field[2]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return accounts;
|
|
73
|
+
}
|
|
74
|
+
function parseCredentialsStore(raw) {
|
|
75
|
+
const text2 = typeof raw === "string" ? raw : "";
|
|
76
|
+
if (topLevelScalar(text2, "schema_version") !== String(CREDENTIALS_SCHEMA_VERSION)) {
|
|
77
|
+
const token = parseMcpToken(text2);
|
|
78
|
+
if (!token) return { schemaVersion: 1, currentAccountId: null, accounts: {} };
|
|
79
|
+
return {
|
|
80
|
+
schemaVersion: 1,
|
|
81
|
+
currentAccountId: LEGACY_ACCOUNT_ID,
|
|
82
|
+
accounts: {
|
|
83
|
+
[LEGACY_ACCOUNT_ID]: { ...emptyAccount(), mcp_token: token, console_url: topLevelScalar(text2, "console_url") }
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const accounts = parseAccountsBlock(text2.split(/\r?\n/));
|
|
88
|
+
const current = topLevelScalar(text2, "current_account_id");
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: 2,
|
|
91
|
+
currentAccountId: current && accounts[current] ? current : null,
|
|
92
|
+
accounts
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function resolveBearer({ envToken, credentialsText, accountId: accountId2, legacyAccountId } = {}) {
|
|
96
|
+
const fromEnv = typeof envToken === "string" ? envToken.trim() : "";
|
|
97
|
+
if (fromEnv) return { token: fromEnv, source: "env" };
|
|
98
|
+
const store = parseCredentialsStore(credentialsText);
|
|
99
|
+
let entry;
|
|
100
|
+
if (typeof accountId2 === "string" && accountId2.trim()) {
|
|
101
|
+
const wanted = accountId2.trim();
|
|
102
|
+
entry = store.accounts[wanted];
|
|
103
|
+
if (!entry && store.schemaVersion === 1 && legacyAccountId && wanted === legacyAccountId) {
|
|
104
|
+
entry = store.accounts[LEGACY_ACCOUNT_ID];
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
entry = store.currentAccountId ? store.accounts[store.currentAccountId] : void 0;
|
|
108
|
+
}
|
|
109
|
+
if (entry?.mcp_token) return { token: entry.mcp_token, source: "credentials-file" };
|
|
110
|
+
return { token: null, source: null };
|
|
111
|
+
}
|
|
112
|
+
function readTextOrNull(read, path4) {
|
|
113
|
+
try {
|
|
114
|
+
const raw = read(path4);
|
|
115
|
+
return typeof raw === "string" ? raw : null;
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function readLegacyConfigAccountId(read, homeDir) {
|
|
121
|
+
const raw = readTextOrNull(read, join2(homeDir, ".heyspex", "config.yaml"));
|
|
122
|
+
const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
|
|
123
|
+
return value || null;
|
|
124
|
+
}
|
|
125
|
+
function readBearer({ env = process.env, homeDir = homedir(), readFile: readFile2, accountId: accountId2 } = {}) {
|
|
126
|
+
const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
|
|
127
|
+
const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
|
|
128
|
+
const named = typeof accountId2 === "string" && accountId2.trim() ? accountId2.trim() : null;
|
|
129
|
+
const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
|
|
130
|
+
return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
|
|
131
|
+
}
|
|
132
|
+
function readCurrentAccountId({ homeDir = homedir(), readFile: readFile2 } = {}) {
|
|
133
|
+
const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
|
|
134
|
+
const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
|
|
135
|
+
if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
|
|
136
|
+
return store.currentAccountId;
|
|
137
|
+
}
|
|
138
|
+
if (store.schemaVersion === 2 && store.currentAccountId === null) return null;
|
|
139
|
+
return readLegacyConfigAccountId(read, homeDir);
|
|
140
|
+
}
|
|
141
|
+
function describeBearerSource(source) {
|
|
142
|
+
if (source === "env") return `${MCP_TOKEN_ENV} (env)`;
|
|
143
|
+
if (source === "credentials-file") return "~/.heyspex/credentials.yaml (mcp_token)";
|
|
144
|
+
return "none";
|
|
145
|
+
}
|
|
146
|
+
var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FIELDS, ACCOUNT_ID_RE, CONFIG_ACCOUNT_ID_RE;
|
|
147
|
+
var init_credentials = __esm({
|
|
148
|
+
"vendor/heyspex-mcp/credentials.mjs"() {
|
|
149
|
+
"use strict";
|
|
150
|
+
MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
|
|
151
|
+
MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
152
|
+
CREDENTIALS_SCHEMA_VERSION = 2;
|
|
153
|
+
LEGACY_ACCOUNT_ID = "legacy";
|
|
154
|
+
ACCOUNT_FIELDS = ["mcp_token", "console_url", "email", "paired_at"];
|
|
155
|
+
ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
|
|
156
|
+
CONFIG_ACCOUNT_ID_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// scripts/lib/dev-mode.mjs
|
|
161
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
162
|
+
import { homedir as homedir6 } from "node:os";
|
|
163
|
+
import { join as join7 } from "node:path";
|
|
164
|
+
function devModeFilePath(homeDir = homedir6()) {
|
|
165
|
+
return join7(homeDir, ".heyspex", DEV_MODE_FILENAME);
|
|
166
|
+
}
|
|
167
|
+
function normalizeMode(raw) {
|
|
168
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
169
|
+
if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
|
|
170
|
+
if (v === "dev") return { mode: "dev", reliance: null };
|
|
171
|
+
if (v === "local") return { mode: "dev", reliance: "local" };
|
|
172
|
+
if (v === "cloud") return { mode: "dev", reliance: "cloud" };
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function normalizeReliance(raw) {
|
|
176
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
177
|
+
return v === "local" || v === "cloud" ? v : null;
|
|
178
|
+
}
|
|
179
|
+
function deriveSemantics(mode, reliance) {
|
|
180
|
+
const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
|
|
181
|
+
const local = mode === "dev" && effectiveReliance === "local";
|
|
182
|
+
return {
|
|
183
|
+
reliance: effectiveReliance,
|
|
184
|
+
consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
|
|
185
|
+
readTier: local ? "disk" : "neon",
|
|
186
|
+
vaultWriteMode: local ? "local" : "cloud"
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function readLocalDevModeRecord({ homeDir = homedir6(), readFile: readFile2 = defaultReadFile } = {}) {
|
|
190
|
+
let raw;
|
|
191
|
+
try {
|
|
192
|
+
raw = readFile2(devModeFilePath(homeDir));
|
|
193
|
+
} catch {
|
|
194
|
+
return { record: null, parseError: null };
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(raw);
|
|
198
|
+
const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
|
|
199
|
+
const reliance = normalizeReliance(parsed?.reliance);
|
|
200
|
+
if (!mode || mode === "dev" && !reliance) {
|
|
201
|
+
return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
record: {
|
|
205
|
+
mode,
|
|
206
|
+
reliance: mode === "prod" ? "cloud" : reliance,
|
|
207
|
+
console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
|
|
208
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
|
|
209
|
+
source: typeof parsed.source === "string" ? parsed.source : null
|
|
210
|
+
},
|
|
211
|
+
parseError: null
|
|
212
|
+
};
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function resolveDevMode({
|
|
218
|
+
env = process.env,
|
|
219
|
+
homeDir = homedir6(),
|
|
220
|
+
readFile: readFile2 = defaultReadFile,
|
|
221
|
+
cloudSetting = void 0
|
|
222
|
+
} = {}) {
|
|
223
|
+
const local = readLocalDevModeRecord({ homeDir, readFile: readFile2 });
|
|
224
|
+
const cloudKnown = cloudSetting !== void 0;
|
|
225
|
+
const cloudValue = cloudKnown && cloudSetting ? {
|
|
226
|
+
mode: cloudSetting.mode === "dev" ? "dev" : "prod",
|
|
227
|
+
reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
|
|
228
|
+
} : null;
|
|
229
|
+
let mode;
|
|
230
|
+
let reliance;
|
|
231
|
+
let source;
|
|
232
|
+
let relianceSource;
|
|
233
|
+
const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
|
|
234
|
+
if (envParsed) {
|
|
235
|
+
mode = envParsed.mode;
|
|
236
|
+
const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
|
|
237
|
+
reliance = envReliance ?? "local";
|
|
238
|
+
source = "env";
|
|
239
|
+
relianceSource = envReliance ? "env" : "assumed-local";
|
|
240
|
+
} else if (local.record) {
|
|
241
|
+
mode = local.record.mode;
|
|
242
|
+
reliance = local.record.reliance;
|
|
243
|
+
source = "local-override";
|
|
244
|
+
} else if (cloudValue) {
|
|
245
|
+
mode = cloudValue.mode;
|
|
246
|
+
reliance = cloudValue.reliance;
|
|
247
|
+
source = "cloud-setting";
|
|
248
|
+
} else {
|
|
249
|
+
mode = "prod";
|
|
250
|
+
reliance = "cloud";
|
|
251
|
+
source = "default";
|
|
252
|
+
}
|
|
253
|
+
const semantics = deriveSemantics(mode, reliance);
|
|
254
|
+
const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
|
|
255
|
+
const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
|
|
256
|
+
return {
|
|
257
|
+
mode,
|
|
258
|
+
reliance: semantics.reliance,
|
|
259
|
+
consoleUrl,
|
|
260
|
+
readTier: semantics.readTier,
|
|
261
|
+
vaultWriteMode: semantics.vaultWriteMode,
|
|
262
|
+
sources: {
|
|
263
|
+
mode: source,
|
|
264
|
+
reliance: relianceSource ?? source,
|
|
265
|
+
consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
|
|
266
|
+
readTier: "derived",
|
|
267
|
+
vaultWriteMode: "derived"
|
|
268
|
+
},
|
|
269
|
+
localOverride: {
|
|
270
|
+
present: local.record !== null,
|
|
271
|
+
record: local.record,
|
|
272
|
+
parseError: local.parseError,
|
|
273
|
+
staleAgainstCloud
|
|
274
|
+
},
|
|
275
|
+
cloudSetting: { known: cloudKnown, value: cloudValue }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
|
|
279
|
+
var init_dev_mode = __esm({
|
|
280
|
+
"scripts/lib/dev-mode.mjs"() {
|
|
281
|
+
"use strict";
|
|
282
|
+
init_credentials();
|
|
283
|
+
CLOUD_CONSOLE_URL = "https://app.heyspex.com";
|
|
284
|
+
LOCAL_CONSOLE_URL = "http://localhost:3000";
|
|
285
|
+
DEV_MODE_FILENAME = "dev-mode.json";
|
|
286
|
+
defaultReadFile = (p) => readFileSync6(p, "utf8");
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
8
290
|
// 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;
|
|
291
|
+
var inert, customType, sql, and, eq, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
|
|
10
292
|
var init_sql_orm_inert_cli_shim = __esm({
|
|
11
293
|
"dist/cli/._sql-orm-inert.cli-shim.mjs"() {
|
|
12
294
|
"use strict";
|
|
@@ -24,6 +306,7 @@ var init_sql_orm_inert_cli_shim = __esm({
|
|
|
24
306
|
return inert;
|
|
25
307
|
}
|
|
26
308
|
});
|
|
309
|
+
customType = inert;
|
|
27
310
|
sql = inert;
|
|
28
311
|
and = inert;
|
|
29
312
|
eq = inert;
|
|
@@ -69,7 +352,7 @@ var init_interfaces = __esm({
|
|
|
69
352
|
});
|
|
70
353
|
|
|
71
354
|
// 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;
|
|
355
|
+
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;
|
|
73
356
|
var init_postgres = __esm({
|
|
74
357
|
"spex-console/src/lib/db/schema/postgres.ts"() {
|
|
75
358
|
"use strict";
|
|
@@ -84,6 +367,11 @@ var init_postgres = __esm({
|
|
|
84
367
|
]);
|
|
85
368
|
validationSeverityEnum = pgEnum("validation_severity", ["error", "warning"]);
|
|
86
369
|
outboxOpEnum = pgEnum("outbox_op", ["upsert", "delete"]);
|
|
370
|
+
tsvector = customType({
|
|
371
|
+
dataType() {
|
|
372
|
+
return "tsvector";
|
|
373
|
+
}
|
|
374
|
+
});
|
|
87
375
|
specs = pgTable(
|
|
88
376
|
"specs",
|
|
89
377
|
{
|
|
@@ -124,11 +412,47 @@ var init_postgres = __esm({
|
|
|
124
412
|
// `family:` field when present, else derives it via `familyOf` (parse
|
|
125
413
|
// fallback from `id`/basename). Nothing should string-parse family from `id`
|
|
126
414
|
// once this column is populated — see `dev-factory/handoffs/NAMING-V3-MIGRATION-BRIEF.md`.
|
|
127
|
-
family: text("family")
|
|
415
|
+
family: text("family"),
|
|
416
|
+
// Lexical search vector ([[HS-0E1G-8KK5-TASK-generated-search-vector-column-gin]]
|
|
417
|
+
// of [[HS-FI33-8KK5-EPIC-lexical-retrieval-in-postgres-generated]]; research:
|
|
418
|
+
// [[HS-HPEH-8KK5-RES-infinitely-scaling-spec-vault-nothing]] §Lexical retrieval).
|
|
419
|
+
// GENERATED ALWAYS … STORED: Postgres rewrites it on every INSERT/UPDATE of
|
|
420
|
+
// `name` / `body_text`, so the write path never touches it and it can never
|
|
421
|
+
// drift from the row. `coalesce` is mandatory — a NULL operand poisons the
|
|
422
|
+
// whole vector (NULL || tsvector = NULL). Title is weight A, body weight B so
|
|
423
|
+
// `ts_rank_cd` ranks a title hit above a body hit. Two-argument
|
|
424
|
+
// `to_tsvector('english', …)` is required inside a generated column (the
|
|
425
|
+
// one-argument form depends on a session setting and is not IMMUTABLE).
|
|
426
|
+
// Still lexical-only: no embeddings ([[retrieval-substrate-lexical-not-embeddings-gate-b-no]]).
|
|
427
|
+
searchVector: tsvector("search_vector").generatedAlwaysAs(
|
|
428
|
+
sql`setweight(to_tsvector('english', coalesce("name", '')), 'A') || setweight(to_tsvector('english', coalesce("body_text", '')), 'B')`
|
|
429
|
+
),
|
|
430
|
+
// Single-column tenant-qualified key ([[HS-ZUZF-8KK5-TASK-per-vault-database-routing-switch-and]]
|
|
431
|
+
// of [[HS-91S9-8KK5-EPIC-large-tenant-tier-bm25-search]]). The specs PRIMARY
|
|
432
|
+
// KEY is COMPOSITE (vault_id, id) — but ParadeDB `pg_search` (the BM25 task,
|
|
433
|
+
// [[HS-KZJ1-8KK5-TASK-bm25-pg-search-index-and-query-path]]) requires a
|
|
434
|
+
// SINGLE-column unique key for its index. `vault_id || ':' || id` is that
|
|
435
|
+
// key: both operands are NOT NULL so no `coalesce` is needed (unlike
|
|
436
|
+
// `search_vector` above), `||` on text is IMMUTABLE so it is legal in a
|
|
437
|
+
// GENERATED … STORED expression, and STORED means the write path never sets
|
|
438
|
+
// it and it can never drift from the row. The unique index below is the
|
|
439
|
+
// constraint pg_search will point at.
|
|
440
|
+
specKey: text("spec_key").generatedAlwaysAs(sql`"vault_id" || ':' || "id"`)
|
|
128
441
|
},
|
|
129
442
|
(table) => [
|
|
130
443
|
// Composite PK (vault_id, id): the same HS-XXXX id may exist once PER VAULT.
|
|
131
444
|
primaryKey({ columns: [table.vaultId, table.id] }),
|
|
445
|
+
// Full-text search: `search_vector @@ websearch_to_tsquery('english', $q)`
|
|
446
|
+
// answers from this GIN index instead of scanning bodies. Built plainly in
|
|
447
|
+
// the migration (scripts/migrate.mjs wraps each migration in a transaction,
|
|
448
|
+
// and CREATE INDEX CONCURRENTLY cannot run inside one); for a large live
|
|
449
|
+
// table use scripts/build-search-indexes-concurrently.mjs
|
|
450
|
+
// (docs/search-index-runbook.md).
|
|
451
|
+
index("specs_search_idx").using("gin", table.searchVector),
|
|
452
|
+
// Near-miss title lookups (`name % $q` / `similarity(name, $q)` / `ILIKE
|
|
453
|
+
// '%term%'`) via pg_trgm. Needs `CREATE EXTENSION pg_trgm` — hand-added to
|
|
454
|
+
// the same migration because drizzle-kit cannot express extensions.
|
|
455
|
+
index("specs_name_trgm_idx").using("gin", table.name.op("gin_trgm_ops")),
|
|
132
456
|
// SpexConsole main list views filter by type + status
|
|
133
457
|
index("specs_type_status_idx").on(table.type, table.status),
|
|
134
458
|
// Family-graph lookups (Naming V3 step 1) — group specs by family key.
|
|
@@ -141,7 +465,22 @@ var init_postgres = __esm({
|
|
|
141
465
|
// (deleted_at IS NULL) so soft-deleted orphans don't block re-indexing. The
|
|
142
466
|
// upsert path soft-deletes any conflicting row first. Scoped by vault so the
|
|
143
467
|
// 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`)
|
|
468
|
+
uniqueIndex("specs_relpath_user_uq").on(table.relPath, table.userId, table.vaultId).where(sql`${table.deletedAt} IS NULL`),
|
|
469
|
+
// Keyset paging ([[HS-C6FO-8KK5-TASK-keyset-spec-list-endpoint-query-layer]]):
|
|
470
|
+
// the immutable sort key is (created, id) — never `updated_at`, which moves
|
|
471
|
+
// edited rows between pages. Partial (deleted_at IS NULL) so soft-deleted
|
|
472
|
+
// rows never cost the index a page. Unfiltered list-query root.
|
|
473
|
+
index("specs_vault_created_id_idx").on(table.vaultId, table.created, table.id).where(sql`${table.deletedAt} IS NULL`),
|
|
474
|
+
// Same keyset shape, pre-filtered by type — the Docs list's most common
|
|
475
|
+
// filter (e.g. `type=task`).
|
|
476
|
+
index("specs_vault_type_created_id_idx").on(table.vaultId, table.type, table.created, table.id).where(sql`${table.deletedAt} IS NULL`),
|
|
477
|
+
// Same keyset shape, pre-filtered by status — the Docs list's other common
|
|
478
|
+
// filter (e.g. `status=open`).
|
|
479
|
+
index("specs_vault_status_created_id_idx").on(table.vaultId, table.status, table.created, table.id).where(sql`${table.deletedAt} IS NULL`),
|
|
480
|
+
// The single-column unique constraint `pg_search` needs (see `specKey`).
|
|
481
|
+
// NOT partial: pg_search indexes the whole table, and a partial unique
|
|
482
|
+
// index would not qualify as its key.
|
|
483
|
+
uniqueIndex("specs_spec_key_uq").on(table.specKey)
|
|
145
484
|
]
|
|
146
485
|
);
|
|
147
486
|
tags = pgTable(
|
|
@@ -221,6 +560,30 @@ var init_postgres = __esm({
|
|
|
221
560
|
index("wikilinks_target_idx").on(table.targetBasename)
|
|
222
561
|
]
|
|
223
562
|
);
|
|
563
|
+
specLinks = pgTable(
|
|
564
|
+
"spec_links",
|
|
565
|
+
{
|
|
566
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
567
|
+
vaultId: text("vault_id").notNull(),
|
|
568
|
+
subjectId: text("subject_id").notNull(),
|
|
569
|
+
objectId: text("object_id"),
|
|
570
|
+
objectKey: text("object_key").notNull(),
|
|
571
|
+
kind: text("kind").notNull().default("wikilink").$type(),
|
|
572
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
573
|
+
},
|
|
574
|
+
(table) => [
|
|
575
|
+
foreignKey({
|
|
576
|
+
columns: [table.vaultId, table.subjectId],
|
|
577
|
+
foreignColumns: [specs.vaultId, specs.id]
|
|
578
|
+
}).onDelete("cascade"),
|
|
579
|
+
// Forward refs: what does this spec point at?
|
|
580
|
+
index("spec_links_subject_idx").on(table.vaultId, table.subjectId, table.id),
|
|
581
|
+
// Backlinks: what points at this spec?
|
|
582
|
+
index("spec_links_object_idx").on(table.vaultId, table.objectId, table.id),
|
|
583
|
+
// Backlinks by basename (unresolved forward references / rename survival).
|
|
584
|
+
index("spec_links_object_key_idx").on(table.vaultId, table.objectKey, table.id)
|
|
585
|
+
]
|
|
586
|
+
);
|
|
224
587
|
aliases = pgTable(
|
|
225
588
|
"aliases",
|
|
226
589
|
{
|
|
@@ -624,6 +987,38 @@ var init_postgres = __esm({
|
|
|
624
987
|
)
|
|
625
988
|
]
|
|
626
989
|
);
|
|
990
|
+
fileClaims = pgTable(
|
|
991
|
+
"file_claims",
|
|
992
|
+
{
|
|
993
|
+
// Tenant-isolation scope ([[HS-ZP5H-TASK]]) — mirrors `active_claims.vault_id`.
|
|
994
|
+
vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
|
|
995
|
+
filePath: text("file_path").notNull(),
|
|
996
|
+
// Persistent human accountable for this lease ([[HS-LLBO-ALE0-RES-multi-user-teams-on-a-shared-spec-vault]]).
|
|
997
|
+
// Resolved from the caller's vault scope at the call site — never a
|
|
998
|
+
// literal `SINGLE_USER_ID` baked in here. Distinct column from `agent_id`
|
|
999
|
+
// below: the agent is the ephemeral worker, never the owner.
|
|
1000
|
+
ownerUserId: text("owner_user_id").notNull().default(SINGLE_USER_ID),
|
|
1001
|
+
agentId: text("agent_id").notNull(),
|
|
1002
|
+
sessionId: text("session_id").notNull(),
|
|
1003
|
+
// Spec id the agent is working on when claiming this file, if any.
|
|
1004
|
+
// Informational only — mirrors `FileClaim.workingSpecId`.
|
|
1005
|
+
workingSpecId: text("working_spec_id"),
|
|
1006
|
+
claimedAt: timestamp("claimed_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1007
|
+
// Reuses `active_claim_status` — same value domain as the file-side
|
|
1008
|
+
// `LeaseStatus` union ("working" | "pending" | "blocked").
|
|
1009
|
+
status: activeClaimStatusEnum("status").notNull().default("working"),
|
|
1010
|
+
lastActivityAt: timestamp("last_activity_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1011
|
+
heartbeatAt: timestamp("heartbeat_at", { withTimezone: true }),
|
|
1012
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
1013
|
+
// Shared global order: DEFAULT nextval on the SAME sequence `active_claims`
|
|
1014
|
+
// and `claim_waiters` mint from — not a per-table bigserial, which would
|
|
1015
|
+
// mint a second, unrelated counter and defeat the shared fencing contract.
|
|
1016
|
+
sequence: bigint("sequence", { mode: "number" }).notNull().unique().default(sql`nextval('active_claims_sequence_seq')`),
|
|
1017
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1018
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
1019
|
+
},
|
|
1020
|
+
(table) => [primaryKey({ columns: [table.vaultId, table.filePath] })]
|
|
1021
|
+
);
|
|
627
1022
|
claimEvents = pgTable(
|
|
628
1023
|
"claim_events",
|
|
629
1024
|
{
|
|
@@ -750,6 +1145,7 @@ var init_postgres = __esm({
|
|
|
750
1145
|
"rate_limit_buckets",
|
|
751
1146
|
{
|
|
752
1147
|
userId: text("user_id").notNull(),
|
|
1148
|
+
vaultId: text("vault_id").notNull().default(""),
|
|
753
1149
|
bucket: text("bucket").notNull(),
|
|
754
1150
|
windowStart: timestamp("window_start", { withTimezone: true }).notNull(),
|
|
755
1151
|
count: integer("count").notNull().default(0),
|
|
@@ -757,7 +1153,7 @@ var init_postgres = __esm({
|
|
|
757
1153
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
758
1154
|
},
|
|
759
1155
|
(table) => [
|
|
760
|
-
primaryKey({ columns: [table.userId, table.bucket, table.windowStart] }),
|
|
1156
|
+
primaryKey({ columns: [table.userId, table.vaultId, table.bucket, table.windowStart] }),
|
|
761
1157
|
// Background reaper / dashboards: enumerate stale rows by `window_start`
|
|
762
1158
|
// older than the longest live window. The hot path uses the composite PK.
|
|
763
1159
|
index("rate_limit_buckets_window_idx").on(table.windowStart)
|
|
@@ -889,6 +1285,17 @@ var init_postgres = __esm({
|
|
|
889
1285
|
},
|
|
890
1286
|
(table) => [primaryKey({ columns: [table.userId, table.vaultId] })]
|
|
891
1287
|
);
|
|
1288
|
+
consoleSettingsStore = pgTable(
|
|
1289
|
+
"console_settings",
|
|
1290
|
+
{
|
|
1291
|
+
userId: text("user_id").notNull().default(SINGLE_USER_ID),
|
|
1292
|
+
vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
|
|
1293
|
+
namespace: text("namespace").notNull(),
|
|
1294
|
+
payload: jsonb("payload").notNull(),
|
|
1295
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
1296
|
+
},
|
|
1297
|
+
(table) => [primaryKey({ columns: [table.userId, table.vaultId, table.namespace] })]
|
|
1298
|
+
);
|
|
892
1299
|
wipLedger = pgTable(
|
|
893
1300
|
"wip_ledger",
|
|
894
1301
|
{
|
|
@@ -901,6 +1308,27 @@ var init_postgres = __esm({
|
|
|
901
1308
|
},
|
|
902
1309
|
(table) => [primaryKey({ columns: [table.vaultId, table.specId] })]
|
|
903
1310
|
);
|
|
1311
|
+
wipLedgerArchive = pgTable(
|
|
1312
|
+
"wip_ledger_archive",
|
|
1313
|
+
{
|
|
1314
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
1315
|
+
vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
|
|
1316
|
+
specId: text("spec_id").notNull(),
|
|
1317
|
+
/** The full WipEntry blob at the moment it was archived — see `WipEntry` in `src/lib/wip-state.ts`. */
|
|
1318
|
+
payload: jsonb("payload").notNull(),
|
|
1319
|
+
archivedAt: timestamp("archived_at", { withTimezone: true }).notNull().default(sql`now()`)
|
|
1320
|
+
},
|
|
1321
|
+
(table) => [
|
|
1322
|
+
// "newest archived record for this (vault, spec)" — the restore lookup.
|
|
1323
|
+
index("wip_ledger_archive_vault_spec_idx").on(
|
|
1324
|
+
table.vaultId,
|
|
1325
|
+
table.specId,
|
|
1326
|
+
table.archivedAt.desc()
|
|
1327
|
+
),
|
|
1328
|
+
// The purge cron's own scan: "every row older than a cutoff, any vault".
|
|
1329
|
+
index("wip_ledger_archive_archived_at_idx").on(table.archivedAt)
|
|
1330
|
+
]
|
|
1331
|
+
);
|
|
904
1332
|
gatherResults = pgTable(
|
|
905
1333
|
"gather_results",
|
|
906
1334
|
{
|
|
@@ -918,6 +1346,16 @@ var init_postgres = __esm({
|
|
|
918
1346
|
* ([[HS-RIZN-DIL5-DIAG-spexcrawl-search-returns-zero-pages-on]]).
|
|
919
1347
|
*/
|
|
920
1348
|
search: jsonb("search"),
|
|
1349
|
+
/**
|
|
1350
|
+
* A completed site walk's duplicate/orphan audit — which pages say
|
|
1351
|
+
* exactly the same thing as each other, and which pages nothing else
|
|
1352
|
+
* links to ([[HS-4TNX-ABF9-TASK-carry-the-site-audit-report-from-a]]).
|
|
1353
|
+
* The second job-level report beside `pages`, carried exactly as
|
|
1354
|
+
* `search` is: open shape (the Companion owns the fields), null for a
|
|
1355
|
+
* single-page gather, a research job, and uploads from Companions that
|
|
1356
|
+
* predate the report.
|
|
1357
|
+
*/
|
|
1358
|
+
siteAudit: jsonb("site_audit"),
|
|
921
1359
|
createdAt: timestamp("created_at", { withTimezone: true }).default(sql`now()`)
|
|
922
1360
|
},
|
|
923
1361
|
(table) => [primaryKey({ columns: [table.jobId, table.idempotencyKey] })]
|
|
@@ -1100,6 +1538,44 @@ var init_postgres = __esm({
|
|
|
1100
1538
|
repoRootAbs: text("repo_root_abs"),
|
|
1101
1539
|
githubRemote: text("github_remote"),
|
|
1102
1540
|
schemaVersion: integer("schema_version").notNull().default(1),
|
|
1541
|
+
/**
|
|
1542
|
+
* Price plan the vault is on — `free` | `pro` | `enterprise`, the same
|
|
1543
|
+
* vocabulary as `ApiKeyPlan` in `api-key-rate-limit.ts`
|
|
1544
|
+
* ([[HS-JRER-8KK5-TASK-per-vault-plan-tier-tier-limits-table]]). Drives the
|
|
1545
|
+
* per-tier statement timeout / work_mem / rate-limit budgets in
|
|
1546
|
+
* `src/lib/plan-tier.ts` (`PLAN_TIER_LIMITS`, `resolveVaultPlanTier`). How a
|
|
1547
|
+
* vault GETS a tier (billing) is out of scope — the column defaults every
|
|
1548
|
+
* vault to `free` so the most conservative limits apply until a tier is set.
|
|
1549
|
+
* Readers must treat an unknown value as `free`.
|
|
1550
|
+
*/
|
|
1551
|
+
planTier: text("plan_tier").notNull().default("free"),
|
|
1552
|
+
/**
|
|
1553
|
+
* Dedicated-database routing key ([[HS-ZUZF-8KK5-TASK-per-vault-database-routing-switch-and]]).
|
|
1554
|
+
*
|
|
1555
|
+
* NULL (the default, and every vault today) = this vault lives in the SHARED
|
|
1556
|
+
* database, i.e. the one `DATABASE_URL` points at. A non-null value is a
|
|
1557
|
+
* short opaque REF — never a connection string and never a secret. The ref
|
|
1558
|
+
* NAMES where the credential lives: environment variable
|
|
1559
|
+
* `HEYSPEX_DEDICATED_DB_<REF>` (uppercased, non-alphanumerics → `_`), or
|
|
1560
|
+
* `dedicated_databases.<ref>` in `~/.heyspex/credentials.yaml`. Resolution
|
|
1561
|
+
* and the per-ref pool memo live in `src/lib/db/clients/vault-router.ts`;
|
|
1562
|
+
* `scripts/migrate.mjs` migrates every ref listed in `DEDICATED_DATABASE_REFS`
|
|
1563
|
+
* alongside the shared database.
|
|
1564
|
+
*
|
|
1565
|
+
* The `vaults` table itself ALWAYS stays on the shared control plane — the
|
|
1566
|
+
* router reads this column through the admin connection, so the row that says
|
|
1567
|
+
* "vault X is elsewhere" is never itself elsewhere.
|
|
1568
|
+
*/
|
|
1569
|
+
databaseRef: text("database_ref"),
|
|
1570
|
+
/**
|
|
1571
|
+
* Which lexical-search substrate this vault's queries use — `gin` (the
|
|
1572
|
+
* built-in `search_vector` GIN index shipped in migration 0068) or `bm25`
|
|
1573
|
+
* (ParadeDB `pg_search`, evaluated by
|
|
1574
|
+
* [[HS-KZJ1-8KK5-TASK-bm25-pg-search-index-and-query-path]]). Declared here
|
|
1575
|
+
* so the BM25 task is a per-vault flag flip rather than a global cutover;
|
|
1576
|
+
* nothing reads it yet. Readers must treat an unknown value as `gin`.
|
|
1577
|
+
*/
|
|
1578
|
+
searchEngine: text("search_engine").notNull().default("gin"),
|
|
1103
1579
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1104
1580
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1105
1581
|
/** Vault equivalent of `workspaces.archived` — see that column's doc comment. */
|
|
@@ -1131,6 +1607,22 @@ var init_postgres = __esm({
|
|
|
1131
1607
|
},
|
|
1132
1608
|
(table) => [primaryKey({ columns: [table.vaultId, table.accountId] })]
|
|
1133
1609
|
);
|
|
1610
|
+
pendingVaultGrants = pgTable(
|
|
1611
|
+
"pending_vault_grants",
|
|
1612
|
+
{
|
|
1613
|
+
id: text("id").primaryKey(),
|
|
1614
|
+
vaultId: text("vault_id").notNull().references(() => vaults.id, { onDelete: "cascade" }),
|
|
1615
|
+
email: text("email").notNull(),
|
|
1616
|
+
access: text("access").notNull().default("read"),
|
|
1617
|
+
invitedBy: text("invited_by").notNull(),
|
|
1618
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().default(sql`now()`),
|
|
1619
|
+
fulfilledAt: timestamp("fulfilled_at", { withTimezone: true }),
|
|
1620
|
+
fulfilledAccountId: text("fulfilled_account_id")
|
|
1621
|
+
},
|
|
1622
|
+
(table) => [
|
|
1623
|
+
uniqueIndex("pending_vault_grants_vault_id_email_idx").on(table.vaultId, table.email)
|
|
1624
|
+
]
|
|
1625
|
+
);
|
|
1134
1626
|
githubInstallations = pgTable(
|
|
1135
1627
|
"github_installations",
|
|
1136
1628
|
{
|
|
@@ -1361,6 +1853,31 @@ var init_postgres = __esm({
|
|
|
1361
1853
|
// new | invited | declined
|
|
1362
1854
|
/** Clerk invitation id, set when an approval actually issues one. */
|
|
1363
1855
|
clerkInvitationId: text("clerk_invitation_id"),
|
|
1856
|
+
/**
|
|
1857
|
+
* The Clerk invitation's accept URL, stored alongside the id at approve
|
|
1858
|
+
* time so the admin surface can show/copy/resend it without a live Clerk
|
|
1859
|
+
* round trip for every render
|
|
1860
|
+
* ([[HS-7DKO-JQ64-TASK-approve-sends-the-customer-a-resend]]).
|
|
1861
|
+
*/
|
|
1862
|
+
clerkInvitationUrl: text("clerk_invitation_url"),
|
|
1863
|
+
/** When the first-party approval email (Resend) was confirmed sent. Null until it succeeds. */
|
|
1864
|
+
approvalEmailSentAt: timestamp("approval_email_sent_at", { withTimezone: true }),
|
|
1865
|
+
/** Why the approval email failed or was skipped — never blocks approval, just recorded. */
|
|
1866
|
+
approvalEmailError: text("approval_email_error"),
|
|
1867
|
+
/**
|
|
1868
|
+
* Resend's message id for the approval email currently on the row — the
|
|
1869
|
+
* key a later `POST /api/webhooks/resend` delivery matches back to this
|
|
1870
|
+
* row ([[HS-HUK3-JQ64-TASK-approval-email-delivery-proof-store-the]]).
|
|
1871
|
+
* Reset (and re-set) on every approve/resend, since each send mints a new
|
|
1872
|
+
* Resend message id.
|
|
1873
|
+
*/
|
|
1874
|
+
approvalEmailId: text("approval_email_id"),
|
|
1875
|
+
/** Resend delivery-webhook outcome: sent | delayed | delivered | bounced | complained. Null until a webhook arrives. */
|
|
1876
|
+
approvalEmailDeliveryStatus: text("approval_email_delivery_status"),
|
|
1877
|
+
/** When the most recently applied delivery-webhook event was recorded. */
|
|
1878
|
+
approvalEmailDeliveryAt: timestamp("approval_email_delivery_at", { withTimezone: true }),
|
|
1879
|
+
/** Bounce/complaint detail from the webhook payload (e.g. the bounce message). */
|
|
1880
|
+
approvalEmailDeliveryDetail: text("approval_email_delivery_detail"),
|
|
1364
1881
|
/** Account id of the admin who approved/declined — audit trail for the decision. */
|
|
1365
1882
|
reviewedBy: text("reviewed_by"),
|
|
1366
1883
|
reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
|
|
@@ -1480,130 +1997,116 @@ var init_postgres = __esm({
|
|
|
1480
1997
|
}
|
|
1481
1998
|
});
|
|
1482
1999
|
|
|
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) {
|
|
2000
|
+
// spex-console/bin/hs-workspace.ts
|
|
2001
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
2002
|
+
import path3 from "node:path";
|
|
2003
|
+
import process3 from "node:process";
|
|
2004
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
2005
|
+
|
|
2006
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
2007
|
+
import path from "node:path";
|
|
2008
|
+
|
|
2009
|
+
// scripts/lib/hs-command.mjs
|
|
2010
|
+
import { existsSync } from "node:fs";
|
|
2011
|
+
import { delimiter, dirname, join } from "node:path";
|
|
2012
|
+
import { fileURLToPath } from "node:url";
|
|
2013
|
+
var HS_BIN_NAME = "hs";
|
|
2014
|
+
var HS_DISPATCHER_PATH = (() => {
|
|
1503
2015
|
try {
|
|
1504
|
-
|
|
2016
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
2017
|
+
return join(dirname(dirname(here)), "bin", "hs.mjs");
|
|
1505
2018
|
} catch {
|
|
2019
|
+
return null;
|
|
1506
2020
|
}
|
|
2021
|
+
})();
|
|
2022
|
+
function shellQuote(p) {
|
|
2023
|
+
return /[\\\s]/.test(p) ? `"${p}"` : p;
|
|
1507
2024
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
emitProbeFailure(options, {
|
|
1526
|
-
reason: "suspended",
|
|
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;
|
|
2025
|
+
var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
|
|
2026
|
+
|
|
2027
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
2028
|
+
init_credentials();
|
|
2029
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2030
|
+
import { homedir as homedir4 } from "node:os";
|
|
2031
|
+
import { join as join5 } from "node:path";
|
|
2032
|
+
|
|
2033
|
+
// vendor/heyspex-mcp/session-owner.mjs
|
|
2034
|
+
init_credentials();
|
|
2035
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2036
|
+
import { homedir as homedir2 } from "node:os";
|
|
2037
|
+
import { join as join3 } from "node:path";
|
|
2038
|
+
function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(), readFile: readFile2 } = {}) {
|
|
2039
|
+
const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
|
|
2040
|
+
if (fromEnv) return fromEnv;
|
|
2041
|
+
return readCurrentAccountId({ homeDir, readFile: readFile2 });
|
|
1555
2042
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
2043
|
+
|
|
2044
|
+
// vendor/heyspex-mcp/vault-less-endpoints.mjs
|
|
2045
|
+
var VAULT_LESS_ENDPOINTS = Object.freeze([
|
|
2046
|
+
"* /api/mcp/session",
|
|
2047
|
+
"* /api/mcp/pairing-codes",
|
|
2048
|
+
"POST /api/vaults",
|
|
2049
|
+
"GET /api/vaults",
|
|
2050
|
+
"POST /api/workspaces",
|
|
2051
|
+
"GET /api/workspaces",
|
|
2052
|
+
"* /api/admin/dev-mode"
|
|
2053
|
+
]);
|
|
2054
|
+
function pathnameOf(path4) {
|
|
2055
|
+
const raw = String(path4 ?? "").split("?")[0].split("#")[0];
|
|
2056
|
+
const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
|
|
2057
|
+
return noTrailing || "/";
|
|
2058
|
+
}
|
|
2059
|
+
function isVaultLessEndpoint(method, path4) {
|
|
2060
|
+
const m = String(method || "GET").toUpperCase();
|
|
2061
|
+
const pathname = pathnameOf(path4);
|
|
2062
|
+
return VAULT_LESS_ENDPOINTS.some((row) => {
|
|
2063
|
+
const [rowMethod, rowPath] = row.split(" ");
|
|
2064
|
+
return rowPath === pathname && (rowMethod === "*" || rowMethod === m);
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
2069
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
2070
|
+
import { homedir as homedir3, tmpdir } from "node:os";
|
|
2071
|
+
import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
|
|
2072
|
+
|
|
2073
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
2074
|
+
var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
|
|
2075
|
+
|
|
2076
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
2077
|
+
init_credentials();
|
|
2078
|
+
|
|
2079
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
2080
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2081
|
+
import { homedir as homedir5 } from "node:os";
|
|
2082
|
+
import { dirname as dirname4, join as join6, resolve as resolve3 } from "node:path";
|
|
1575
2083
|
|
|
1576
2084
|
// spex-console/src/lib/vault-registry/types.ts
|
|
2085
|
+
var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
|
|
2086
|
+
var VAULT_REGISTRY_SCHEMA_VERSION = 2;
|
|
1577
2087
|
function emptyVaultRegistry() {
|
|
1578
2088
|
return {
|
|
1579
2089
|
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
2090
|
+
current_account_id: null,
|
|
2091
|
+
accounts: {},
|
|
1580
2092
|
workspaces: [],
|
|
1581
|
-
vaults: []
|
|
1582
|
-
active_workspace_id: null,
|
|
1583
|
-
active_vault_id: null
|
|
2093
|
+
vaults: []
|
|
1584
2094
|
};
|
|
1585
2095
|
}
|
|
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
2096
|
|
|
1594
2097
|
// 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
2098
|
function vaultRegistryDir(opts = {}) {
|
|
1599
|
-
return opts.registryDir ??
|
|
2099
|
+
return opts.registryDir ?? join6(homedir5(), ".heyspex");
|
|
1600
2100
|
}
|
|
1601
2101
|
function vaultRegistryFilePath(opts = {}) {
|
|
1602
|
-
return
|
|
2102
|
+
return join6(vaultRegistryDir(opts), "registry.json");
|
|
1603
2103
|
}
|
|
1604
2104
|
function isNonEmptyString(v) {
|
|
1605
2105
|
return typeof v === "string" && v.trim().length > 0;
|
|
1606
2106
|
}
|
|
2107
|
+
function accountOf(o) {
|
|
2108
|
+
return isNonEmptyString(o.account_id) ? o.account_id : LEGACY_REGISTRY_ACCOUNT_ID;
|
|
2109
|
+
}
|
|
1607
2110
|
function normalizeWorkspace(raw) {
|
|
1608
2111
|
if (!raw || typeof raw !== "object") return null;
|
|
1609
2112
|
const o = raw;
|
|
@@ -1614,7 +2117,8 @@ function normalizeWorkspace(raw) {
|
|
|
1614
2117
|
id: o.id,
|
|
1615
2118
|
display_name: o.display_name,
|
|
1616
2119
|
created_at: o.created_at,
|
|
1617
|
-
archived: o.archived === true
|
|
2120
|
+
archived: o.archived === true,
|
|
2121
|
+
account_id: accountOf(o)
|
|
1618
2122
|
};
|
|
1619
2123
|
if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
|
|
1620
2124
|
return record;
|
|
@@ -1635,57 +2139,161 @@ function normalizeVault(raw) {
|
|
|
1635
2139
|
repo_root_abs: o.repo_root_abs,
|
|
1636
2140
|
schema_version: o.schema_version,
|
|
1637
2141
|
registered_at: o.registered_at,
|
|
1638
|
-
archived: o.archived === true
|
|
2142
|
+
archived: o.archived === true,
|
|
2143
|
+
account_id: accountOf(o)
|
|
1639
2144
|
};
|
|
1640
2145
|
if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
|
|
1641
2146
|
return record;
|
|
1642
2147
|
}
|
|
2148
|
+
function normalizeStoredPointers(raw) {
|
|
2149
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
2150
|
+
const out = {};
|
|
2151
|
+
for (const field of ["active_vault_id", "active_workspace_id"]) {
|
|
2152
|
+
const v = o[field];
|
|
2153
|
+
if (v === null || isNonEmptyString(v)) out[field] = v;
|
|
2154
|
+
}
|
|
2155
|
+
return out;
|
|
2156
|
+
}
|
|
2157
|
+
function normalizePointers(raw) {
|
|
2158
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
2159
|
+
return {
|
|
2160
|
+
active_vault_id: isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null,
|
|
2161
|
+
active_workspace_id: isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function registryFileSchemaOf(json) {
|
|
2165
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) return null;
|
|
2166
|
+
return json.schema_version === 2 ? 2 : 1;
|
|
2167
|
+
}
|
|
1643
2168
|
function parseVaultRegistryFile(json) {
|
|
1644
|
-
const
|
|
1645
|
-
if (
|
|
2169
|
+
const schema = registryFileSchemaOf(json);
|
|
2170
|
+
if (schema === null) return emptyVaultRegistry();
|
|
1646
2171
|
const o = json;
|
|
1647
2172
|
const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
|
|
1648
2173
|
const workspaces2 = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
|
|
1649
2174
|
const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
|
|
1650
2175
|
const vaults2 = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
|
|
1651
|
-
|
|
1652
|
-
|
|
2176
|
+
if (schema === 1) {
|
|
2177
|
+
const legacy = normalizePointers(o);
|
|
2178
|
+
const hasLegacyState = legacy.active_vault_id !== null || legacy.active_workspace_id !== null || workspaces2.length > 0 || vaults2.length > 0;
|
|
2179
|
+
return {
|
|
2180
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
2181
|
+
current_account_id: hasLegacyState ? LEGACY_REGISTRY_ACCOUNT_ID : null,
|
|
2182
|
+
accounts: hasLegacyState ? { [LEGACY_REGISTRY_ACCOUNT_ID]: legacy } : {},
|
|
2183
|
+
workspaces: workspaces2,
|
|
2184
|
+
vaults: vaults2
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
const accounts = {};
|
|
2188
|
+
if (o.accounts && typeof o.accounts === "object" && !Array.isArray(o.accounts)) {
|
|
2189
|
+
for (const [id, pointers] of Object.entries(o.accounts)) {
|
|
2190
|
+
if (isNonEmptyString(id)) accounts[id] = normalizeStoredPointers(pointers);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
1653
2193
|
return {
|
|
1654
2194
|
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
2195
|
+
current_account_id: isNonEmptyString(o.current_account_id) ? o.current_account_id : null,
|
|
2196
|
+
accounts,
|
|
1655
2197
|
workspaces: workspaces2,
|
|
1656
|
-
vaults: vaults2
|
|
1657
|
-
active_workspace_id,
|
|
1658
|
-
active_vault_id
|
|
2198
|
+
vaults: vaults2
|
|
1659
2199
|
};
|
|
1660
2200
|
}
|
|
1661
|
-
function
|
|
2201
|
+
function readRegistryJson(opts) {
|
|
1662
2202
|
const filePath = vaultRegistryFilePath(opts);
|
|
1663
|
-
if (!
|
|
2203
|
+
if (!existsSync3(filePath)) return { kind: "missing" };
|
|
1664
2204
|
try {
|
|
1665
|
-
const raw =
|
|
1666
|
-
|
|
2205
|
+
const raw = readFileSync5(filePath, "utf8");
|
|
2206
|
+
const json = JSON.parse(raw);
|
|
2207
|
+
if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
|
|
2208
|
+
return { kind: "ok", raw, json };
|
|
1667
2209
|
} catch {
|
|
1668
|
-
return
|
|
2210
|
+
return { kind: "unreadable" };
|
|
1669
2211
|
}
|
|
1670
2212
|
}
|
|
1671
|
-
function
|
|
1672
|
-
const
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2213
|
+
function inspectVaultRegistry(opts = {}) {
|
|
2214
|
+
const read = readRegistryJson(opts);
|
|
2215
|
+
if (read.kind !== "ok") {
|
|
2216
|
+
return {
|
|
2217
|
+
registry: emptyVaultRegistry(),
|
|
2218
|
+
file_schema_version: null,
|
|
2219
|
+
registry_unreadable: read.kind === "unreadable"
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
return {
|
|
2223
|
+
registry: parseVaultRegistryFile(read.json),
|
|
2224
|
+
file_schema_version: registryFileSchemaOf(read.json),
|
|
2225
|
+
registry_unreadable: false
|
|
2226
|
+
};
|
|
1678
2227
|
}
|
|
1679
|
-
function
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2228
|
+
function loadVaultRegistry(opts = {}) {
|
|
2229
|
+
return inspectVaultRegistry(opts).registry;
|
|
2230
|
+
}
|
|
2231
|
+
function ownsRegistryRows(registry, accountId2) {
|
|
2232
|
+
return registry.vaults.some((v) => v.account_id === accountId2) || registry.workspaces.some((w) => w.account_id === accountId2);
|
|
2233
|
+
}
|
|
2234
|
+
function accountPointersOf(registry, accountId2) {
|
|
2235
|
+
const own = accountId2 ? registry.accounts[accountId2] : void 0;
|
|
2236
|
+
const legacy = registry.accounts[LEGACY_REGISTRY_ACCOUNT_ID];
|
|
2237
|
+
const clearsCount = Boolean(accountId2) && ownsRegistryRows(registry, accountId2);
|
|
2238
|
+
const pick = (field) => {
|
|
2239
|
+
const mine = own?.[field];
|
|
2240
|
+
if (isNonEmptyString(mine)) return mine;
|
|
2241
|
+
if (mine === null && clearsCount && accountId2 !== LEGACY_REGISTRY_ACCOUNT_ID) return null;
|
|
2242
|
+
const fallback = legacy?.[field];
|
|
2243
|
+
return isNonEmptyString(fallback) ? fallback : null;
|
|
2244
|
+
};
|
|
2245
|
+
return {
|
|
2246
|
+
active_vault_id: pick("active_vault_id"),
|
|
2247
|
+
active_workspace_id: pick("active_workspace_id")
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
function serializeVaultRegistry(registry) {
|
|
2251
|
+
return `${JSON.stringify(registry, null, 2)}
|
|
2252
|
+
`;
|
|
2253
|
+
}
|
|
2254
|
+
function underTestRunner() {
|
|
2255
|
+
return Boolean(process.env.VITEST) || Boolean(process.env.NODE_TEST_CONTEXT);
|
|
2256
|
+
}
|
|
2257
|
+
function samePathLoose(a, b) {
|
|
2258
|
+
const norm = (p) => {
|
|
2259
|
+
const r = resolve3(p).replace(/[\\/]+$/, "");
|
|
2260
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
2261
|
+
};
|
|
2262
|
+
return norm(a) === norm(b);
|
|
2263
|
+
}
|
|
2264
|
+
function assertNotRealHomeRegistryUnderTest(filePath) {
|
|
2265
|
+
if (!underTestRunner()) return;
|
|
2266
|
+
if (samePathLoose(dirname4(filePath), join6(homedir5(), ".heyspex"))) {
|
|
2267
|
+
throw new Error(
|
|
2268
|
+
`registry-file: refusing to write ${filePath} under a test runner \u2014 that is the real ~/.heyspex registry. Pass opts.registryDir (a temp dir).`
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
function writeVaultRegistry(registry, opts = {}) {
|
|
2273
|
+
const filePath = vaultRegistryFilePath(opts);
|
|
2274
|
+
assertNotRealHomeRegistryUnderTest(filePath);
|
|
2275
|
+
mkdirSync2(dirname4(filePath), { recursive: true });
|
|
2276
|
+
const tmpPath = `${filePath}.tmp`;
|
|
2277
|
+
writeFileSync2(tmpPath, serializeVaultRegistry(registry), "utf8");
|
|
2278
|
+
renameSync2(tmpPath, filePath);
|
|
2279
|
+
}
|
|
2280
|
+
function isValidId(id) {
|
|
2281
|
+
if (!isNonEmptyString(id)) return false;
|
|
2282
|
+
if (id.includes("/") || id.includes("\\")) return false;
|
|
2283
|
+
if (id === "." || id === "..") return false;
|
|
1683
2284
|
return true;
|
|
1684
2285
|
}
|
|
2286
|
+
function requireAccountId(fn, accountId2) {
|
|
2287
|
+
if (!isNonEmptyString(accountId2)) {
|
|
2288
|
+
throw new Error(`${fn}: account_id is required`);
|
|
2289
|
+
}
|
|
2290
|
+
return accountId2;
|
|
2291
|
+
}
|
|
1685
2292
|
function registerWorkspace(record, opts = {}) {
|
|
1686
2293
|
if (!isValidId(record.id)) {
|
|
1687
2294
|
throw new Error(`registerWorkspace: invalid workspace id "${record.id}"`);
|
|
1688
2295
|
}
|
|
2296
|
+
requireAccountId("registerWorkspace", record.account_id);
|
|
1689
2297
|
const registry = loadVaultRegistry(opts);
|
|
1690
2298
|
const idx = registry.workspaces.findIndex((w) => w.id === record.id);
|
|
1691
2299
|
if (idx >= 0) {
|
|
@@ -1696,30 +2304,927 @@ function registerWorkspace(record, opts = {}) {
|
|
|
1696
2304
|
writeVaultRegistry(registry, opts);
|
|
1697
2305
|
return registry;
|
|
1698
2306
|
}
|
|
1699
|
-
function
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2307
|
+
function writeAccountPointer(fn, accountId2, field, value, opts) {
|
|
2308
|
+
requireAccountId(fn, accountId2);
|
|
2309
|
+
if (value !== null && !isNonEmptyString(value)) {
|
|
2310
|
+
throw new Error(`${fn}: ${field} must be a non-empty string or null`);
|
|
2311
|
+
}
|
|
2312
|
+
if (opts === null || typeof opts !== "object") {
|
|
2313
|
+
throw new Error(`${fn}: opts must be an object`);
|
|
2314
|
+
}
|
|
2315
|
+
const registry = loadVaultRegistry(opts);
|
|
2316
|
+
const row = { ...registry.accounts[accountId2] ?? {}, [field]: value };
|
|
2317
|
+
registry.accounts = { ...registry.accounts, [accountId2]: row };
|
|
2318
|
+
if (registry.current_account_id === null || registry.current_account_id === LEGACY_REGISTRY_ACCOUNT_ID && accountId2 !== LEGACY_REGISTRY_ACCOUNT_ID) {
|
|
2319
|
+
registry.current_account_id = accountId2;
|
|
2320
|
+
}
|
|
2321
|
+
writeVaultRegistry(registry, opts);
|
|
2322
|
+
return registry;
|
|
2323
|
+
}
|
|
2324
|
+
function setActiveWorkspaceId(accountId2, workspaceId, opts = {}) {
|
|
2325
|
+
return writeAccountPointer(
|
|
2326
|
+
"setActiveWorkspaceId",
|
|
2327
|
+
accountId2,
|
|
2328
|
+
"active_workspace_id",
|
|
2329
|
+
workspaceId,
|
|
2330
|
+
opts
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
function clearPointerEverywhere(registry, field, id) {
|
|
2334
|
+
for (const [accountId2, pointers] of Object.entries(registry.accounts)) {
|
|
2335
|
+
if (pointers[field] === id) registry.accounts[accountId2] = { ...pointers, [field]: null };
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function removeWorkspace(id, opts = {}) {
|
|
2339
|
+
const registry = loadVaultRegistry(opts);
|
|
2340
|
+
registry.workspaces = registry.workspaces.filter((w) => w.id !== id);
|
|
2341
|
+
clearPointerEverywhere(registry, "active_workspace_id", id);
|
|
2342
|
+
writeVaultRegistry(registry, opts);
|
|
2343
|
+
return registry;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
2347
|
+
var EXIT = {
|
|
2348
|
+
/** Success. */
|
|
2349
|
+
OK: 0,
|
|
2350
|
+
/**
|
|
2351
|
+
* A read-only CHECK ran correctly and its answer is "no" — `hs connect
|
|
2352
|
+
* --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
|
|
2353
|
+
* script can tell "you typed the command wrong" from "the command worked,
|
|
2354
|
+
* the repo is not connected". Both were `1` until 2026-09-18, which made
|
|
2355
|
+
* `--check` unusable in a conditional: every caller had to parse stdout to
|
|
2356
|
+
* find out which of the two had happened.
|
|
2357
|
+
*/
|
|
2358
|
+
CHECK_FAILED: 2,
|
|
2359
|
+
/** Usage error — unknown subcommand/flag, or a missing required argument. */
|
|
2360
|
+
USAGE: 1,
|
|
2361
|
+
/** Claim race lost — the spec is already claimed by another agent (409). */
|
|
2362
|
+
CONFLICT: 9,
|
|
2363
|
+
/** Target not found — release of an unheld claim, etc. (404). */
|
|
2364
|
+
NOT_FOUND: 10,
|
|
2365
|
+
/** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
|
|
2366
|
+
INVALID_ARG: 11,
|
|
2367
|
+
/** SpexConsole unreachable (network error reaching the dev server). */
|
|
2368
|
+
CONSOLE_UNREACHABLE: 12,
|
|
2369
|
+
/** Server failure — a non-OK response that isn't 400/402/404/409. */
|
|
2370
|
+
WRITE_FAILED: 13,
|
|
2371
|
+
/** Neon control plane absent — the endpoint returned 402 (upgrade required). */
|
|
2372
|
+
NEON_REQUIRED: 14,
|
|
2373
|
+
/**
|
|
2374
|
+
* A cross-account `--account <id>` override was refused — the caller is
|
|
2375
|
+
* not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
|
|
2376
|
+
* [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
|
|
2377
|
+
* from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
|
|
2378
|
+
* malformed command.
|
|
2379
|
+
*/
|
|
2380
|
+
FORBIDDEN: 15,
|
|
2381
|
+
/**
|
|
2382
|
+
* A CLOUD write refused because no account id could be resolved (no
|
|
2383
|
+
* `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
|
|
2384
|
+
* `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
|
|
2385
|
+
* ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
|
|
2386
|
+
* `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
|
|
2387
|
+
* authorized for this override".
|
|
2388
|
+
*/
|
|
2389
|
+
SIGN_IN_REQUIRED: 16,
|
|
2390
|
+
/**
|
|
2391
|
+
* Signed in, but the account a repo would be attached to was not confirmed:
|
|
2392
|
+
* a non-interactive install/connect with no matching `--account` / `--yes`,
|
|
2393
|
+
* an `account_id` that disagrees with the bearer's user, or the human
|
|
2394
|
+
* aborting the identity prompt (`install-identity.ts`,
|
|
2395
|
+
* [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
|
|
2396
|
+
*/
|
|
2397
|
+
IDENTITY_UNCONFIRMED: 17,
|
|
2398
|
+
/**
|
|
2399
|
+
* The repo this verb runs in is not registered on this machine (no
|
|
2400
|
+
* `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
|
|
2401
|
+
* cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
|
|
2402
|
+
* nothing was sent. Names `hs connect`
|
|
2403
|
+
* ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
|
|
2404
|
+
*/
|
|
2405
|
+
VAULT_NOT_REGISTERED: 18,
|
|
2406
|
+
/**
|
|
2407
|
+
* The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
|
|
2408
|
+
* *`) was written but the CLOUD half did not land -- the console refused
|
|
2409
|
+
* it or was never reached. The verb's receipt names the failed half and the
|
|
2410
|
+
* exact command to re-run
|
|
2411
|
+
* ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
|
|
2412
|
+
* Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
|
|
2413
|
+
* sync the cloud" from a generic server failure.
|
|
2414
|
+
*/
|
|
2415
|
+
CLOUD_WRITE_FAILED: 19,
|
|
2416
|
+
/**
|
|
2417
|
+
* The repo's committed `heyspex.yaml` records `account: <A>` but the machine
|
|
2418
|
+
* is signed in as another account, so `hs connect` refused and wrote
|
|
2419
|
+
* nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
|
|
2420
|
+
* ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
|
|
2421
|
+
*/
|
|
2422
|
+
ACCOUNT_MISMATCH: 20,
|
|
2423
|
+
/**
|
|
2424
|
+
* Both sides of a setup verb's receipt were read back and still DISAGREE
|
|
2425
|
+
* after a short bounded re-read of the cloud side (a briefly stale read is
|
|
2426
|
+
* absorbed first). The write may well have landed; what the verb cannot
|
|
2427
|
+
* claim is that this machine and the console now say the same thing. The
|
|
2428
|
+
* receipt prints the diffs and the command to re-run
|
|
2429
|
+
* ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
|
|
2430
|
+
* Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
|
|
2431
|
+
*/
|
|
2432
|
+
RECEIPT_DISAGREE: 21
|
|
2433
|
+
};
|
|
2434
|
+
var VAULT_ID_HEADER = "x-heyspex-vault-id";
|
|
2435
|
+
var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
|
|
2436
|
+
function readPairedBearer(opts = {}) {
|
|
2437
|
+
return readBearer(opts);
|
|
2438
|
+
}
|
|
2439
|
+
function readPairedToken(opts = {}) {
|
|
2440
|
+
return readPairedBearer(opts).token ?? void 0;
|
|
2441
|
+
}
|
|
2442
|
+
function scopeHeadersFor(scope, method, path4) {
|
|
2443
|
+
if (scope === "none") {
|
|
2444
|
+
if (!isVaultLessEndpoint(method, path4)) {
|
|
2445
|
+
throw new Error(
|
|
2446
|
+
`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 }.`
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
return {};
|
|
2450
|
+
}
|
|
2451
|
+
return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
|
|
2452
|
+
}
|
|
2453
|
+
function cloudRequestScope(scope) {
|
|
2454
|
+
const named = { accountId: scope.accountId, vaultId: scope.vaultId };
|
|
2455
|
+
return {
|
|
2456
|
+
...named,
|
|
2457
|
+
headersFor: (method, path4) => isVaultLessEndpoint(method, path4) ? {} : scopeHeadersFor(named, method, path4)
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
function withCloudScope(deps, accountId2, vaultId) {
|
|
2461
|
+
if (!deps?.http || !vaultId || !accountId2) return deps;
|
|
2462
|
+
return { ...deps, http: { ...deps.http, scope: cloudRequestScope({ accountId: accountId2, vaultId }) } };
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// scripts/lib/console-url-resolver.mjs
|
|
2466
|
+
init_dev_mode();
|
|
2467
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2468
|
+
import { homedir as homedir7 } from "node:os";
|
|
2469
|
+
import { join as join8 } from "node:path";
|
|
2470
|
+
var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
|
|
2471
|
+
function trimTrailingSlash(url) {
|
|
2472
|
+
return url.replace(/\/+$/, "");
|
|
2473
|
+
}
|
|
2474
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
|
2475
|
+
function isLoopbackConsoleUrl(url) {
|
|
2476
|
+
if (typeof url !== "string" || !url) return false;
|
|
2477
|
+
try {
|
|
2478
|
+
return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
|
|
2479
|
+
} catch {
|
|
2480
|
+
return false;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
var staleLoopbackCredentialsWarned = false;
|
|
2484
|
+
function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
|
|
2485
|
+
if (staleLoopbackCredentialsWarned) return;
|
|
2486
|
+
staleLoopbackCredentialsWarned = true;
|
|
2487
|
+
try {
|
|
2488
|
+
log(
|
|
2489
|
+
`[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).
|
|
2490
|
+
`
|
|
2491
|
+
);
|
|
2492
|
+
} catch {
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile: readFile2 = defaultReadFile2 } = {}) {
|
|
2496
|
+
try {
|
|
2497
|
+
const raw = readFile2(join8(homeDir, ".heyspex", "credentials.yaml"));
|
|
2498
|
+
const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
2499
|
+
const value = match?.[1]?.trim();
|
|
2500
|
+
return value && value.length > 0 ? trimTrailingSlash(value) : null;
|
|
2501
|
+
} catch {
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
function defaultReadFile2(p) {
|
|
2506
|
+
return readFileSync7(p, "utf8");
|
|
2507
|
+
}
|
|
2508
|
+
function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
|
|
2509
|
+
if (!repoRoot) return null;
|
|
2510
|
+
try {
|
|
2511
|
+
const discovery = String(readDiscovery(join8(repoRoot, ".heyspex", "console-url")) ?? "").trim();
|
|
2512
|
+
if (discovery.length > 0) return trimTrailingSlash(discovery);
|
|
2513
|
+
} catch {
|
|
2514
|
+
}
|
|
2515
|
+
return null;
|
|
2516
|
+
}
|
|
2517
|
+
var ConsoleUrlUnresolvedError = class extends Error {
|
|
2518
|
+
constructor() {
|
|
2519
|
+
super(
|
|
2520
|
+
"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."
|
|
2521
|
+
);
|
|
2522
|
+
this.name = "ConsoleUrlUnresolvedError";
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
function resolveModeEnvelope({ env, homeDir }) {
|
|
2526
|
+
try {
|
|
2527
|
+
const resolved = resolveDevMode({ env, homeDir });
|
|
2528
|
+
return {
|
|
2529
|
+
reliance: resolved.reliance === "cloud" ? "cloud" : "local",
|
|
2530
|
+
consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
|
|
2531
|
+
};
|
|
2532
|
+
} catch {
|
|
2533
|
+
return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
function resolveConsoleUrl({
|
|
2537
|
+
repoRoot,
|
|
2538
|
+
env = process.env,
|
|
2539
|
+
homeDir = homedir7(),
|
|
2540
|
+
strict = false,
|
|
2541
|
+
devMode,
|
|
2542
|
+
log = (line) => process.stderr.write(line),
|
|
2543
|
+
readFile: readFile2 = defaultReadFile2,
|
|
2544
|
+
readDiscovery = defaultReadFile2
|
|
2545
|
+
} = {}) {
|
|
2546
|
+
const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
|
|
2547
|
+
if (envUrl) return trimTrailingSlash(envUrl);
|
|
2548
|
+
const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
|
|
2549
|
+
const cloud = mode.reliance === "cloud";
|
|
2550
|
+
const derivedUrl = trimTrailingSlash(
|
|
2551
|
+
cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
|
|
2552
|
+
);
|
|
2553
|
+
const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile: readFile2 });
|
|
2554
|
+
if (credentialsUrl) {
|
|
2555
|
+
if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
|
|
2556
|
+
warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
|
|
2557
|
+
} else {
|
|
2558
|
+
return credentialsUrl;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (!cloud) {
|
|
2562
|
+
const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery });
|
|
2563
|
+
if (discoveryUrl) return discoveryUrl;
|
|
2564
|
+
if (strict) throw new ConsoleUrlUnresolvedError();
|
|
2565
|
+
}
|
|
2566
|
+
return derivedUrl;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// spex-console/src/lib/cli/defer.ts
|
|
2570
|
+
function resolveSpexConsoleUrl(vaultRoot, readDiscovery, opts = {}) {
|
|
2571
|
+
return resolveConsoleUrl({
|
|
2572
|
+
repoRoot: vaultRoot,
|
|
2573
|
+
env: opts.env,
|
|
2574
|
+
homeDir: opts.homeDir,
|
|
2575
|
+
readFile: opts.readFile,
|
|
2576
|
+
readDiscovery,
|
|
2577
|
+
log: opts.log
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
var HELP = [
|
|
2581
|
+
"Usage:",
|
|
2582
|
+
" hs defer <spec-id> [--until <when>] [--json] [--no-daemon]",
|
|
2583
|
+
" hs undefer <spec-id> [--json] [--no-daemon]",
|
|
2584
|
+
"",
|
|
2585
|
+
"Time-shift a spec via its `defer_until` frontmatter field. A deferred spec is",
|
|
2586
|
+
"scheduler-invisible until the date passes, then auto-revives.",
|
|
2587
|
+
"",
|
|
2588
|
+
"Arguments:",
|
|
2589
|
+
" <spec-id> The spec to defer/undefer (e.g. HS-TASK-EWL5).",
|
|
2590
|
+
"",
|
|
2591
|
+
"Flags:",
|
|
2592
|
+
" --until <when> ISO date, or relative offset +Nd / +Nw / +Nm / +Ny.",
|
|
2593
|
+
" Defaults to +1y (matches the console Defer button).",
|
|
2594
|
+
" Ignored by `undefer` (which always clears the field).",
|
|
2595
|
+
" --json Emit a machine-readable JSON result.",
|
|
2596
|
+
" --no-daemon Write straight to the vault (no console needed; CI path).",
|
|
2597
|
+
" --help, -h Show this help.",
|
|
2598
|
+
"",
|
|
2599
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
|
|
2600
|
+
].join("\n");
|
|
2601
|
+
|
|
2602
|
+
// spex-console/src/lib/cli/install-identity.ts
|
|
2603
|
+
import { spawnSync } from "node:child_process";
|
|
2604
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
2605
|
+
import { homedir as homedir9 } from "node:os";
|
|
2606
|
+
import { join as join9 } from "node:path";
|
|
2607
|
+
import { createInterface } from "node:readline/promises";
|
|
2608
|
+
|
|
2609
|
+
// scripts/lib/shared-path-helpers.mjs
|
|
2610
|
+
import { isAbsolute, resolve as resolve4, sep } from "node:path";
|
|
2611
|
+
function resolveCodeRoot({
|
|
2612
|
+
explicit,
|
|
2613
|
+
packageRoot,
|
|
2614
|
+
vaultRoot,
|
|
2615
|
+
env = process.env
|
|
2616
|
+
} = {}) {
|
|
2617
|
+
const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
|
|
2618
|
+
if (fromExplicit) return fromExplicit;
|
|
2619
|
+
const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
|
|
2620
|
+
if (fromEnv) return fromEnv;
|
|
2621
|
+
const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
|
|
2622
|
+
if (fromPackage) return fromPackage;
|
|
2623
|
+
const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
|
|
2624
|
+
return fromVault;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// spex-console/src/lib/cli/install-identity.ts
|
|
2628
|
+
init_credentials();
|
|
2629
|
+
|
|
2630
|
+
// vendor/heyspex-mcp/whoami.mjs
|
|
2631
|
+
init_credentials();
|
|
2632
|
+
import { homedir as homedir8 } from "node:os";
|
|
2633
|
+
var SESSION_PATH = "/api/mcp/session";
|
|
2634
|
+
async function probeCredentialIdentity({ consoleUrl, token, fetchImpl = fetch } = {}) {
|
|
2635
|
+
if (!token) return { kind: "no_credential" };
|
|
2636
|
+
const base = String(consoleUrl ?? "").trim().replace(/\/+$/, "");
|
|
2637
|
+
let res;
|
|
2638
|
+
try {
|
|
2639
|
+
res = await fetchImpl(`${base}${SESSION_PATH}`, {
|
|
2640
|
+
method: "GET",
|
|
2641
|
+
headers: { authorization: `Bearer ${token}` },
|
|
2642
|
+
cache: "no-store"
|
|
2643
|
+
});
|
|
2644
|
+
} catch (err) {
|
|
2645
|
+
return { kind: "unreachable", message: err?.message ?? String(err) };
|
|
2646
|
+
}
|
|
2647
|
+
if (res.status === 401 || res.status === 403) return { kind: "rejected", status: res.status };
|
|
2648
|
+
if (res.status === 404 || res.status === 405) return { kind: "unsupported", status: res.status };
|
|
2649
|
+
let body = null;
|
|
2650
|
+
try {
|
|
2651
|
+
body = await res.json();
|
|
2652
|
+
} catch {
|
|
2653
|
+
body = null;
|
|
2654
|
+
}
|
|
2655
|
+
if (!res.ok || !body || typeof body.userId !== "string" || !body.userId.trim()) {
|
|
2656
|
+
return { kind: "unreachable", message: `console returned ${res.status}` };
|
|
2657
|
+
}
|
|
2658
|
+
return {
|
|
2659
|
+
kind: "identified",
|
|
2660
|
+
accountId: body.userId.trim(),
|
|
2661
|
+
email: typeof body.email === "string" && body.email.trim() ? body.email.trim() : null,
|
|
2662
|
+
displayName: typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : null,
|
|
2663
|
+
// How the console authenticated this caller (the route's `mode`, e.g. a
|
|
2664
|
+
// bearer); the `hs login` receipt shows it ([[HS-CRNE-WW9R-TASK-give-hs-install-and-hs-login-the]]).
|
|
2665
|
+
mode: typeof body.mode === "string" && body.mode.trim() ? body.mode.trim() : null,
|
|
2666
|
+
consoleUrl: base
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
function formatIdentity({ email, accountId: accountId2 }) {
|
|
2670
|
+
return email ? `${email} (${accountId2})` : accountId2;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// spex-console/src/lib/cli/init.ts
|
|
2674
|
+
import { randomUUID } from "node:crypto";
|
|
2675
|
+
var HELP2 = [
|
|
2676
|
+
"Usage:",
|
|
2677
|
+
" hs init [--console-url <url>] [--vault-root <path>] [--force] [--json]",
|
|
2678
|
+
" hs init --customer-vault <dir> [--console-url <url>] [--principles <all|none|a,b>]",
|
|
2679
|
+
" [--vault-name <name>] [--git] [--json]",
|
|
2680
|
+
"",
|
|
2681
|
+
"Write the top-level heyspex.yaml vault marker (schema_version, console_url,",
|
|
2682
|
+
"vault_root, vault_id; `hs connect` also records `account:`, the account id the",
|
|
2683
|
+
"repo was connected under) \u2014 the committed file the console / web companion read",
|
|
2684
|
+
"to bootstrap a vault. Idempotent: an existing marker is left untouched unless",
|
|
2685
|
+
"--force, EXCEPT a missing vault_id is backfilled in place either way (backward",
|
|
2686
|
+
"compatible \u2014 no schema_version bump). vault_id is reused exactly when already",
|
|
2687
|
+
"present; a fresh one is minted only when absent.",
|
|
2688
|
+
"Also scaffolds the vault_root folder and seeds .heyspex/config.yaml (the",
|
|
2689
|
+
"HeySpex workflow settings, ADR-0009) from the shipped template",
|
|
2690
|
+
"(vendor/heyspex-mcp/config-seed.yaml) when that file is absent. An existing",
|
|
2691
|
+
".heyspex/config.yaml is never rewritten \u2014 not even with --force; change its",
|
|
2692
|
+
"values with `hs settings set <key> <value>`.",
|
|
2693
|
+
"",
|
|
2694
|
+
"--customer-vault <dir> scaffolds a REAL customer vault at <dir> instead: the",
|
|
2695
|
+
"specs/ folder tree, heyspex.yaml, the seeded .heyspex/config.yaml, and every",
|
|
2696
|
+
"row of the template shipping manifest",
|
|
2697
|
+
"(spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
|
|
2698
|
+
"copied verbatim, template-shape docs written as author-prompt skeletons \u2014",
|
|
2699
|
+
"plus a generated CLAUDE.md, the kept-principles index, and the template_version",
|
|
2700
|
+
"pin; with --git, one initial `git init` + commit. Rerunnable: a <dir> that already",
|
|
2701
|
+
"holds heyspex.yaml and an empty/placeholder-only specs/ is accepted, existing",
|
|
2702
|
+
"files are never overwritten (an existing CLAUDE.md gets a CLAUDE.md.template",
|
|
2703
|
+
"beside it), and the result lists each manifest row as created or kept. Refuses a",
|
|
2704
|
+
"non-empty <dir> without heyspex.yaml, or a specs/ holding managed specs.",
|
|
2705
|
+
"See init.customer-vault.ts.",
|
|
2706
|
+
"",
|
|
2707
|
+
"Flags:",
|
|
2708
|
+
" --console-url <url> console_url value (default: the mode-aware console --",
|
|
2709
|
+
" $HEYSPEX_CONSOLE_URL, else the signed-in console, else",
|
|
2710
|
+
" https://app.heyspex.com in prod mode).",
|
|
2711
|
+
" --vault-root <path> vault_root value (default: specs/).",
|
|
2712
|
+
" --customer-vault <dir> Scaffold a new customer vault at <dir> (see above).",
|
|
2713
|
+
" --principles <all|none|a,b> Which universal principle docs to ship",
|
|
2714
|
+
" (--customer-vault only). Omitted + a TTY: interactive",
|
|
2715
|
+
" keep/skip prompt per principle. Omitted + no TTY: all.",
|
|
2716
|
+
" --vault-name <name> Display name for the scaffolded vault (--customer-vault",
|
|
2717
|
+
" only; default: <dir>'s basename).",
|
|
2718
|
+
" --git Also run git init + one initial commit (--customer-vault only;",
|
|
2719
|
+
" default: no git). --no-git is accepted as a no-op.",
|
|
2720
|
+
" --force Overwrite an existing heyspex.yaml (never .heyspex/config.yaml).",
|
|
2721
|
+
" --json Emit a machine-readable JSON result.",
|
|
2722
|
+
" --help, -h Show this help.",
|
|
2723
|
+
"",
|
|
2724
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed"
|
|
2725
|
+
].join("\n");
|
|
2726
|
+
|
|
2727
|
+
// spex-console/src/lib/cli/install-identity.ts
|
|
2728
|
+
var SWITCH_HINT = "Sign in as another account with `hs login --switch-account` (or create one with `hs login --create-account`).";
|
|
2729
|
+
var ATTACH_WORDING = {
|
|
2730
|
+
willBe: "This repo will be attached to",
|
|
2731
|
+
wouldBe: "It would be attached to",
|
|
2732
|
+
act: "attach this repo",
|
|
2733
|
+
choose: "Choose the account this repo should belong to.",
|
|
2734
|
+
noAccount: "there is no account to attach this repo to",
|
|
2735
|
+
whichAccount: "which account this repo would be attached to",
|
|
2736
|
+
accountFlag: "--account",
|
|
2737
|
+
yesFlag: "--yes"
|
|
2738
|
+
};
|
|
2739
|
+
var WRITE_WORDING = {
|
|
2740
|
+
willBe: "This change will be made as",
|
|
2741
|
+
wouldBe: "It would be made as",
|
|
2742
|
+
act: "make this change",
|
|
2743
|
+
choose: "Choose the account this change should be made as.",
|
|
2744
|
+
noAccount: "there is no account to make this change as",
|
|
2745
|
+
whichAccount: "which account this change would be made as",
|
|
2746
|
+
accountFlag: "--as",
|
|
2747
|
+
yesFlag: "--confirm-account"
|
|
2748
|
+
};
|
|
2749
|
+
function extractWriteIdentityFlags(argv) {
|
|
2750
|
+
const flags = { yes: false };
|
|
2751
|
+
const rest = [];
|
|
2752
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2753
|
+
const arg = argv[i];
|
|
2754
|
+
if (arg === "--as" || arg.startsWith("--as=")) {
|
|
2755
|
+
const v = arg === "--as" ? argv[++i] : arg.slice("--as=".length);
|
|
2756
|
+
if (v === void 0 || v.trim() === "" || v.trim().startsWith("-")) {
|
|
2757
|
+
return { flags, rest, error: "--as requires the signed-in account's email or account id" };
|
|
2758
|
+
}
|
|
2759
|
+
flags.account = v.trim();
|
|
2760
|
+
} else if (arg === "--confirm-account") {
|
|
2761
|
+
flags.yes = true;
|
|
2762
|
+
} else {
|
|
2763
|
+
rest.push(arg);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
return { flags, rest };
|
|
2767
|
+
}
|
|
2768
|
+
function matchesAccount(identity, wanted) {
|
|
2769
|
+
const w = wanted.trim().toLowerCase();
|
|
2770
|
+
return identity.accountId.toLowerCase() === w || (identity.email?.toLowerCase() ?? "") === w;
|
|
2771
|
+
}
|
|
2772
|
+
function formatShownIdentity(identity) {
|
|
2773
|
+
return formatIdentity(identity) + (identity.unverified ? " [unverified - console not yet updated]" : "") + (identity.tokenSource ? ` [token: ${describeBearerSource(identity.tokenSource)}]` : "");
|
|
2774
|
+
}
|
|
2775
|
+
async function probeBearerIdentity(opts) {
|
|
2776
|
+
const probe = await probeCredentialIdentity({
|
|
2777
|
+
consoleUrl: opts.consoleUrl,
|
|
2778
|
+
token: opts.bearer.token,
|
|
2779
|
+
fetchImpl: opts.fetchImpl ?? fetch
|
|
2780
|
+
});
|
|
2781
|
+
if (probe.kind === "identified" && opts.bearer.source) {
|
|
2782
|
+
return { ...probe, tokenSource: opts.bearer.source };
|
|
2783
|
+
}
|
|
2784
|
+
return probe;
|
|
2785
|
+
}
|
|
2786
|
+
function decideIdentity(input) {
|
|
2787
|
+
const { probe, configAccountId, flags, interactive, consoleUrl, verb } = input;
|
|
2788
|
+
const w = input.wording ?? ATTACH_WORDING;
|
|
2789
|
+
if (isLoopbackConsoleUrl(consoleUrl)) {
|
|
2790
|
+
return {
|
|
2791
|
+
action: "proceed",
|
|
2792
|
+
identity: null,
|
|
2793
|
+
note: `Local console (${consoleUrl}): no cloud account is bound by this run.`
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
if (probe.kind === "no_credential" || probe.kind === "rejected") {
|
|
2797
|
+
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`;
|
|
2798
|
+
if (interactive) {
|
|
2799
|
+
return {
|
|
2800
|
+
action: "prompt",
|
|
2801
|
+
identity: null,
|
|
2802
|
+
message: `${why}. ${w.choose}`,
|
|
2803
|
+
choices: ["switch", "create", "quit"]
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
return {
|
|
2807
|
+
action: "refuse",
|
|
2808
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
2809
|
+
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.`
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
if (probe.kind === "unreachable") {
|
|
2813
|
+
if (flags.yes) {
|
|
2814
|
+
return {
|
|
2815
|
+
action: "proceed",
|
|
2816
|
+
identity: null,
|
|
2817
|
+
note: `Could not reach ${consoleUrl} to confirm the account (${probe.message}); continuing because ${w.yesFlag} was given.`
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
return {
|
|
2821
|
+
action: "refuse",
|
|
2822
|
+
code: EXIT.CONSOLE_UNREACHABLE,
|
|
2823
|
+
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.`
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
let identity;
|
|
2827
|
+
if (probe.kind === "unsupported") {
|
|
2828
|
+
if (!configAccountId) {
|
|
2829
|
+
return {
|
|
2830
|
+
action: "refuse",
|
|
2831
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
2832
|
+
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.`
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
identity = { accountId: configAccountId, email: null, unverified: true };
|
|
2836
|
+
} else {
|
|
2837
|
+
identity = {
|
|
2838
|
+
accountId: probe.accountId,
|
|
2839
|
+
email: probe.email,
|
|
2840
|
+
...probe.tokenSource ? { tokenSource: probe.tokenSource } : {}
|
|
2841
|
+
};
|
|
2842
|
+
if (configAccountId && configAccountId !== probe.accountId) {
|
|
2843
|
+
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.`;
|
|
2844
|
+
if (interactive) {
|
|
2845
|
+
return { action: "prompt", identity, message: message2, choices: ["switch", "create", "quit"] };
|
|
2846
|
+
}
|
|
2847
|
+
return {
|
|
2848
|
+
action: "refuse",
|
|
2849
|
+
code: EXIT.IDENTITY_UNCONFIRMED,
|
|
2850
|
+
message: `${verb}: refusing -- ${message2} ${SWITCH_HINT} Nothing was written.`
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
const shown = formatShownIdentity(identity);
|
|
2855
|
+
if (flags.account !== void 0) {
|
|
2856
|
+
if (matchesAccount(identity, flags.account)) {
|
|
2857
|
+
return { action: "proceed", identity, note: `${w.willBe} ${shown}.` };
|
|
2858
|
+
}
|
|
2859
|
+
return {
|
|
2860
|
+
action: "refuse",
|
|
2861
|
+
code: EXIT.IDENTITY_UNCONFIRMED,
|
|
2862
|
+
message: `${verb}: refusing -- ${w.accountFlag} ${flags.account} does not match the signed-in account ${shown}. ${SWITCH_HINT} Nothing was written.`
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
if (flags.yes) {
|
|
2866
|
+
return { action: "proceed", identity, note: `${w.willBe} ${shown} (${w.yesFlag}).` };
|
|
2867
|
+
}
|
|
2868
|
+
if (interactive) {
|
|
2869
|
+
return {
|
|
2870
|
+
action: "prompt",
|
|
2871
|
+
identity,
|
|
2872
|
+
message: `${w.willBe} ${shown} on ${consoleUrl}.`,
|
|
2873
|
+
choices: ["continue", "switch", "create", "quit"]
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
return {
|
|
2877
|
+
action: "refuse",
|
|
2878
|
+
code: EXIT.IDENTITY_UNCONFIRMED,
|
|
2879
|
+
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.`
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
function renderChoices(choices, signedIn) {
|
|
2883
|
+
const label = {
|
|
2884
|
+
continue: "[Enter] continue",
|
|
2885
|
+
switch: signedIn ? "[s] sign in with a different account" : "[s] sign in",
|
|
2886
|
+
create: "[n] create a new account",
|
|
2887
|
+
quit: "[q] abort"
|
|
2888
|
+
};
|
|
2889
|
+
return choices.map((c) => label[c]).join(" - ");
|
|
2890
|
+
}
|
|
2891
|
+
function parseChoice(answer, choices) {
|
|
2892
|
+
const a = answer.trim().toLowerCase();
|
|
2893
|
+
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;
|
|
2894
|
+
return picked && choices.includes(picked) ? picked : null;
|
|
2895
|
+
}
|
|
2896
|
+
function refusal(code, message2, reason) {
|
|
2897
|
+
return {
|
|
2898
|
+
ok: false,
|
|
2899
|
+
result: { code, message: message2, payload: { ok: false, error: reason, message: message2 }, isError: true }
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
var MAX_ROUNDS = 4;
|
|
2903
|
+
async function runIdentityGate(io, flags, verb, wording = ATTACH_WORDING) {
|
|
2904
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
2905
|
+
const probe = await io.probe();
|
|
2906
|
+
const decision = decideIdentity({
|
|
2907
|
+
probe,
|
|
2908
|
+
configAccountId: io.readConfigAccountId(),
|
|
2909
|
+
flags,
|
|
2910
|
+
interactive: io.interactive,
|
|
2911
|
+
consoleUrl: io.consoleUrl,
|
|
2912
|
+
verb,
|
|
2913
|
+
wording
|
|
2914
|
+
});
|
|
2915
|
+
if (decision.action === "proceed") {
|
|
2916
|
+
io.print(decision.note);
|
|
2917
|
+
return { ok: true, identity: decision.identity, note: decision.note };
|
|
2918
|
+
}
|
|
2919
|
+
if (decision.action === "refuse") {
|
|
2920
|
+
return refusal(decision.code, decision.message, "identity_unconfirmed");
|
|
2921
|
+
}
|
|
2922
|
+
io.print(decision.message);
|
|
2923
|
+
const menu = renderChoices(decision.choices, decision.identity !== null);
|
|
2924
|
+
let choice = null;
|
|
2925
|
+
while (choice === null) {
|
|
2926
|
+
choice = parseChoice(await io.ask(`${menu}: `), decision.choices);
|
|
2927
|
+
}
|
|
2928
|
+
if (choice === "continue") {
|
|
2929
|
+
const note = `${wording.willBe} ${formatShownIdentity(decision.identity)}.`;
|
|
2930
|
+
return { ok: true, identity: decision.identity, note };
|
|
2931
|
+
}
|
|
2932
|
+
if (choice === "quit") {
|
|
2933
|
+
return refusal(
|
|
2934
|
+
EXIT.IDENTITY_UNCONFIRMED,
|
|
2935
|
+
`${verb}: aborted - nothing was written.`,
|
|
2936
|
+
"aborted"
|
|
2937
|
+
);
|
|
2938
|
+
}
|
|
2939
|
+
const code = await io.runLogin(choice === "create" ? "create" : "select_account");
|
|
2940
|
+
if (code !== 0) {
|
|
2941
|
+
io.print(`hs login exited ${code}; the account was not changed.`);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
return refusal(
|
|
2945
|
+
EXIT.IDENTITY_UNCONFIRMED,
|
|
2946
|
+
`${verb}: refusing -- the account was still not confirmed after several sign-in attempts. Nothing was written.`,
|
|
2947
|
+
"identity_unconfirmed"
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
var WORKSPACE_WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
2951
|
+
"create",
|
|
2952
|
+
"rename",
|
|
2953
|
+
"unregister",
|
|
2954
|
+
"archive",
|
|
2955
|
+
"restore"
|
|
2956
|
+
]);
|
|
2957
|
+
async function gateWriteVerb(opts) {
|
|
2958
|
+
if (!opts.writeVerbs.has(opts.sub)) return null;
|
|
2959
|
+
return runIdentityGate(opts.io, opts.flags, `${opts.namespace} ${opts.sub}`, WRITE_WORDING);
|
|
2960
|
+
}
|
|
2961
|
+
function readConfiguredAccountId(env = process.env, homeDir = homedir9()) {
|
|
2962
|
+
return resolveSessionOwnerUncached({ env, homeDir });
|
|
2963
|
+
}
|
|
2964
|
+
function isInteractiveTerminal() {
|
|
2965
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY) && process.env.CI !== "true";
|
|
2966
|
+
}
|
|
2967
|
+
function defaultIdentityGateIo(opts) {
|
|
2968
|
+
const consoleUrl = opts.consoleUrl.trim().replace(/\/+$/, "");
|
|
2969
|
+
const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join9(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync4(p));
|
|
2970
|
+
return {
|
|
2971
|
+
consoleUrl,
|
|
2972
|
+
interactive: opts.interactive ?? isInteractiveTerminal(),
|
|
2973
|
+
probe: () => probeBearerIdentity({ consoleUrl, bearer: readPairedBearer(), fetchImpl: fetch }),
|
|
2974
|
+
readConfigAccountId: () => readConfiguredAccountId(),
|
|
2975
|
+
ask: async (question) => {
|
|
2976
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2977
|
+
try {
|
|
2978
|
+
return await rl.question(question);
|
|
2979
|
+
} finally {
|
|
2980
|
+
rl.close();
|
|
2981
|
+
}
|
|
2982
|
+
},
|
|
2983
|
+
runLogin: async (prompt) => {
|
|
2984
|
+
const flag = prompt === "create" ? "--create-account" : "--switch-account";
|
|
2985
|
+
if (!loginCli) {
|
|
2986
|
+
process.stderr.write(
|
|
2987
|
+
`Run \`hs login ${flag}\` in another terminal, then come back here.
|
|
2988
|
+
`
|
|
2989
|
+
);
|
|
2990
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2991
|
+
try {
|
|
2992
|
+
await rl.question("Press Enter when the sign-in has finished: ");
|
|
2993
|
+
} finally {
|
|
2994
|
+
rl.close();
|
|
2995
|
+
}
|
|
2996
|
+
return 0;
|
|
2997
|
+
}
|
|
2998
|
+
const res = spawnSync(
|
|
2999
|
+
process.execPath,
|
|
3000
|
+
[loginCli, "login", flag, "--console-url", consoleUrl],
|
|
3001
|
+
{
|
|
3002
|
+
stdio: "inherit",
|
|
3003
|
+
env: { ...process.env, HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1" }
|
|
3004
|
+
}
|
|
3005
|
+
);
|
|
3006
|
+
return res.status ?? 1;
|
|
3007
|
+
},
|
|
3008
|
+
print: (line) => process.stderr.write(`${line}
|
|
3009
|
+
`)
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// spex-console/src/lib/vault-registry/account-pointer.ts
|
|
3014
|
+
init_sql_orm_inert_cli_shim();
|
|
3015
|
+
|
|
3016
|
+
// spex-console/src/lib/cli/vault-workspace-shared.ts
|
|
3017
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3018
|
+
import os2 from "node:os";
|
|
3019
|
+
|
|
3020
|
+
// spex-console/src/lib/cli/default-workspace.ts
|
|
3021
|
+
function defaultWorkspaceId(accountId2) {
|
|
3022
|
+
const sanitized = accountId2.replace(/[/\\.\s]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3023
|
+
return `ws-${sanitized || "account"}`;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// spex-console/src/lib/cli/vault-workspace-shared.ts
|
|
3027
|
+
function resolveConfiguredAccountId(opts = {}) {
|
|
3028
|
+
return resolveSessionOwnerUncached({
|
|
3029
|
+
env: opts.env ?? process.env,
|
|
3030
|
+
homeDir: opts.homeDir ?? os2.homedir(),
|
|
3031
|
+
readFile: opts.readFile ?? ((p) => readFileSync8(p, "utf-8"))
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
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.";
|
|
3035
|
+
function resolveCloudAccountIdOrRefuse(opts = {}) {
|
|
3036
|
+
const accountId2 = resolveConfiguredAccountId(opts);
|
|
3037
|
+
return accountId2 ? { ok: true, accountId: accountId2 } : { ok: false, message: CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE };
|
|
3038
|
+
}
|
|
3039
|
+
var WORKSPACE_OWNED_BY_OTHER_ACCOUNT = "workspace_owned_by_other_account";
|
|
3040
|
+
function workspaceOwnedByOtherAccountMessage(id) {
|
|
3041
|
+
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.`;
|
|
3042
|
+
}
|
|
3043
|
+
function classifyHttpFailureReason(res) {
|
|
3044
|
+
if (res.status === 0) return "network-error";
|
|
3045
|
+
if (res.status === 401) return "unauthorized";
|
|
3046
|
+
return "server-error";
|
|
3047
|
+
}
|
|
3048
|
+
async function cloudHttpRequest(http, method, path4, body) {
|
|
3049
|
+
const scopeHeaders = http.scope ? http.scope.headersFor(method, path4) : {};
|
|
3050
|
+
try {
|
|
3051
|
+
const res = await http.fetchImpl(`${http.consoleUrl}${path4}`, {
|
|
3052
|
+
method,
|
|
3053
|
+
headers: {
|
|
3054
|
+
"content-type": "application/json",
|
|
3055
|
+
...http.token ? { authorization: `Bearer ${http.token}` } : {},
|
|
3056
|
+
...scopeHeaders
|
|
3057
|
+
},
|
|
3058
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
3059
|
+
});
|
|
3060
|
+
let json = null;
|
|
3061
|
+
try {
|
|
3062
|
+
json = await res.json();
|
|
3063
|
+
} catch {
|
|
3064
|
+
}
|
|
3065
|
+
return { ok: res.ok, status: res.status, json };
|
|
3066
|
+
} catch {
|
|
3067
|
+
return { ok: false, status: 0, json: null };
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
function isCloudWriteFailure(value) {
|
|
3071
|
+
return typeof value === "object" && value !== null && value.kind === "cloud_write_failed";
|
|
3072
|
+
}
|
|
3073
|
+
function cloudWriteFailure(http, res) {
|
|
3074
|
+
const where = http.consoleUrl;
|
|
3075
|
+
let message2;
|
|
3076
|
+
if (res.status === 0) {
|
|
3077
|
+
message2 = `could not reach the console at ${where} -- nothing was changed; check the connection and re-run the command to retry`;
|
|
3078
|
+
} else if (res.status === 401) {
|
|
3079
|
+
message2 = `the console at ${where} refused the sign-in token (HTTP 401) -- nothing was changed; run \`hs login\` and re-run the command`;
|
|
3080
|
+
} else {
|
|
3081
|
+
const code = errorCodeOf(res.json);
|
|
3082
|
+
message2 = `the console at ${where} answered HTTP ${res.status}${code ? ` (${code})` : ""} -- nothing was changed; re-run the command to retry`;
|
|
3083
|
+
}
|
|
3084
|
+
return { kind: "cloud_write_failed", status: res.status, message: message2 };
|
|
3085
|
+
}
|
|
3086
|
+
function errorCodeOf(json) {
|
|
3087
|
+
if (!json || typeof json !== "object") return null;
|
|
3088
|
+
const v = json.error;
|
|
3089
|
+
return typeof v === "string" ? v : null;
|
|
3090
|
+
}
|
|
3091
|
+
async function classifyWorkspaceConflictHttp(id, conflictJson, http) {
|
|
3092
|
+
if (errorCodeOf(conflictJson) === WORKSPACE_OWNED_BY_OTHER_ACCOUNT) {
|
|
3093
|
+
return { written: false, reason: "owned-by-other-account" };
|
|
3094
|
+
}
|
|
3095
|
+
if (conflictJson && typeof conflictJson === "object" && conflictJson.owned_by_caller === true) {
|
|
3096
|
+
return { written: true };
|
|
3097
|
+
}
|
|
3098
|
+
const list = await cloudHttpRequest(http, "GET", "/api/workspaces?include_archived=true");
|
|
3099
|
+
if (!list.ok) return { written: false, reason: classifyHttpFailureReason(list) };
|
|
3100
|
+
const rows = list.json?.workspaces;
|
|
3101
|
+
if (!Array.isArray(rows)) return { written: false, reason: "server-error" };
|
|
3102
|
+
return rows.some((w) => w.id === id) ? { written: true } : { written: false, reason: "owned-by-other-account" };
|
|
3103
|
+
}
|
|
3104
|
+
async function mutateHttp(http, method, path4, body, landed) {
|
|
3105
|
+
const res = await cloudHttpRequest(http, method, path4, body);
|
|
3106
|
+
if (res.ok) return landed;
|
|
3107
|
+
if (res.status === 404) return "not_found";
|
|
3108
|
+
return cloudWriteFailure(http, res);
|
|
3109
|
+
}
|
|
3110
|
+
function manageVerbPath(kind, id) {
|
|
3111
|
+
return `/api/${kind}/${encodeURIComponent(id)}`;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// spex-console/src/lib/vault-registry/account-pointer.ts
|
|
3115
|
+
init_postgres_client_cli_shim();
|
|
3116
|
+
init_postgres();
|
|
3117
|
+
|
|
3118
|
+
// spex-console/src/lib/vault-registry/neon-probe.ts
|
|
3119
|
+
var NEON_PROBE_TIMEOUT_MS = 1500;
|
|
3120
|
+
var SUSPENSION_ELAPSED_FACTOR = 3;
|
|
3121
|
+
var SUSPENSION_MIN_OVERSHOOT_MS = 5e3;
|
|
3122
|
+
var MAX_SUSPENSION_RETRIES = 2;
|
|
3123
|
+
function withTimeout(promise, ms, label) {
|
|
3124
|
+
return new Promise((resolve5, reject) => {
|
|
3125
|
+
const timer = setTimeout(() => reject(new NeonProbeTimeoutError(label, ms)), ms);
|
|
3126
|
+
promise.then(
|
|
3127
|
+
(value) => {
|
|
3128
|
+
clearTimeout(timer);
|
|
3129
|
+
resolve5(value);
|
|
3130
|
+
},
|
|
3131
|
+
(err) => {
|
|
3132
|
+
clearTimeout(timer);
|
|
3133
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
3134
|
+
}
|
|
3135
|
+
);
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
var NeonProbeTimeoutError = class _NeonProbeTimeoutError extends Error {
|
|
3139
|
+
timeoutMs;
|
|
3140
|
+
constructor(label, timeoutMs) {
|
|
3141
|
+
super(`${label} timed out after ${timeoutMs}ms`);
|
|
3142
|
+
this.name = "NeonProbeTimeoutError";
|
|
3143
|
+
this.timeoutMs = timeoutMs;
|
|
3144
|
+
Object.setPrototypeOf(this, _NeonProbeTimeoutError.prototype);
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
function sleep(ms) {
|
|
3148
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
1704
3149
|
}
|
|
1705
|
-
function
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
registry.active_workspace_id = null;
|
|
3150
|
+
function emitProbeFailure(options, failure) {
|
|
3151
|
+
try {
|
|
3152
|
+
options?.onFailure?.(failure);
|
|
3153
|
+
} catch {
|
|
1710
3154
|
}
|
|
1711
|
-
writeVaultRegistry(registry, opts);
|
|
1712
|
-
return registry;
|
|
1713
3155
|
}
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
3156
|
+
async function runNeonReachabilityProbe(attempt, options = {}, label = "Neon reachability probe") {
|
|
3157
|
+
const timeoutMs = options.timeoutMs ?? NEON_PROBE_TIMEOUT_MS;
|
|
3158
|
+
const backoffMs = options.backoffMs ?? [];
|
|
3159
|
+
const deadlineMs = options.deadlineMs ?? Number.POSITIVE_INFINITY;
|
|
3160
|
+
const attempts = backoffMs.length + 1;
|
|
3161
|
+
let startedAt = Date.now();
|
|
3162
|
+
let suspensions = 0;
|
|
3163
|
+
for (let i = 0; i < attempts; i++) {
|
|
3164
|
+
const attemptStartedAt = Date.now();
|
|
3165
|
+
try {
|
|
3166
|
+
await withTimeout(Promise.resolve().then(attempt), timeoutMs, label);
|
|
3167
|
+
return true;
|
|
3168
|
+
} catch (err) {
|
|
3169
|
+
const elapsedMs = Date.now() - attemptStartedAt;
|
|
3170
|
+
const suspended = elapsedMs >= timeoutMs * SUSPENSION_ELAPSED_FACTOR && elapsedMs - timeoutMs >= SUSPENSION_MIN_OVERSHOOT_MS;
|
|
3171
|
+
if (suspended && suspensions < MAX_SUSPENSION_RETRIES) {
|
|
3172
|
+
suspensions += 1;
|
|
3173
|
+
emitProbeFailure(options, {
|
|
3174
|
+
reason: "suspended",
|
|
3175
|
+
attempt: i + 1,
|
|
3176
|
+
attempts,
|
|
3177
|
+
timeoutMs,
|
|
3178
|
+
elapsedMs,
|
|
3179
|
+
willRetry: true,
|
|
3180
|
+
error: err
|
|
3181
|
+
});
|
|
3182
|
+
startedAt = Date.now();
|
|
3183
|
+
i -= 1;
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
const isLast = i === attempts - 1;
|
|
3187
|
+
const pastDeadline = Date.now() - startedAt >= deadlineMs;
|
|
3188
|
+
const willRetry = !isLast && !pastDeadline;
|
|
3189
|
+
emitProbeFailure(options, {
|
|
3190
|
+
reason: err instanceof NeonProbeTimeoutError ? "timeout" : "error",
|
|
3191
|
+
attempt: i + 1,
|
|
3192
|
+
attempts,
|
|
3193
|
+
timeoutMs,
|
|
3194
|
+
elapsedMs,
|
|
3195
|
+
willRetry,
|
|
3196
|
+
error: err
|
|
3197
|
+
});
|
|
3198
|
+
if (!willRetry) return false;
|
|
3199
|
+
await sleep(backoffMs[i]);
|
|
3200
|
+
}
|
|
1719
3201
|
}
|
|
1720
|
-
|
|
3202
|
+
return false;
|
|
3203
|
+
}
|
|
1721
3204
|
|
|
1722
3205
|
// spex-console/src/lib/vault-registry/account-pointer.ts
|
|
3206
|
+
var UnknownWorkspaceIdError = class extends Error {
|
|
3207
|
+
workspaceId;
|
|
3208
|
+
constructor(workspaceId) {
|
|
3209
|
+
super(
|
|
3210
|
+
`setActiveWorkspace: unknown workspace id "${workspaceId}" \u2014 no matching row in Neon "workspaces" or registry.json's workspaces[] list. Nothing was written.`
|
|
3211
|
+
);
|
|
3212
|
+
this.name = "UnknownWorkspaceIdError";
|
|
3213
|
+
this.workspaceId = workspaceId;
|
|
3214
|
+
}
|
|
3215
|
+
};
|
|
3216
|
+
var CloudPointerWriteError = class extends Error {
|
|
3217
|
+
status;
|
|
3218
|
+
consoleUrl;
|
|
3219
|
+
constructor(consoleUrl, status, code) {
|
|
3220
|
+
super(
|
|
3221
|
+
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`
|
|
3222
|
+
);
|
|
3223
|
+
this.name = "CloudPointerWriteError";
|
|
3224
|
+
this.status = status;
|
|
3225
|
+
this.consoleUrl = consoleUrl;
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
1723
3228
|
async function isNeonReachable(deps) {
|
|
1724
3229
|
if (deps.neonReachable !== void 0) return deps.neonReachable;
|
|
1725
3230
|
const usingInjectedDb = deps.db !== void 0;
|
|
@@ -1742,6 +3247,19 @@ async function isNeonReachable(deps) {
|
|
|
1742
3247
|
await db.execute(sql`select 1`);
|
|
1743
3248
|
}, deps.probe);
|
|
1744
3249
|
}
|
|
3250
|
+
function shouldUseHttp(deps) {
|
|
3251
|
+
return deps.db === void 0 && deps.http !== void 0;
|
|
3252
|
+
}
|
|
3253
|
+
async function readPointerHttp(http) {
|
|
3254
|
+
const res = await cloudHttpRequest(http, "GET", "/api/vaults/active");
|
|
3255
|
+
if (!res.ok || !res.json || typeof res.json !== "object") return null;
|
|
3256
|
+
return res.json;
|
|
3257
|
+
}
|
|
3258
|
+
function errorCodeOf2(json) {
|
|
3259
|
+
if (!json || typeof json !== "object") return null;
|
|
3260
|
+
const v = json.error;
|
|
3261
|
+
return typeof v === "string" ? v : null;
|
|
3262
|
+
}
|
|
1745
3263
|
async function accountSettingsRow(accountId2, db) {
|
|
1746
3264
|
const rows = await db.select().from(accountSettings).where(eq(accountSettings.accountId, accountId2)).limit(1);
|
|
1747
3265
|
return rows[0] ?? null;
|
|
@@ -1761,14 +3279,30 @@ function workspaceExistsInRegistry(workspaceId, opts) {
|
|
|
1761
3279
|
return loadVaultRegistry(opts).workspaces.some((w) => w.id === workspaceId);
|
|
1762
3280
|
}
|
|
1763
3281
|
async function getActiveWorkspace(accountId2, deps = {}) {
|
|
1764
|
-
if (
|
|
3282
|
+
if (shouldUseHttp(deps)) {
|
|
3283
|
+
const body = await readPointerHttp(deps.http);
|
|
3284
|
+
if (body) {
|
|
3285
|
+
return typeof body.workspace_id === "string" && body.workspace_id ? body.workspace_id : null;
|
|
3286
|
+
}
|
|
3287
|
+
} else if (await isNeonReachable(deps)) {
|
|
1765
3288
|
const db = deps.db ?? getPostgresDb();
|
|
1766
3289
|
const row = await accountSettingsRow(accountId2, db);
|
|
1767
3290
|
return row?.activeWorkspaceId ?? null;
|
|
1768
3291
|
}
|
|
1769
|
-
return loadVaultRegistry(deps.registryOpts).active_workspace_id;
|
|
3292
|
+
return accountPointersOf(loadVaultRegistry(deps.registryOpts), accountId2).active_workspace_id;
|
|
1770
3293
|
}
|
|
1771
3294
|
async function setActiveWorkspace(accountId2, workspaceId, deps = {}) {
|
|
3295
|
+
if (shouldUseHttp(deps)) {
|
|
3296
|
+
const res = await cloudHttpRequest(deps.http, "POST", "/api/vaults/active", {
|
|
3297
|
+
workspace_id: workspaceId
|
|
3298
|
+
});
|
|
3299
|
+
if (res.ok) return;
|
|
3300
|
+
const code = errorCodeOf2(res.json);
|
|
3301
|
+
if (res.status === 404 && code === "unknown_workspace") {
|
|
3302
|
+
throw new UnknownWorkspaceIdError(workspaceId);
|
|
3303
|
+
}
|
|
3304
|
+
throw new CloudPointerWriteError(deps.http.consoleUrl, res.status, code);
|
|
3305
|
+
}
|
|
1772
3306
|
if (await isNeonReachable(deps)) {
|
|
1773
3307
|
const db = deps.db ?? getPostgresDb();
|
|
1774
3308
|
if (!await workspaceExistsInNeon(accountId2, workspaceId, db)) {
|
|
@@ -1781,108 +3315,50 @@ async function setActiveWorkspace(accountId2, workspaceId, deps = {}) {
|
|
|
1781
3315
|
if (!workspaceExistsInRegistry(workspaceId, registryOpts)) {
|
|
1782
3316
|
throw new UnknownWorkspaceIdError(workspaceId);
|
|
1783
3317
|
}
|
|
1784
|
-
setActiveWorkspaceId(workspaceId, registryOpts);
|
|
3318
|
+
setActiveWorkspaceId(accountId2, workspaceId, registryOpts);
|
|
1785
3319
|
}
|
|
1786
3320
|
async function clearActiveWorkspace(accountId2, deps = {}) {
|
|
1787
|
-
if (await isNeonReachable(deps)) {
|
|
3321
|
+
if (!shouldUseHttp(deps) && await isNeonReachable(deps)) {
|
|
1788
3322
|
const db = deps.db ?? getPostgresDb();
|
|
1789
3323
|
await upsertAccountSettings(accountId2, { activeWorkspaceId: null }, db);
|
|
1790
3324
|
return;
|
|
1791
3325
|
}
|
|
1792
|
-
setActiveWorkspaceId(null, deps.registryOpts ?? {});
|
|
3326
|
+
setActiveWorkspaceId(accountId2, null, deps.registryOpts ?? {});
|
|
1793
3327
|
}
|
|
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
|
-
};
|
|
3328
|
+
|
|
3329
|
+
// dist/cli/._admin-scope-inert.cli-shim.mjs
|
|
3330
|
+
async function resolveScopedAccountId(callerAccountId, requestedAccountId) {
|
|
3331
|
+
const target = requestedAccountId == null ? "" : String(requestedAccountId).trim();
|
|
3332
|
+
if (!target || target === callerAccountId) {
|
|
3333
|
+
return { ok: true, accountId: callerAccountId };
|
|
1813
3334
|
}
|
|
1814
|
-
|
|
3335
|
+
return {
|
|
3336
|
+
ok: false,
|
|
3337
|
+
code: "forbidden_account_override",
|
|
3338
|
+
message: "Only a builder/admin may act on another account's workspaces or vaults."
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
// spex-console/src/lib/vault-registry/vault-workspace-store.ts
|
|
3343
|
+
init_sql_orm_inert_cli_shim();
|
|
3344
|
+
init_postgres_client_cli_shim();
|
|
3345
|
+
init_postgres();
|
|
1815
3346
|
|
|
1816
3347
|
// spex-console/src/lib/vault-registry/repo-identity.ts
|
|
1817
3348
|
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
3349
|
|
|
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
|
-
}
|
|
3350
|
+
// spex-console/src/lib/vault-registry/vault-workspace-store.ts
|
|
3351
|
+
function mapProbeFailureReason(reason) {
|
|
3352
|
+
return reason === "not-configured" ? "not-configured" : "network-error";
|
|
1862
3353
|
}
|
|
1863
3354
|
function shouldUseCloudHttp(deps) {
|
|
1864
|
-
return deps.db === void 0 && deps.http !== void 0
|
|
3355
|
+
return deps.db === void 0 && deps.http !== void 0;
|
|
1865
3356
|
}
|
|
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;
|
|
3357
|
+
async function isNeonReachableForWriteWithReason(deps) {
|
|
3358
|
+
if (shouldUseCloudHttp(deps)) return { reachable: false, reason: "not-configured" };
|
|
3359
|
+
if (deps.neonReachable !== void 0) {
|
|
3360
|
+
return deps.neonReachable ? { reachable: true } : { reachable: false, reason: "network-error" };
|
|
3361
|
+
}
|
|
1886
3362
|
const usingInjectedDb = deps.db !== void 0;
|
|
1887
3363
|
if (!usingInjectedDb && !isPostgresConfigured()) {
|
|
1888
3364
|
emitProbeFailure(deps.probe, {
|
|
@@ -1896,19 +3372,46 @@ async function isNeonReachableForWrite(deps) {
|
|
|
1896
3372
|
"isPostgresConfigured() is false \u2014 no DATABASE_URL env var and no usable ~/.heyspex/credentials.yaml. Neon was never contacted."
|
|
1897
3373
|
)
|
|
1898
3374
|
});
|
|
1899
|
-
return false;
|
|
3375
|
+
return { reachable: false, reason: "not-configured" };
|
|
1900
3376
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
3377
|
+
let reason = "network-error";
|
|
3378
|
+
const reachable = await runNeonReachabilityProbe(
|
|
3379
|
+
async () => {
|
|
3380
|
+
const db = deps.db ?? getPostgresDb();
|
|
3381
|
+
await db.execute(sql`select 1`);
|
|
3382
|
+
},
|
|
3383
|
+
{
|
|
3384
|
+
...deps.probe,
|
|
3385
|
+
onFailure: (failure) => {
|
|
3386
|
+
reason = mapProbeFailureReason(failure.reason);
|
|
3387
|
+
deps.probe?.onFailure?.(failure);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
);
|
|
3391
|
+
return reachable ? { reachable: true } : { reachable: false, reason };
|
|
3392
|
+
}
|
|
3393
|
+
async function isNeonReachableForWrite(deps) {
|
|
3394
|
+
return (await isNeonReachableForWriteWithReason(deps)).reachable;
|
|
1905
3395
|
}
|
|
1906
|
-
async function
|
|
1907
|
-
if (
|
|
1908
|
-
|
|
3396
|
+
async function upsertWorkspaceNeonReasoned(record, accountId2, deps = {}) {
|
|
3397
|
+
if (isReservedForOtherAccountsDefault(record.id, accountId2)) {
|
|
3398
|
+
return { written: false, reason: "owned-by-other-account" };
|
|
3399
|
+
}
|
|
3400
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3401
|
+
const res = await cloudHttpRequest(deps.http, "POST", "/api/workspaces", {
|
|
3402
|
+
id: record.id,
|
|
3403
|
+
display_name: record.displayName,
|
|
3404
|
+
...record.githubOrg ? { github_org: record.githubOrg } : {}
|
|
3405
|
+
});
|
|
3406
|
+
if (res.status === 201) return { written: true };
|
|
3407
|
+
if (res.status === 409) return classifyWorkspaceConflictHttp(record.id, res.json, deps.http);
|
|
3408
|
+
return { written: false, reason: classifyHttpFailureReason(res) };
|
|
3409
|
+
}
|
|
3410
|
+
const reach = await isNeonReachableForWriteWithReason(deps);
|
|
3411
|
+
if (!reach.reachable) return { written: false, reason: reach.reason };
|
|
1909
3412
|
try {
|
|
1910
3413
|
const db = deps.db ?? getPostgresDb();
|
|
1911
|
-
await db.insert(workspaces).values({
|
|
3414
|
+
const rows = await db.insert(workspaces).values({
|
|
1912
3415
|
id: record.id,
|
|
1913
3416
|
accountId: accountId2,
|
|
1914
3417
|
displayName: record.displayName,
|
|
@@ -1918,16 +3421,27 @@ async function upsertWorkspaceNeon(record, accountId2, deps = {}) {
|
|
|
1918
3421
|
set: {
|
|
1919
3422
|
displayName: record.displayName,
|
|
1920
3423
|
...record.githubOrg ? { githubOrg: record.githubOrg } : {}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
|
|
3424
|
+
},
|
|
3425
|
+
setWhere: eq(workspaces.accountId, accountId2)
|
|
3426
|
+
}).returning({ id: workspaces.id });
|
|
3427
|
+
return rows.length > 0 ? { written: true } : { written: false, reason: "owned-by-other-account" };
|
|
1924
3428
|
} catch {
|
|
1925
|
-
return false;
|
|
3429
|
+
return { written: false, reason: "server-error" };
|
|
1926
3430
|
}
|
|
1927
3431
|
}
|
|
1928
|
-
async function
|
|
1929
|
-
if (shouldUseCloudHttp(deps))
|
|
1930
|
-
|
|
3432
|
+
async function insertWorkspaceNeonStrictReasoned(record, accountId2, deps = {}) {
|
|
3433
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3434
|
+
const res = await cloudHttpRequest(deps.http, "POST", "/api/workspaces", {
|
|
3435
|
+
id: record.id,
|
|
3436
|
+
display_name: record.displayName,
|
|
3437
|
+
...record.githubOrg ? { github_org: record.githubOrg } : {}
|
|
3438
|
+
});
|
|
3439
|
+
if (res.status === 201) return { result: "inserted" };
|
|
3440
|
+
if (res.status === 409) return { result: "conflict" };
|
|
3441
|
+
return { result: "unreachable", reason: classifyHttpFailureReason(res) };
|
|
3442
|
+
}
|
|
3443
|
+
const reach = await isNeonReachableForWriteWithReason(deps);
|
|
3444
|
+
if (!reach.reachable) return { result: "unreachable", reason: reach.reason };
|
|
1931
3445
|
try {
|
|
1932
3446
|
const db = deps.db ?? getPostgresDb();
|
|
1933
3447
|
const rows = await db.insert(workspaces).values({
|
|
@@ -1936,9 +3450,9 @@ async function insertWorkspaceNeonStrict(record, accountId2, deps = {}) {
|
|
|
1936
3450
|
displayName: record.displayName,
|
|
1937
3451
|
...record.githubOrg ? { githubOrg: record.githubOrg } : {}
|
|
1938
3452
|
}).onConflictDoNothing({ target: workspaces.id }).returning({ id: workspaces.id });
|
|
1939
|
-
return rows.length > 0 ? "inserted" : "conflict";
|
|
3453
|
+
return { result: rows.length > 0 ? "inserted" : "conflict" };
|
|
1940
3454
|
} catch {
|
|
1941
|
-
return "unreachable";
|
|
3455
|
+
return { result: "unreachable", reason: "server-error" };
|
|
1942
3456
|
}
|
|
1943
3457
|
}
|
|
1944
3458
|
async function listWorkspacesNeon(accountId2, deps = {}) {
|
|
@@ -1962,6 +3476,15 @@ async function listWorkspacesNeon(accountId2, deps = {}) {
|
|
|
1962
3476
|
}
|
|
1963
3477
|
}
|
|
1964
3478
|
async function renameWorkspaceNeon(id, displayName, accountId2, deps = {}) {
|
|
3479
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3480
|
+
return mutateHttp(
|
|
3481
|
+
deps.http,
|
|
3482
|
+
"PATCH",
|
|
3483
|
+
manageVerbPath("workspaces", id),
|
|
3484
|
+
{ display_name: displayName },
|
|
3485
|
+
"updated"
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
1965
3488
|
if (!await isNeonReachableForWrite(deps)) return "unreachable";
|
|
1966
3489
|
try {
|
|
1967
3490
|
const db = deps.db ?? getPostgresDb();
|
|
@@ -1972,6 +3495,15 @@ async function renameWorkspaceNeon(id, displayName, accountId2, deps = {}) {
|
|
|
1972
3495
|
}
|
|
1973
3496
|
}
|
|
1974
3497
|
async function setWorkspaceArchivedNeon(id, archived, accountId2, deps = {}) {
|
|
3498
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3499
|
+
return mutateHttp(
|
|
3500
|
+
deps.http,
|
|
3501
|
+
"PATCH",
|
|
3502
|
+
manageVerbPath("workspaces", id),
|
|
3503
|
+
{ archived },
|
|
3504
|
+
"updated"
|
|
3505
|
+
);
|
|
3506
|
+
}
|
|
1975
3507
|
if (!await isNeonReachableForWrite(deps)) return "unreachable";
|
|
1976
3508
|
try {
|
|
1977
3509
|
const db = deps.db ?? getPostgresDb();
|
|
@@ -1982,6 +3514,9 @@ async function setWorkspaceArchivedNeon(id, archived, accountId2, deps = {}) {
|
|
|
1982
3514
|
}
|
|
1983
3515
|
}
|
|
1984
3516
|
async function deleteWorkspaceNeon(id, accountId2, deps = {}) {
|
|
3517
|
+
if (shouldUseCloudHttp(deps)) {
|
|
3518
|
+
return mutateHttp(deps.http, "DELETE", manageVerbPath("workspaces", id), void 0, "deleted");
|
|
3519
|
+
}
|
|
1985
3520
|
if (!await isNeonReachableForWrite(deps)) return "unreachable";
|
|
1986
3521
|
try {
|
|
1987
3522
|
const db = deps.db ?? getPostgresDb();
|
|
@@ -1991,146 +3526,22 @@ async function deleteWorkspaceNeon(id, accountId2, deps = {}) {
|
|
|
1991
3526
|
return "unreachable";
|
|
1992
3527
|
}
|
|
1993
3528
|
}
|
|
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(/\/+$/, "");
|
|
3529
|
+
async function findWorkspaceForAccountNeon(workspaceId, accountId2, deps = {}) {
|
|
3530
|
+
if (!await isNeonReachableForWrite(deps)) return "unreachable";
|
|
3531
|
+
const db = deps.db ?? getPostgresDb();
|
|
2070
3532
|
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(/\/+$/, "");
|
|
3533
|
+
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);
|
|
3534
|
+
const row = rows[0];
|
|
3535
|
+
return row ? { id: row.id, displayName: row.displayName } : null;
|
|
2077
3536
|
} catch {
|
|
3537
|
+
return "unreachable";
|
|
2078
3538
|
}
|
|
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
|
-
}
|
|
2090
|
-
var HELP = [
|
|
2091
|
-
"Usage:",
|
|
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
|
-
};
|
|
2126
3539
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
3540
|
+
function isReservedForOtherAccountsDefault(workspaceId, accountId2) {
|
|
3541
|
+
return workspaceId.startsWith("ws-user_") && workspaceId !== defaultWorkspaceId(accountId2);
|
|
3542
|
+
}
|
|
2130
3543
|
|
|
2131
3544
|
// spex-console/src/lib/vault-registry/active-repoint.ts
|
|
2132
|
-
init_account_pointer();
|
|
2133
|
-
init_registry_file();
|
|
2134
3545
|
function pointerDeps(deps) {
|
|
2135
3546
|
return deps.accountPointer ?? { registryOpts: deps.registryOpts };
|
|
2136
3547
|
}
|
|
@@ -2151,7 +3562,9 @@ async function repointActiveWorkspaceOnRemoval(accountId2, removedWorkspaceId, d
|
|
|
2151
3562
|
}
|
|
2152
3563
|
|
|
2153
3564
|
// spex-console/src/lib/vault-registry/archive.ts
|
|
2154
|
-
|
|
3565
|
+
function cloudWriteFailed(failure) {
|
|
3566
|
+
return { ok: false, code: "cloud_write_failed", message: failure.message };
|
|
3567
|
+
}
|
|
2155
3568
|
function workspaceNotFound(id) {
|
|
2156
3569
|
return { ok: false, code: "workspace_not_found", message: `workspace "${id}" not found` };
|
|
2157
3570
|
}
|
|
@@ -2166,6 +3579,7 @@ async function setWorkspaceArchivedState(input, archived, deps = {}) {
|
|
|
2166
3579
|
deps.neon ?? {}
|
|
2167
3580
|
);
|
|
2168
3581
|
if (neonResult === "not_found") return workspaceNotFound(input.id);
|
|
3582
|
+
if (isCloudWriteFailure(neonResult)) return cloudWriteFailed(neonResult);
|
|
2169
3583
|
if (archived) {
|
|
2170
3584
|
await repointActiveWorkspaceOnRemoval(input.accountId, input.id, {
|
|
2171
3585
|
registryOpts: deps.registryOpts,
|
|
@@ -2218,24 +3632,68 @@ function buildCloseNotDestroyConfirm(kind, verb, displayName) {
|
|
|
2218
3632
|
}
|
|
2219
3633
|
|
|
2220
3634
|
// spex-console/src/lib/vault-registry/list.ts
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
3635
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
3636
|
+
|
|
3637
|
+
// spex-console/src/lib/vault-registry/side-effects.ts
|
|
3638
|
+
function isLocalRegistryHost() {
|
|
3639
|
+
const override = process.env.HEYSPEX_VAULT_WRITE_MODE?.trim().toLowerCase();
|
|
3640
|
+
if (override === "cloud") return false;
|
|
3641
|
+
if (override === "local") return true;
|
|
3642
|
+
return process.env.VERCEL !== "1";
|
|
3643
|
+
}
|
|
3644
|
+
function sideEffectErrorMessage(err) {
|
|
3645
|
+
return err instanceof Error ? err.message : String(err);
|
|
3646
|
+
}
|
|
3647
|
+
function runRegistrySideEffect(write) {
|
|
3648
|
+
const step = "registry_json";
|
|
3649
|
+
if (!isLocalRegistryHost()) return { step, ok: true, skipped: true };
|
|
3650
|
+
try {
|
|
3651
|
+
write();
|
|
3652
|
+
return { step, ok: true };
|
|
3653
|
+
} catch (err) {
|
|
3654
|
+
const error = sideEffectErrorMessage(err);
|
|
3655
|
+
console.error(`[vault-registry] post-write side step "${step}" failed: ${error}`);
|
|
3656
|
+
return { step, ok: false, error };
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
function registryWritten(effect) {
|
|
3660
|
+
return effect.ok && effect.skipped !== true;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
// spex-console/src/lib/vault-registry/list.ts
|
|
2225
3664
|
function pointerDeps2(deps) {
|
|
2226
3665
|
return deps.accountPointer ?? { registryOpts: deps.registryOpts };
|
|
2227
3666
|
}
|
|
3667
|
+
async function resolveSharedNeonReachable(deps, pDeps) {
|
|
3668
|
+
if (pDeps.neonReachable !== void 0) return pDeps.neonReachable;
|
|
3669
|
+
if (!isLocalRegistryHost()) return true;
|
|
3670
|
+
const probe = deps.neonReachableProbe ?? ((d) => isNeonReachableForWrite({ db: d.db, probe: d.probe }));
|
|
3671
|
+
return probe(pDeps);
|
|
3672
|
+
}
|
|
2228
3673
|
function localWorkspaceRows(deps) {
|
|
2229
3674
|
return loadVaultRegistry(deps.registryOpts).workspaces;
|
|
2230
3675
|
}
|
|
3676
|
+
function originTaggedWorkspaceRows(neonRows, deps) {
|
|
3677
|
+
if (neonRows === null) {
|
|
3678
|
+
const rows = deps.allowLocalRegistryFallback ? localWorkspaceRows(deps) : [];
|
|
3679
|
+
return rows.map((w) => ({ ...w, origin: "registry-only" }));
|
|
3680
|
+
}
|
|
3681
|
+
const cloudRows = neonRows.map((w) => ({ ...w, origin: "cloud-backed" }));
|
|
3682
|
+
if (!deps.allowLocalRegistryFallback) return cloudRows;
|
|
3683
|
+
const cloudIds = new Set(cloudRows.map((w) => w.id));
|
|
3684
|
+
const localOnlyRows = localWorkspaceRows(deps).filter((w) => !cloudIds.has(w.id)).map((w) => ({ ...w, origin: "registry-only" }));
|
|
3685
|
+
return [...cloudRows, ...localOnlyRows];
|
|
3686
|
+
}
|
|
2231
3687
|
async function listWorkspaces(accountId2, deps = {}) {
|
|
2232
3688
|
const pDeps = pointerDeps2(deps);
|
|
3689
|
+
const neonReachable = await resolveSharedNeonReachable(deps, pDeps);
|
|
3690
|
+
const sharedPointerDeps = { ...pDeps, neonReachable };
|
|
2233
3691
|
const neonRows = await (deps.neonListWorkspaces ?? listWorkspacesNeon)(accountId2, {
|
|
2234
3692
|
db: pDeps.db,
|
|
2235
|
-
neonReachable
|
|
3693
|
+
neonReachable
|
|
2236
3694
|
});
|
|
2237
|
-
const rows = neonRows
|
|
2238
|
-
const activeId = await getActiveWorkspace(accountId2,
|
|
3695
|
+
const rows = originTaggedWorkspaceRows(neonRows, deps);
|
|
3696
|
+
const activeId = await getActiveWorkspace(accountId2, sharedPointerDeps);
|
|
2239
3697
|
const scoped = deps.includeArchived ? rows : rows.filter((w) => !w.archived);
|
|
2240
3698
|
return {
|
|
2241
3699
|
workspaces: scoped.map((w) => ({ ...w, active: w.id === activeId })),
|
|
@@ -2243,12 +3701,7 @@ async function listWorkspaces(accountId2, deps = {}) {
|
|
|
2243
3701
|
};
|
|
2244
3702
|
}
|
|
2245
3703
|
|
|
2246
|
-
// spex-console/src/lib/cli/workspace.ts
|
|
2247
|
-
init_registry_file();
|
|
2248
|
-
|
|
2249
3704
|
// spex-console/src/lib/vault-registry/workspace-create.ts
|
|
2250
|
-
init_vault_workspace_shared();
|
|
2251
|
-
init_registry_file();
|
|
2252
3705
|
var MAX_DISPLAY_NAME_LEN = 60;
|
|
2253
3706
|
var MAX_SLUG_LEN = 40;
|
|
2254
3707
|
var MAX_SLUG_SUFFIX = 9;
|
|
@@ -2285,7 +3738,7 @@ function exhaustedSlugError(trimmedName, base) {
|
|
|
2285
3738
|
message: `could not find a free slug for "${trimmedName}" -- ${base} through ${base}-${MAX_SLUG_SUFFIX} are all taken`
|
|
2286
3739
|
};
|
|
2287
3740
|
}
|
|
2288
|
-
function createOffline(input, trimmedName, now, deps) {
|
|
3741
|
+
function createOffline(input, trimmedName, now, deps, neonWriteReason) {
|
|
2289
3742
|
const existingIds = new Set(loadVaultRegistry(deps.registryOpts).workspaces.map((w) => w.id));
|
|
2290
3743
|
let id;
|
|
2291
3744
|
if (input.id !== void 0) {
|
|
@@ -2303,10 +3756,22 @@ function createOffline(input, trimmedName, now, deps) {
|
|
|
2303
3756
|
if (!free) return exhaustedSlugError(trimmedName, base);
|
|
2304
3757
|
id = free;
|
|
2305
3758
|
}
|
|
2306
|
-
|
|
3759
|
+
const registry = runRegistrySideEffect(
|
|
3760
|
+
() => registerWorkspace(
|
|
3761
|
+
{ id, display_name: trimmedName, created_at: now, account_id: input.accountId },
|
|
3762
|
+
deps.registryOpts
|
|
3763
|
+
)
|
|
3764
|
+
);
|
|
2307
3765
|
return {
|
|
2308
3766
|
ok: true,
|
|
2309
|
-
workspace: {
|
|
3767
|
+
workspace: {
|
|
3768
|
+
id,
|
|
3769
|
+
display_name: trimmedName,
|
|
3770
|
+
neon_written: false,
|
|
3771
|
+
...neonWriteReason ? { neon_write_reason: neonWriteReason } : {},
|
|
3772
|
+
registry_written: registryWritten(registry),
|
|
3773
|
+
side_effects: [registry]
|
|
3774
|
+
}
|
|
2310
3775
|
};
|
|
2311
3776
|
}
|
|
2312
3777
|
async function createWorkspace(input, deps = {}) {
|
|
@@ -2328,19 +3793,40 @@ async function createWorkspace(input, deps = {}) {
|
|
|
2328
3793
|
};
|
|
2329
3794
|
}
|
|
2330
3795
|
const id = input.id;
|
|
2331
|
-
|
|
2332
|
-
|
|
3796
|
+
if (isReservedForOtherAccountsDefault(id, input.accountId)) {
|
|
3797
|
+
return {
|
|
3798
|
+
ok: false,
|
|
3799
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
3800
|
+
message: workspaceOwnedByOtherAccountMessage(id)
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
const upserted = await upsertWorkspaceNeonReasoned(
|
|
2333
3804
|
{ id, displayName: trimmedName },
|
|
2334
3805
|
input.accountId,
|
|
2335
3806
|
deps.neon
|
|
2336
3807
|
);
|
|
3808
|
+
if (upserted.reason === "owned-by-other-account") {
|
|
3809
|
+
return {
|
|
3810
|
+
ok: false,
|
|
3811
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
3812
|
+
message: workspaceOwnedByOtherAccountMessage(id)
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
const registry = runRegistrySideEffect(
|
|
3816
|
+
() => registerWorkspace(
|
|
3817
|
+
{ id, display_name: trimmedName, created_at: now, account_id: input.accountId },
|
|
3818
|
+
deps.registryOpts
|
|
3819
|
+
)
|
|
3820
|
+
);
|
|
2337
3821
|
return {
|
|
2338
3822
|
ok: true,
|
|
2339
3823
|
workspace: {
|
|
2340
3824
|
id,
|
|
2341
3825
|
display_name: trimmedName,
|
|
2342
|
-
neon_written:
|
|
2343
|
-
|
|
3826
|
+
neon_written: upserted.written,
|
|
3827
|
+
...upserted.reason ? { neon_write_reason: upserted.reason } : {},
|
|
3828
|
+
registry_written: registryWritten(registry),
|
|
3829
|
+
side_effects: [registry]
|
|
2344
3830
|
}
|
|
2345
3831
|
};
|
|
2346
3832
|
}
|
|
@@ -2351,17 +3837,31 @@ async function createWorkspace(input, deps = {}) {
|
|
|
2351
3837
|
message: `invalid workspace id "${input.id}" -- must be non-empty with no path separators`
|
|
2352
3838
|
};
|
|
2353
3839
|
}
|
|
3840
|
+
if (input.id !== void 0 && isReservedForOtherAccountsDefault(input.id, input.accountId)) {
|
|
3841
|
+
return {
|
|
3842
|
+
ok: false,
|
|
3843
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
3844
|
+
message: workspaceOwnedByOtherAccountMessage(input.id)
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
2354
3847
|
const { base, candidates } = input.id !== void 0 ? { base: input.id, candidates: [input.id] } : derivedCandidates(trimmedName);
|
|
2355
3848
|
for (const candidate of candidates) {
|
|
2356
|
-
const result = await
|
|
3849
|
+
const { result, reason } = await insertWorkspaceNeonStrictReasoned(
|
|
2357
3850
|
{ id: candidate, displayName: trimmedName },
|
|
2358
3851
|
input.accountId,
|
|
2359
3852
|
deps.neon ?? {}
|
|
2360
3853
|
);
|
|
2361
3854
|
if (result === "inserted") {
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
3855
|
+
const registry = runRegistrySideEffect(
|
|
3856
|
+
() => registerWorkspace(
|
|
3857
|
+
{
|
|
3858
|
+
id: candidate,
|
|
3859
|
+
display_name: trimmedName,
|
|
3860
|
+
created_at: now,
|
|
3861
|
+
account_id: input.accountId
|
|
3862
|
+
},
|
|
3863
|
+
deps.registryOpts
|
|
3864
|
+
)
|
|
2365
3865
|
);
|
|
2366
3866
|
return {
|
|
2367
3867
|
ok: true,
|
|
@@ -2369,18 +3869,28 @@ async function createWorkspace(input, deps = {}) {
|
|
|
2369
3869
|
id: candidate,
|
|
2370
3870
|
display_name: trimmedName,
|
|
2371
3871
|
neon_written: true,
|
|
2372
|
-
registry_written:
|
|
3872
|
+
registry_written: registryWritten(registry),
|
|
3873
|
+
side_effects: [registry]
|
|
2373
3874
|
}
|
|
2374
3875
|
};
|
|
2375
3876
|
}
|
|
2376
3877
|
if (result === "unreachable") {
|
|
2377
|
-
return createOffline(input, trimmedName, now, deps);
|
|
3878
|
+
return createOffline(input, trimmedName, now, deps, reason);
|
|
2378
3879
|
}
|
|
2379
3880
|
if (input.id !== void 0) {
|
|
3881
|
+
const owner = await findWorkspaceForAccountNeon(input.id, input.accountId, deps.neon ?? {});
|
|
3882
|
+
if (owner === null) {
|
|
3883
|
+
return {
|
|
3884
|
+
ok: false,
|
|
3885
|
+
code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
|
|
3886
|
+
message: workspaceOwnedByOtherAccountMessage(input.id)
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
2380
3889
|
return {
|
|
2381
3890
|
ok: false,
|
|
2382
3891
|
code: "duplicate_workspace",
|
|
2383
|
-
message: `workspace "${input.id}" already exists
|
|
3892
|
+
message: `workspace "${input.id}" already exists`,
|
|
3893
|
+
...owner !== "unreachable" ? { owned_by_caller: true } : {}
|
|
2384
3894
|
};
|
|
2385
3895
|
}
|
|
2386
3896
|
}
|
|
@@ -2388,12 +3898,13 @@ async function createWorkspace(input, deps = {}) {
|
|
|
2388
3898
|
}
|
|
2389
3899
|
|
|
2390
3900
|
// spex-console/src/lib/vault-registry/workspace-manage.ts
|
|
2391
|
-
init_vault_workspace_shared();
|
|
2392
|
-
init_registry_file();
|
|
2393
3901
|
var MAX_DISPLAY_NAME_LEN2 = 60;
|
|
2394
3902
|
function notFound(id) {
|
|
2395
3903
|
return { ok: false, code: "workspace_not_found", message: `workspace "${id}" not found` };
|
|
2396
3904
|
}
|
|
3905
|
+
function cloudWriteFailed2(failure) {
|
|
3906
|
+
return { ok: false, code: "cloud_write_failed", message: failure.message };
|
|
3907
|
+
}
|
|
2397
3908
|
async function renameWorkspace(input, deps = {}) {
|
|
2398
3909
|
const trimmedName = input.displayName.trim();
|
|
2399
3910
|
if (trimmedName.length < 1 || trimmedName.length > MAX_DISPLAY_NAME_LEN2) {
|
|
@@ -2413,6 +3924,7 @@ async function renameWorkspace(input, deps = {}) {
|
|
|
2413
3924
|
deps.neon ?? {}
|
|
2414
3925
|
);
|
|
2415
3926
|
if (neonResult === "not_found") return notFound(input.id);
|
|
3927
|
+
if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
|
|
2416
3928
|
registerWorkspace(
|
|
2417
3929
|
{
|
|
2418
3930
|
...existing,
|
|
@@ -2436,6 +3948,7 @@ async function unregisterWorkspace(input, deps = {}) {
|
|
|
2436
3948
|
if (!existing) return notFound(input.id);
|
|
2437
3949
|
const neonResult = await deleteWorkspaceNeon(input.id, input.accountId, deps.neon ?? {});
|
|
2438
3950
|
if (neonResult === "not_found") return notFound(input.id);
|
|
3951
|
+
if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
|
|
2439
3952
|
await repointActiveWorkspaceOnRemoval(input.accountId, input.id, {
|
|
2440
3953
|
registryOpts: deps.registryOpts,
|
|
2441
3954
|
accountPointer: { ...deps.neon, registryOpts: deps.registryOpts }
|
|
@@ -2451,8 +3964,312 @@ async function unregisterWorkspace(input, deps = {}) {
|
|
|
2451
3964
|
};
|
|
2452
3965
|
}
|
|
2453
3966
|
|
|
3967
|
+
// spex-console/src/lib/cli/receipt.ts
|
|
3968
|
+
var RECEIPT_AGREEMENT_FIELDS = [
|
|
3969
|
+
"vault_id",
|
|
3970
|
+
"workspace_id",
|
|
3971
|
+
"account_id",
|
|
3972
|
+
"active_vault_id"
|
|
3973
|
+
];
|
|
3974
|
+
var ALL_FIELDS = [
|
|
3975
|
+
...RECEIPT_AGREEMENT_FIELDS,
|
|
3976
|
+
"display_name",
|
|
3977
|
+
"archived"
|
|
3978
|
+
];
|
|
3979
|
+
function unreadSide(reason) {
|
|
3980
|
+
return {
|
|
3981
|
+
read: false,
|
|
3982
|
+
reason,
|
|
3983
|
+
vault_id: null,
|
|
3984
|
+
workspace_id: null,
|
|
3985
|
+
account_id: null,
|
|
3986
|
+
active_vault_id: null
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
function valueOf(side, field) {
|
|
3990
|
+
const v = side[field];
|
|
3991
|
+
return v === void 0 ? null : v;
|
|
3992
|
+
}
|
|
3993
|
+
function comparable(field, a, b) {
|
|
3994
|
+
return field !== "account_id" || a !== null && b !== null;
|
|
3995
|
+
}
|
|
3996
|
+
function buildReceipt(input) {
|
|
3997
|
+
const local = input.local ?? unreadSide("not read back");
|
|
3998
|
+
const cloud = input.cloud ?? unreadSide("not read back");
|
|
3999
|
+
if (input.failed?.reason) {
|
|
4000
|
+
const target = input.failed.side === "cloud" ? cloud : local;
|
|
4001
|
+
target.reason = input.failed.reason;
|
|
4002
|
+
}
|
|
4003
|
+
const verified = local.read && cloud.read;
|
|
4004
|
+
const expected = input.expected ?? {};
|
|
4005
|
+
const fields = ALL_FIELDS.filter(
|
|
4006
|
+
(f) => RECEIPT_AGREEMENT_FIELDS.includes(f) || Object.hasOwn(expected, f)
|
|
4007
|
+
);
|
|
4008
|
+
const diffs = [];
|
|
4009
|
+
if (verified) {
|
|
4010
|
+
for (const field of fields) {
|
|
4011
|
+
const l = valueOf(local, field);
|
|
4012
|
+
const c = valueOf(cloud, field);
|
|
4013
|
+
const hasExpected = Object.hasOwn(expected, field);
|
|
4014
|
+
const want = hasExpected ? expected[field] ?? null : void 0;
|
|
4015
|
+
const sidesDiffer = comparable(field, l, c) && l !== c;
|
|
4016
|
+
const offTarget = hasExpected && (l !== want || c !== want);
|
|
4017
|
+
if (sidesDiffer || offTarget) {
|
|
4018
|
+
diffs.push({ field, local: l, cloud: c, ...hasExpected ? { expected: want } : {} });
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
const changed = [];
|
|
4023
|
+
for (const side of ["local", "cloud"]) {
|
|
4024
|
+
const before = input.before?.[side];
|
|
4025
|
+
const after = side === "local" ? local : cloud;
|
|
4026
|
+
if (!before?.read || !after.read) continue;
|
|
4027
|
+
for (const field of ALL_FIELDS) {
|
|
4028
|
+
const from = valueOf(before, field);
|
|
4029
|
+
const to = valueOf(after, field);
|
|
4030
|
+
if (from !== to) changed.push({ side, field, from, to });
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
return {
|
|
4034
|
+
local,
|
|
4035
|
+
cloud,
|
|
4036
|
+
verified,
|
|
4037
|
+
agree: verified && diffs.length === 0,
|
|
4038
|
+
diffs,
|
|
4039
|
+
changed,
|
|
4040
|
+
...input.failed ? { failed: input.failed.side } : {},
|
|
4041
|
+
...input.retry ? { retry: input.retry } : {}
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
function withReceipt(result, receipt) {
|
|
4045
|
+
return {
|
|
4046
|
+
...result,
|
|
4047
|
+
message: [result.message, formatReceipt(receipt)].filter(Boolean).join("\n"),
|
|
4048
|
+
payload: { ...result.payload, receipt }
|
|
4049
|
+
};
|
|
4050
|
+
}
|
|
4051
|
+
function shellArg(value) {
|
|
4052
|
+
return /^[\w@%+=:,./\\-]+$/.test(value) ? value : JSON.stringify(value);
|
|
4053
|
+
}
|
|
4054
|
+
function accountFlag(account) {
|
|
4055
|
+
return account ? ` --account ${shellArg(account)}` : "";
|
|
4056
|
+
}
|
|
4057
|
+
function receiptStatus(receipt) {
|
|
4058
|
+
if (receipt.failed) return "failed";
|
|
4059
|
+
if (!receipt.verified) return "unverified";
|
|
4060
|
+
if (!receipt.agree) return "disagree";
|
|
4061
|
+
return "verified";
|
|
4062
|
+
}
|
|
4063
|
+
var LABELS = {
|
|
4064
|
+
vault_id: "vault",
|
|
4065
|
+
workspace_id: "workspace",
|
|
4066
|
+
account_id: "account",
|
|
4067
|
+
active_vault_id: "active vault",
|
|
4068
|
+
display_name: "name",
|
|
4069
|
+
archived: "archived"
|
|
4070
|
+
};
|
|
4071
|
+
function cell(side, field) {
|
|
4072
|
+
if (!side.read) return "(not read)";
|
|
4073
|
+
const v = valueOf(side, field);
|
|
4074
|
+
if (v === null) return "(none)";
|
|
4075
|
+
if (typeof v === "boolean") return v ? "yes" : "no";
|
|
4076
|
+
return v;
|
|
4077
|
+
}
|
|
4078
|
+
function formatReceipt(receipt) {
|
|
4079
|
+
const shown = ALL_FIELDS.filter(
|
|
4080
|
+
(f) => RECEIPT_AGREEMENT_FIELDS.includes(f) || receipt.local[f] !== void 0 || receipt.cloud[f] !== void 0
|
|
4081
|
+
);
|
|
4082
|
+
const rows = [
|
|
4083
|
+
["", "local (this machine)", "cloud (console)"],
|
|
4084
|
+
...shown.map((f) => [
|
|
4085
|
+
LABELS[f],
|
|
4086
|
+
cell(receipt.local, f),
|
|
4087
|
+
cell(receipt.cloud, f)
|
|
4088
|
+
])
|
|
4089
|
+
];
|
|
4090
|
+
const w0 = Math.max(...rows.map((r) => r[0].length));
|
|
4091
|
+
const w1 = Math.max(...rows.map((r) => r[1].length));
|
|
4092
|
+
const lines = [
|
|
4093
|
+
"Read back after the write:",
|
|
4094
|
+
...rows.map(([a, b, c]) => ` ${a.padEnd(w0)} ${b.padEnd(w1)} ${c}`.trimEnd())
|
|
4095
|
+
];
|
|
4096
|
+
const status = receiptStatus(receipt);
|
|
4097
|
+
if (status === "failed") {
|
|
4098
|
+
const side = receipt.failed;
|
|
4099
|
+
const reason = receipt[side].reason;
|
|
4100
|
+
lines.push(
|
|
4101
|
+
`Result: failed -- the ${side} half did not land${reason ? ` (${reason})` : ""}.` + (receipt.retry ? ` Retry: \`${receipt.retry}\`` : "")
|
|
4102
|
+
);
|
|
4103
|
+
} else if (status === "unverified") {
|
|
4104
|
+
const unread = ["local", "cloud"].filter((s) => !receipt[s].read);
|
|
4105
|
+
const why = unread.map((s) => `${s}${receipt[s].reason ? `: ${receipt[s].reason}` : ""}`).join("; ");
|
|
4106
|
+
lines.push(
|
|
4107
|
+
`Result: unverified -- the write was made, but the ${unread.join(" and ")} side was not read back (${why}).`
|
|
4108
|
+
);
|
|
4109
|
+
} else if (status === "disagree") {
|
|
4110
|
+
const fields = receipt.diffs.map((d) => LABELS[d.field]).join(", ");
|
|
4111
|
+
const reads = receipt.cloud_reads && receipt.cloud_reads > 1 ? ` after ${receipt.cloud_reads} reads of the cloud side` : "";
|
|
4112
|
+
const show = (v) => v === void 0 || v === null ? "(none)" : typeof v === "boolean" ? v ? "yes" : "no" : v;
|
|
4113
|
+
lines.push(
|
|
4114
|
+
`Result: the two sides disagree on ${fields}${reads}.` + (receipt.retry ? ` Retry: \`${receipt.retry}\`` : ""),
|
|
4115
|
+
...receipt.diffs.map(
|
|
4116
|
+
(d) => ` ${LABELS[d.field]}: local ${show(d.local)} | cloud ${show(d.cloud)}` + (d.expected !== void 0 ? ` | expected ${show(d.expected)}` : "")
|
|
4117
|
+
)
|
|
4118
|
+
);
|
|
4119
|
+
} else {
|
|
4120
|
+
lines.push("Result: verified -- both sides were read back and agree.");
|
|
4121
|
+
}
|
|
4122
|
+
return lines.join("\n");
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
// spex-console/src/lib/cli/receipt-readback.ts
|
|
4126
|
+
var RECEIPT_CLOUD_READS = 3;
|
|
4127
|
+
var RECEIPT_REREAD_DELAY_MS = 1e3;
|
|
4128
|
+
var realSleep = (ms) => new Promise((resolve5) => {
|
|
4129
|
+
setTimeout(resolve5, ms);
|
|
4130
|
+
});
|
|
4131
|
+
function message(err) {
|
|
4132
|
+
return err instanceof Error ? err.message : String(err);
|
|
4133
|
+
}
|
|
4134
|
+
function readLocalReceiptSide(subject, registryOpts = {}) {
|
|
4135
|
+
try {
|
|
4136
|
+
const registry = loadVaultRegistry(registryOpts);
|
|
4137
|
+
const pointer = accountPointersOf(registry, subject.accountId);
|
|
4138
|
+
const ownerOf = (account) => account && account !== LEGACY_REGISTRY_ACCOUNT_ID ? account : null;
|
|
4139
|
+
if (subject.vaultId) {
|
|
4140
|
+
const row2 = registry.vaults.find((v) => v.id === subject.vaultId);
|
|
4141
|
+
return {
|
|
4142
|
+
read: true,
|
|
4143
|
+
vault_id: row2?.id ?? null,
|
|
4144
|
+
workspace_id: row2?.workspace_id ?? null,
|
|
4145
|
+
account_id: ownerOf(row2?.account_id),
|
|
4146
|
+
active_vault_id: pointer.active_vault_id ?? null,
|
|
4147
|
+
display_name: row2?.display_name ?? null,
|
|
4148
|
+
archived: row2 ? row2.archived ?? false : null
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
const row = subject.workspaceId ? registry.workspaces.find((w) => w.id === subject.workspaceId) : void 0;
|
|
4152
|
+
return {
|
|
4153
|
+
read: true,
|
|
4154
|
+
vault_id: null,
|
|
4155
|
+
workspace_id: row?.id ?? null,
|
|
4156
|
+
account_id: ownerOf(row?.account_id),
|
|
4157
|
+
active_vault_id: pointer.active_vault_id ?? null,
|
|
4158
|
+
...subject.workspaceId ? {
|
|
4159
|
+
display_name: row?.display_name ?? null,
|
|
4160
|
+
archived: row ? row.archived ?? false : null
|
|
4161
|
+
} : {}
|
|
4162
|
+
};
|
|
4163
|
+
} catch (err) {
|
|
4164
|
+
return unreadSide(`registry.json could not be read: ${message(err)}`);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
async function readList(http, path4, key) {
|
|
4168
|
+
const res = await cloudHttpRequest(http, "GET", path4);
|
|
4169
|
+
const rows = res.json?.[key];
|
|
4170
|
+
if (!res.ok || !Array.isArray(rows)) {
|
|
4171
|
+
return { reason: `GET ${path4.split("?")[0]} answered HTTP ${res.status}` };
|
|
4172
|
+
}
|
|
4173
|
+
return { rows: rows.filter((r) => Boolean(r) && typeof r === "object") };
|
|
4174
|
+
}
|
|
4175
|
+
var str = (v) => typeof v === "string" && v ? v : null;
|
|
4176
|
+
async function readCloudReceiptSide(subject, http) {
|
|
4177
|
+
if (!http) return unreadSide("no cloud transport on this call");
|
|
4178
|
+
const active = await cloudHttpRequest(http, "GET", "/api/vaults/active");
|
|
4179
|
+
if (!active.ok || !active.json || typeof active.json !== "object") {
|
|
4180
|
+
return unreadSide(`GET /api/vaults/active answered HTTP ${active.status}`);
|
|
4181
|
+
}
|
|
4182
|
+
const activeVaultId = str(active.json.vault_id);
|
|
4183
|
+
if (subject.vaultId) {
|
|
4184
|
+
const list2 = await readList(http, "/api/vaults?include_archived=true", "vaults");
|
|
4185
|
+
if ("reason" in list2) return unreadSide(list2.reason);
|
|
4186
|
+
const row2 = list2.rows.find((r) => r.id === subject.vaultId);
|
|
4187
|
+
return {
|
|
4188
|
+
read: true,
|
|
4189
|
+
vault_id: row2 ? subject.vaultId : null,
|
|
4190
|
+
workspace_id: str(row2?.workspace_id),
|
|
4191
|
+
account_id: row2 && row2.granted !== true ? subject.accountId : null,
|
|
4192
|
+
active_vault_id: activeVaultId,
|
|
4193
|
+
display_name: str(row2?.display_name),
|
|
4194
|
+
archived: row2 ? row2.archived === true : null
|
|
4195
|
+
};
|
|
4196
|
+
}
|
|
4197
|
+
if (!subject.workspaceId) {
|
|
4198
|
+
return {
|
|
4199
|
+
read: true,
|
|
4200
|
+
vault_id: null,
|
|
4201
|
+
workspace_id: null,
|
|
4202
|
+
account_id: null,
|
|
4203
|
+
active_vault_id: activeVaultId
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
const list = await readList(http, "/api/workspaces?include_archived=true", "workspaces");
|
|
4207
|
+
if ("reason" in list) return unreadSide(list.reason);
|
|
4208
|
+
const row = list.rows.find((r) => r.id === subject.workspaceId);
|
|
4209
|
+
return {
|
|
4210
|
+
read: true,
|
|
4211
|
+
vault_id: null,
|
|
4212
|
+
workspace_id: row ? subject.workspaceId : null,
|
|
4213
|
+
account_id: row ? subject.accountId : null,
|
|
4214
|
+
active_vault_id: activeVaultId,
|
|
4215
|
+
display_name: str(row?.display_name),
|
|
4216
|
+
archived: row ? row.archived === true : null
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
async function readSettledReceipt(subject, deps, build) {
|
|
4220
|
+
const sides = await readReceiptSides(subject, deps);
|
|
4221
|
+
let receipt = build(sides);
|
|
4222
|
+
const maxReads = Math.max(1, deps.cloudReads ?? RECEIPT_CLOUD_READS);
|
|
4223
|
+
const sleep2 = deps.sleep ?? realSleep;
|
|
4224
|
+
let reads = 1;
|
|
4225
|
+
while (!receipt.failed && receipt.verified && !receipt.agree && reads < maxReads) {
|
|
4226
|
+
await sleep2(RECEIPT_REREAD_DELAY_MS);
|
|
4227
|
+
const cloud = await readCloudReceiptSide(subject, deps.http);
|
|
4228
|
+
reads += 1;
|
|
4229
|
+
receipt = build({ local: sides.local, cloud });
|
|
4230
|
+
}
|
|
4231
|
+
return { ...receipt, cloud_reads: reads };
|
|
4232
|
+
}
|
|
4233
|
+
function receiptDisagrees(receipt) {
|
|
4234
|
+
return !receipt.failed && receipt.verified && !receipt.agree;
|
|
4235
|
+
}
|
|
4236
|
+
async function finishWithReceipt(result, deps, subject, opts) {
|
|
4237
|
+
const failed = opts.failedReason !== void 0;
|
|
4238
|
+
const receipt = await readSettledReceipt(
|
|
4239
|
+
subject,
|
|
4240
|
+
deps,
|
|
4241
|
+
(sides) => buildReceipt({
|
|
4242
|
+
...sides,
|
|
4243
|
+
...opts.before ? { before: opts.before } : {},
|
|
4244
|
+
...opts.expected && !failed ? { expected: opts.expected } : {},
|
|
4245
|
+
...failed ? { failed: { side: "cloud", reason: opts.failedReason } } : {},
|
|
4246
|
+
...failed && opts.retry ? { retry: opts.retry } : {}
|
|
4247
|
+
})
|
|
4248
|
+
);
|
|
4249
|
+
if (failed) {
|
|
4250
|
+
return withReceipt({ ...result, code: EXIT.CLOUD_WRITE_FAILED, isError: true }, receipt);
|
|
4251
|
+
}
|
|
4252
|
+
if (receiptDisagrees(receipt)) {
|
|
4253
|
+
if (opts.retry) receipt.retry = opts.retry;
|
|
4254
|
+
return withReceipt(
|
|
4255
|
+
{
|
|
4256
|
+
...result,
|
|
4257
|
+
code: EXIT.RECEIPT_DISAGREE,
|
|
4258
|
+
isError: true,
|
|
4259
|
+
payload: { ...result.payload, ok: false, error: "receipt_disagree" }
|
|
4260
|
+
},
|
|
4261
|
+
receipt
|
|
4262
|
+
);
|
|
4263
|
+
}
|
|
4264
|
+
return withReceipt(result, receipt);
|
|
4265
|
+
}
|
|
4266
|
+
async function readReceiptSides(subject, deps) {
|
|
4267
|
+
const local = readLocalReceiptSide(subject, deps.registryOpts);
|
|
4268
|
+
const cloud = await readCloudReceiptSide(subject, deps.http);
|
|
4269
|
+
return { local, cloud };
|
|
4270
|
+
}
|
|
4271
|
+
|
|
2454
4272
|
// spex-console/src/lib/cli/workspace.ts
|
|
2455
|
-
init_vault_workspace_shared();
|
|
2456
4273
|
function parseWorkspaceArgs(argv) {
|
|
2457
4274
|
const subRaw = argv[0];
|
|
2458
4275
|
if (subRaw === "--help" || subRaw === "-h" || subRaw === void 0) {
|
|
@@ -2564,8 +4381,78 @@ function parseWorkspaceArgs(argv) {
|
|
|
2564
4381
|
function accountPointerDeps(deps) {
|
|
2565
4382
|
return { registryOpts: deps.registryOpts, ...deps.neon ?? {} };
|
|
2566
4383
|
}
|
|
4384
|
+
function scopedToActiveVault(deps) {
|
|
4385
|
+
const acct = accountId(deps);
|
|
4386
|
+
let vaultId = null;
|
|
4387
|
+
try {
|
|
4388
|
+
vaultId = accountPointersOf(loadVaultRegistry(deps.registryOpts), acct).active_vault_id ?? null;
|
|
4389
|
+
} catch {
|
|
4390
|
+
}
|
|
4391
|
+
return { ...deps, neon: withCloudScope(deps.neon, acct, vaultId) };
|
|
4392
|
+
}
|
|
4393
|
+
function readWorkspaceSides(deps, subject) {
|
|
4394
|
+
return readReceiptSides(subject, { registryOpts: deps.registryOpts, http: deps.neon?.http });
|
|
4395
|
+
}
|
|
4396
|
+
function finishWorkspaceWrite(result, deps, subject, opts) {
|
|
4397
|
+
return finishWithReceipt(
|
|
4398
|
+
result,
|
|
4399
|
+
{ registryOpts: deps.registryOpts, http: deps.neon?.http, sleep: deps.receiptSleep },
|
|
4400
|
+
subject,
|
|
4401
|
+
opts
|
|
4402
|
+
);
|
|
4403
|
+
}
|
|
4404
|
+
async function manageWithReceipt(verb, workspaceId, effectiveAccountId, deps, retry, write, success, expected) {
|
|
4405
|
+
deps = scopedToActiveVault(deps);
|
|
4406
|
+
const subject = { accountId: effectiveAccountId, workspaceId };
|
|
4407
|
+
const before = await readWorkspaceSides(deps, subject);
|
|
4408
|
+
const result = await write(deps);
|
|
4409
|
+
if (!result.ok) {
|
|
4410
|
+
if (result.code === "cloud_write_failed") {
|
|
4411
|
+
return finishWorkspaceWrite(
|
|
4412
|
+
{
|
|
4413
|
+
code: EXIT.CLOUD_WRITE_FAILED,
|
|
4414
|
+
message: `hs workspace ${verb}: the cloud half failed -- ${result.message} Retry: \`${retry}\``,
|
|
4415
|
+
payload: { ok: false, error: result.code, message: result.message, retry },
|
|
4416
|
+
isError: true
|
|
4417
|
+
},
|
|
4418
|
+
deps,
|
|
4419
|
+
subject,
|
|
4420
|
+
{ before, failedReason: result.message, retry }
|
|
4421
|
+
);
|
|
4422
|
+
}
|
|
4423
|
+
return {
|
|
4424
|
+
code: statusForWorkspaceManageError(result.code),
|
|
4425
|
+
message: `hs workspace ${verb}: ${result.message}`,
|
|
4426
|
+
payload: { ok: false, error: result.code, message: result.message },
|
|
4427
|
+
isError: true
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
const selection = await activeWorkspaceSelection(deps, effectiveAccountId);
|
|
4431
|
+
const done = success(result.workspace, selection);
|
|
4432
|
+
return finishWorkspaceWrite(
|
|
4433
|
+
{ code: EXIT.OK, message: done.message, payload: done.payload, isError: false },
|
|
4434
|
+
deps,
|
|
4435
|
+
subject,
|
|
4436
|
+
{ before, expected, retry }
|
|
4437
|
+
);
|
|
4438
|
+
}
|
|
2567
4439
|
function accountId(deps) {
|
|
2568
|
-
|
|
4440
|
+
if (deps.accountId) return deps.accountId;
|
|
4441
|
+
const strict = resolveCloudAccountIdOrRefuse(deps.accountIdOpts);
|
|
4442
|
+
if (strict.ok) return strict.accountId;
|
|
4443
|
+
throw new Error(`hs workspace: ${strict.message}`);
|
|
4444
|
+
}
|
|
4445
|
+
function strictAccountId(deps) {
|
|
4446
|
+
if (deps.accountId) return { ok: true, accountId: deps.accountId };
|
|
4447
|
+
return resolveCloudAccountIdOrRefuse(deps.accountIdOpts);
|
|
4448
|
+
}
|
|
4449
|
+
function signInRequiredResult(verb, message2) {
|
|
4450
|
+
return {
|
|
4451
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
4452
|
+
message: `hs workspace ${verb}: ${message2}`,
|
|
4453
|
+
payload: { ok: false, error: "sign_in_required", message: message2 },
|
|
4454
|
+
isError: true
|
|
4455
|
+
};
|
|
2569
4456
|
}
|
|
2570
4457
|
async function resolveEffectiveAccountId(deps, requestedAccount, verb) {
|
|
2571
4458
|
const scope = await resolveScopedAccountId(accountId(deps), requestedAccount);
|
|
@@ -2604,7 +4491,9 @@ async function runList(args, deps) {
|
|
|
2604
4491
|
isError: false
|
|
2605
4492
|
};
|
|
2606
4493
|
}
|
|
2607
|
-
const lines = workspaces2.length ? workspaces2.map(
|
|
4494
|
+
const lines = workspaces2.length ? workspaces2.map(
|
|
4495
|
+
(w) => `${w.active ? "* " : " "}${w.id} ${w.display_name}` + (w.origin === "registry-only" ? " (this machine only)" : "")
|
|
4496
|
+
).join("\n") : ' (no workspaces registered -- run `hs workspace create --id <slug> --name "..."`)';
|
|
2608
4497
|
return {
|
|
2609
4498
|
code: EXIT.OK,
|
|
2610
4499
|
message: `${workspaces2.length} registered workspace(s):
|
|
@@ -2613,13 +4502,58 @@ ${lines}`,
|
|
|
2613
4502
|
isError: false
|
|
2614
4503
|
};
|
|
2615
4504
|
}
|
|
4505
|
+
function describeNeonWriteFailure(reason) {
|
|
4506
|
+
switch (reason) {
|
|
4507
|
+
case "not-configured":
|
|
4508
|
+
return "Neon is not configured on this machine";
|
|
4509
|
+
case "unauthorized":
|
|
4510
|
+
return "the cloud write was unauthorized -- run `hs login`";
|
|
4511
|
+
case "network-error":
|
|
4512
|
+
return "the cloud console could not be reached";
|
|
4513
|
+
case "server-error":
|
|
4514
|
+
return "the cloud console rejected the write";
|
|
4515
|
+
case "owned-by-other-account":
|
|
4516
|
+
return "that id belongs to another account";
|
|
4517
|
+
case "workspace-owned-by-other-account":
|
|
4518
|
+
return "that workspace belongs to another account";
|
|
4519
|
+
default:
|
|
4520
|
+
return "Neon unreachable";
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
2616
4523
|
async function runCreate(args, deps) {
|
|
4524
|
+
const strict = strictAccountId(deps);
|
|
4525
|
+
if (!strict.ok) return signInRequiredResult("create", strict.message);
|
|
4526
|
+
deps = scopedToActiveVault(deps);
|
|
4527
|
+
const subject = { accountId: strict.accountId, workspaceId: args.id };
|
|
4528
|
+
const before = await readWorkspaceSides(deps, subject);
|
|
4529
|
+
const retry = `hs workspace create --id ${shellArg(args.id)} --name ${shellArg(args.name)}`;
|
|
4530
|
+
const holds = (s) => s.read && s.workspace_id === args.id && s.display_name === args.name && s.archived !== true;
|
|
4531
|
+
if (deps.neon?.http && holds(before.local) && holds(before.cloud)) {
|
|
4532
|
+
return finishWorkspaceWrite(
|
|
4533
|
+
{
|
|
4534
|
+
code: EXIT.OK,
|
|
4535
|
+
message: `Workspace "${args.id}" (${args.name}) already exists on both sides -- nothing to change.`,
|
|
4536
|
+
payload: {
|
|
4537
|
+
ok: true,
|
|
4538
|
+
id: args.id,
|
|
4539
|
+
display_name: args.name,
|
|
4540
|
+
unchanged: true,
|
|
4541
|
+
neon_written: false,
|
|
4542
|
+
registry_written: false
|
|
4543
|
+
},
|
|
4544
|
+
isError: false
|
|
4545
|
+
},
|
|
4546
|
+
deps,
|
|
4547
|
+
subject,
|
|
4548
|
+
{ before, expected: { workspace_id: args.id, display_name: args.name }, retry }
|
|
4549
|
+
);
|
|
4550
|
+
}
|
|
2617
4551
|
const result = await createWorkspace(
|
|
2618
|
-
{ accountId: accountId
|
|
4552
|
+
{ accountId: strict.accountId, id: args.id, displayName: args.name },
|
|
2619
4553
|
{ registryOpts: deps.registryOpts, neon: deps.neon, now: deps.now, allowIdUpsert: true }
|
|
2620
4554
|
);
|
|
2621
4555
|
if (!result.ok) {
|
|
2622
|
-
const code = result.code === "duplicate_workspace" ? EXIT.CONFLICT : EXIT.INVALID_ARG;
|
|
4556
|
+
const code = result.code === "duplicate_workspace" || result.code === "workspace_owned_by_other_account" ? EXIT.CONFLICT : EXIT.INVALID_ARG;
|
|
2623
4557
|
return {
|
|
2624
4558
|
code,
|
|
2625
4559
|
message: `hs workspace create: ${result.message}`,
|
|
@@ -2627,15 +4561,36 @@ async function runCreate(args, deps) {
|
|
|
2627
4561
|
isError: true
|
|
2628
4562
|
};
|
|
2629
4563
|
}
|
|
2630
|
-
const { id, display_name, neon_written, registry_written } = result.workspace;
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
4564
|
+
const { id, display_name, neon_written, neon_write_reason, registry_written } = result.workspace;
|
|
4565
|
+
const cloudFailed = !neon_written && deps.neon?.http !== void 0;
|
|
4566
|
+
const failureText = describeNeonWriteFailure(neon_write_reason);
|
|
4567
|
+
return finishWorkspaceWrite(
|
|
4568
|
+
{
|
|
4569
|
+
code: EXIT.OK,
|
|
4570
|
+
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}).`),
|
|
4571
|
+
payload: {
|
|
4572
|
+
ok: !cloudFailed,
|
|
4573
|
+
...cloudFailed ? { error: "cloud_write_failed", retry } : {},
|
|
4574
|
+
id,
|
|
4575
|
+
display_name,
|
|
4576
|
+
neon_written,
|
|
4577
|
+
...neon_write_reason ? { neon_write_reason } : {},
|
|
4578
|
+
registry_written
|
|
4579
|
+
},
|
|
4580
|
+
isError: false
|
|
4581
|
+
},
|
|
4582
|
+
deps,
|
|
4583
|
+
{ ...subject, workspaceId: id },
|
|
4584
|
+
{
|
|
4585
|
+
before,
|
|
4586
|
+
expected: { workspace_id: id, display_name },
|
|
4587
|
+
...cloudFailed ? { failedReason: neon_write_reason ?? "server-error" } : {},
|
|
4588
|
+
retry
|
|
4589
|
+
}
|
|
4590
|
+
);
|
|
2637
4591
|
}
|
|
2638
4592
|
async function runSwitch(args, deps) {
|
|
4593
|
+
deps = scopedToActiveVault(deps);
|
|
2639
4594
|
try {
|
|
2640
4595
|
await setActiveWorkspace(accountId(deps), args.id, accountPointerDeps(deps));
|
|
2641
4596
|
} catch (err) {
|
|
@@ -2647,6 +4602,21 @@ async function runSwitch(args, deps) {
|
|
|
2647
4602
|
isError: true
|
|
2648
4603
|
};
|
|
2649
4604
|
}
|
|
4605
|
+
if (err instanceof CloudPointerWriteError) {
|
|
4606
|
+
const message2 = `hs workspace switch: active workspace pointer not updated in the cloud (${err.message}) -- re-run \`hs workspace switch ${args.id}\` to retry.`;
|
|
4607
|
+
return {
|
|
4608
|
+
code: EXIT.CLOUD_WRITE_FAILED,
|
|
4609
|
+
message: message2,
|
|
4610
|
+
payload: {
|
|
4611
|
+
ok: false,
|
|
4612
|
+
error: "cloud_pointer_failed",
|
|
4613
|
+
workspace_id: args.id,
|
|
4614
|
+
http_status: err.status,
|
|
4615
|
+
message: message2
|
|
4616
|
+
},
|
|
4617
|
+
isError: true
|
|
4618
|
+
};
|
|
4619
|
+
}
|
|
2650
4620
|
throw err;
|
|
2651
4621
|
}
|
|
2652
4622
|
return {
|
|
@@ -2661,36 +4631,35 @@ async function activeWorkspaceSelection(deps, effectiveAccountId = accountId(dep
|
|
|
2661
4631
|
return { line: `Active workspace: ${workspaceId ?? "(none)"}`, workspaceId };
|
|
2662
4632
|
}
|
|
2663
4633
|
function statusForWorkspaceManageError(code) {
|
|
2664
|
-
|
|
4634
|
+
if (code === "workspace_not_found" || code === "vault_not_found") return EXIT.NOT_FOUND;
|
|
4635
|
+
if (code === "cloud_write_failed") return EXIT.CLOUD_WRITE_FAILED;
|
|
4636
|
+
return EXIT.INVALID_ARG;
|
|
2665
4637
|
}
|
|
2666
4638
|
async function runRename(args, deps) {
|
|
2667
4639
|
const scoped = await resolveEffectiveAccountId(deps, args.account, "rename");
|
|
2668
4640
|
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}".
|
|
4641
|
+
return manageWithReceipt(
|
|
4642
|
+
"rename",
|
|
4643
|
+
args.id,
|
|
4644
|
+
scoped.accountId,
|
|
4645
|
+
deps,
|
|
4646
|
+
`hs workspace rename ${shellArg(args.id)} --name ${shellArg(args.name)}${accountFlag(args.account)}`,
|
|
4647
|
+
(d) => renameWorkspace(
|
|
4648
|
+
{ accountId: scoped.accountId, id: args.id, displayName: args.name },
|
|
4649
|
+
{ registryOpts: d.registryOpts, neon: d.neon }
|
|
4650
|
+
),
|
|
4651
|
+
(workspace, selection) => ({
|
|
4652
|
+
message: `Renamed workspace "${workspace.id}" to "${workspace.display_name}".
|
|
2685
4653
|
${selection.line}`,
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
4654
|
+
payload: {
|
|
4655
|
+
ok: true,
|
|
4656
|
+
id: workspace.id,
|
|
4657
|
+
display_name: workspace.display_name,
|
|
4658
|
+
active_workspace_id: selection.workspaceId
|
|
4659
|
+
}
|
|
4660
|
+
}),
|
|
4661
|
+
{ display_name: args.name.trim() }
|
|
4662
|
+
);
|
|
2694
4663
|
}
|
|
2695
4664
|
async function confirmRemoval(deps, displayName, verb, yes) {
|
|
2696
4665
|
if (yes) return true;
|
|
@@ -2714,6 +4683,7 @@ async function runUnregister(args, deps) {
|
|
|
2714
4683
|
const displayName = existing?.display_name ?? args.id;
|
|
2715
4684
|
const proceed = await confirmRemoval(deps, displayName, "unregister", args.yes);
|
|
2716
4685
|
if (!proceed) return cancelledResult("unregister");
|
|
4686
|
+
deps = scopedToActiveVault(deps);
|
|
2717
4687
|
const result = await unregisterWorkspace(
|
|
2718
4688
|
{ accountId: scoped.accountId, id: args.id },
|
|
2719
4689
|
{ registryOpts: deps.registryOpts, neon: deps.neon }
|
|
@@ -2747,62 +4717,59 @@ async function runArchive(args, deps) {
|
|
|
2747
4717
|
const displayName = existing?.display_name ?? args.id;
|
|
2748
4718
|
const proceed = await confirmRemoval(deps, displayName, "archive", args.yes);
|
|
2749
4719
|
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}
|
|
4720
|
+
return manageWithReceipt(
|
|
4721
|
+
"archive",
|
|
4722
|
+
args.id,
|
|
4723
|
+
scoped.accountId,
|
|
4724
|
+
deps,
|
|
4725
|
+
`hs workspace archive ${shellArg(args.id)} --yes${accountFlag(args.account)}`,
|
|
4726
|
+
(d) => archiveWorkspace(
|
|
4727
|
+
{ accountId: scoped.accountId, id: args.id },
|
|
4728
|
+
{ registryOpts: d.registryOpts, neon: d.neon }
|
|
4729
|
+
),
|
|
4730
|
+
(workspace, selection) => ({
|
|
4731
|
+
message: `Archived workspace "${workspace.id}". ${CLOSE_NOT_DESTROY_STATEMENT}
|
|
2766
4732
|
${selection.line}`,
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
4733
|
+
payload: {
|
|
4734
|
+
ok: true,
|
|
4735
|
+
id: workspace.id,
|
|
4736
|
+
archived: workspace.archived,
|
|
4737
|
+
active_workspace_id: selection.workspaceId
|
|
4738
|
+
}
|
|
4739
|
+
}),
|
|
4740
|
+
{ archived: true }
|
|
4741
|
+
);
|
|
2775
4742
|
}
|
|
2776
4743
|
async function runRestore(args, deps) {
|
|
2777
4744
|
const scoped = await resolveEffectiveAccountId(deps, args.account, "restore");
|
|
2778
4745
|
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}".
|
|
4746
|
+
return manageWithReceipt(
|
|
4747
|
+
"restore",
|
|
4748
|
+
args.id,
|
|
4749
|
+
scoped.accountId,
|
|
4750
|
+
deps,
|
|
4751
|
+
`hs workspace restore ${shellArg(args.id)}${accountFlag(args.account)}`,
|
|
4752
|
+
(d) => restoreWorkspace(
|
|
4753
|
+
{ accountId: scoped.accountId, id: args.id },
|
|
4754
|
+
{ registryOpts: d.registryOpts, neon: d.neon }
|
|
4755
|
+
),
|
|
4756
|
+
(workspace, selection) => ({
|
|
4757
|
+
message: `Restored workspace "${workspace.id}".
|
|
2795
4758
|
${selection.line}`,
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
4759
|
+
payload: {
|
|
4760
|
+
ok: true,
|
|
4761
|
+
id: workspace.id,
|
|
4762
|
+
archived: workspace.archived,
|
|
4763
|
+
active_workspace_id: selection.workspaceId
|
|
4764
|
+
}
|
|
4765
|
+
}),
|
|
4766
|
+
{ archived: false }
|
|
4767
|
+
);
|
|
2804
4768
|
}
|
|
2805
4769
|
async function runWorkspace(args, deps) {
|
|
4770
|
+
const strict = strictAccountId(deps);
|
|
4771
|
+
if (!strict.ok) return signInRequiredResult(args.sub, strict.message);
|
|
4772
|
+
deps = { ...deps, accountId: strict.accountId };
|
|
2806
4773
|
if (args.sub === "list") return runList(args, deps);
|
|
2807
4774
|
if (args.sub === "create") return runCreate(args, deps);
|
|
2808
4775
|
if (args.sub === "switch") return runSwitch(args, deps);
|
|
@@ -2811,7 +4778,7 @@ async function runWorkspace(args, deps) {
|
|
|
2811
4778
|
if (args.sub === "archive") return runArchive(args, deps);
|
|
2812
4779
|
return runRestore(args, deps);
|
|
2813
4780
|
}
|
|
2814
|
-
var
|
|
4781
|
+
var HELP3 = [
|
|
2815
4782
|
"Usage:",
|
|
2816
4783
|
" hs workspace list [--json] List registered workspaces, mark active",
|
|
2817
4784
|
' hs workspace create --id <slug> --name "<name>" Create a workspace (Neon + registry.json)',
|
|
@@ -2841,24 +4808,64 @@ var HELP2 = [
|
|
|
2841
4808
|
" --account <id> Act on a DIFFERENT account's workspace instead of your own",
|
|
2842
4809
|
" (list/rename/unregister/archive/restore only) -- builder/admin-gated",
|
|
2843
4810
|
" server-side; refused for a non-builder caller (exit 15).",
|
|
4811
|
+
" --as <email|id> Confirm the SIGNED-IN account this write is made as (create/rename/",
|
|
4812
|
+
" unregister/archive/restore). Must match the account this machine is",
|
|
4813
|
+
" signed in as, or the command refuses. Not the same as --account:",
|
|
4814
|
+
" --account picks the TARGET account, --as confirms who is acting.",
|
|
4815
|
+
" --confirm-account Confirm whichever account is signed in, without naming it (--as is safer).",
|
|
4816
|
+
" A terminal is asked instead; with no terminal, a write verb needs --as or",
|
|
4817
|
+
" --confirm-account (exit 17 otherwise). A machine whose config.yaml",
|
|
4818
|
+
" account_id disagrees with its sign-in is always refused.",
|
|
2844
4819
|
" --json Emit a machine-readable JSON result.",
|
|
2845
4820
|
" --help, -h Show this help.",
|
|
2846
4821
|
"",
|
|
2847
|
-
"
|
|
4822
|
+
"A workspace id that belongs to ANOTHER account is never overwritten: create refuses (exit 9).",
|
|
4823
|
+
"",
|
|
4824
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 9 conflict \xB7 10 not-found \xB7 11 bad-arg \xB7 15 forbidden (--account) \xB7",
|
|
4825
|
+
"12 console unreachable (account check) \xB7 16 sign-in-required (create -- run `hs login`) \xB7",
|
|
4826
|
+
"17 account not confirmed (--as / --confirm-account)"
|
|
2848
4827
|
].join("\n");
|
|
2849
4828
|
|
|
4829
|
+
// spex-console/bin/run-entry.ts
|
|
4830
|
+
import process2 from "node:process";
|
|
4831
|
+
var BinExit = class extends Error {
|
|
4832
|
+
code;
|
|
4833
|
+
constructor(code) {
|
|
4834
|
+
super(`exit ${code}`);
|
|
4835
|
+
this.name = "BinExit";
|
|
4836
|
+
this.code = code;
|
|
4837
|
+
}
|
|
4838
|
+
};
|
|
4839
|
+
function runEntry(name, main2, options = {}) {
|
|
4840
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
|
|
4841
|
+
void (async () => {
|
|
4842
|
+
try {
|
|
4843
|
+
await main2();
|
|
4844
|
+
} catch (err) {
|
|
4845
|
+
if (err instanceof BinExit) {
|
|
4846
|
+
process2.exitCode = err.code;
|
|
4847
|
+
return;
|
|
4848
|
+
}
|
|
4849
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
4850
|
+
process2.stderr.write(`${name}: ${unhandledLabel}: ${message2}
|
|
4851
|
+
`);
|
|
4852
|
+
process2.exitCode = fallbackCode;
|
|
4853
|
+
}
|
|
4854
|
+
})();
|
|
4855
|
+
}
|
|
4856
|
+
|
|
2850
4857
|
// spex-console/bin/hs-workspace.ts
|
|
2851
4858
|
function readFile(p) {
|
|
2852
4859
|
try {
|
|
2853
|
-
return
|
|
4860
|
+
return readFileSync9(p, "utf8");
|
|
2854
4861
|
} catch {
|
|
2855
4862
|
return null;
|
|
2856
4863
|
}
|
|
2857
4864
|
}
|
|
2858
|
-
var INTERACTIVE =
|
|
4865
|
+
var INTERACTIVE = process3.stdin.isTTY === true;
|
|
2859
4866
|
async function confirm(question) {
|
|
2860
4867
|
if (!INTERACTIVE) return false;
|
|
2861
|
-
const rl =
|
|
4868
|
+
const rl = createInterface2({ input: process3.stdin, output: process3.stdout });
|
|
2862
4869
|
try {
|
|
2863
4870
|
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
2864
4871
|
return answer === "y" || answer === "yes" || answer === "proceed";
|
|
@@ -2867,32 +4874,51 @@ async function confirm(question) {
|
|
|
2867
4874
|
}
|
|
2868
4875
|
}
|
|
2869
4876
|
async function main() {
|
|
2870
|
-
const
|
|
4877
|
+
const identityArgv = extractWriteIdentityFlags(process3.argv.slice(3));
|
|
4878
|
+
if (identityArgv.error) {
|
|
4879
|
+
process3.stderr.write(`hs workspace: ${identityArgv.error}
|
|
4880
|
+
`);
|
|
4881
|
+
process3.exitCode = EXIT.INVALID_ARG;
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4884
|
+
const parsed = parseWorkspaceArgs(identityArgv.rest);
|
|
2871
4885
|
if (parsed.help) {
|
|
2872
|
-
|
|
4886
|
+
process3.stdout.write(`${HELP3}
|
|
2873
4887
|
`);
|
|
2874
|
-
|
|
4888
|
+
process3.exitCode = EXIT.OK;
|
|
4889
|
+
return;
|
|
2875
4890
|
}
|
|
2876
4891
|
if (parsed.error) {
|
|
2877
|
-
|
|
4892
|
+
process3.stderr.write(`hs workspace: ${parsed.error.message}
|
|
2878
4893
|
`);
|
|
2879
|
-
|
|
4894
|
+
process3.exitCode = parsed.error.code;
|
|
4895
|
+
return;
|
|
2880
4896
|
}
|
|
2881
|
-
const vaultRoot =
|
|
4897
|
+
const vaultRoot = process3.env.HS_VAULT_ROOT ? path3.resolve(process3.env.HS_VAULT_ROOT) : process3.cwd();
|
|
2882
4898
|
const consoleUrl = resolveSpexConsoleUrl(vaultRoot, readFile);
|
|
4899
|
+
const gate = await gateWriteVerb({
|
|
4900
|
+
namespace: "hs workspace",
|
|
4901
|
+
sub: parsed.args.sub,
|
|
4902
|
+
writeVerbs: WORKSPACE_WRITE_VERBS,
|
|
4903
|
+
flags: identityArgv.flags,
|
|
4904
|
+
io: defaultIdentityGateIo({ consoleUrl, platformRoot: path3.resolve(process3.cwd(), "..") })
|
|
4905
|
+
});
|
|
4906
|
+
if (gate && !gate.ok) {
|
|
4907
|
+
process3.stderr.write(
|
|
4908
|
+
`${parsed.args.json ? JSON.stringify(gate.result.payload) : gate.result.message}
|
|
4909
|
+
`
|
|
4910
|
+
);
|
|
4911
|
+
process3.exitCode = gate.result.code;
|
|
4912
|
+
return;
|
|
4913
|
+
}
|
|
2883
4914
|
const result = await runWorkspace(parsed.args, {
|
|
2884
4915
|
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
2885
4916
|
confirm,
|
|
2886
4917
|
neon: { http: { fetchImpl: fetch, consoleUrl, token: readPairedToken() } }
|
|
2887
4918
|
});
|
|
2888
|
-
const sink = result.isError ?
|
|
4919
|
+
const sink = result.isError ? process3.stderr : process3.stdout;
|
|
2889
4920
|
sink.write(`${parsed.args.json ? JSON.stringify(result.payload) : result.message}
|
|
2890
4921
|
`);
|
|
2891
|
-
|
|
4922
|
+
process3.exitCode = result.code;
|
|
2892
4923
|
}
|
|
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
|
-
});
|
|
4924
|
+
runEntry("hs workspace", main, { fallbackCode: EXIT.WRITE_FAILED });
|