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
package/scripts/lib/dev-mode.mjs
CHANGED
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
* (~/.heyspex/dev-mode.json) → cloud admin setting (account_settings.dev_mode,
|
|
20
20
|
* injected by the caller — this module never talks to Neon) → default prod.
|
|
21
21
|
*
|
|
22
|
+
* ONE HOME ([[HS-R0YM-WW9R-TASK-publish-the-routing-fact-table-test]], routing
|
|
23
|
+
* fact G in `spex-console/src/lib/cli/routing-facts.ts`): the cloud
|
|
24
|
+
* `account_settings.dev_mode` row is the home of the mode. `dev-mode.json` is a
|
|
25
|
+
* machine-local CACHE of it (owner decision: kept as a cache, not deleted),
|
|
26
|
+
* validated by the `hs doctor` `mode-override` row, which names the file's
|
|
27
|
+
* writer and date and fails when it disagrees with a known cloud setting.
|
|
28
|
+
* Both implementations are held to the one precedence table in
|
|
29
|
+
* `./dev-mode.fixtures.mjs`.
|
|
30
|
+
*
|
|
22
31
|
* MACHINE-LOCAL RECORD — CROSS-COMPONENT CONTRACT (the Windows Companion
|
|
23
32
|
* reads the same file; keep the shape exactly this):
|
|
24
33
|
* ~/.heyspex/dev-mode.json
|
|
@@ -28,6 +37,13 @@
|
|
|
28
37
|
* Env overrides:
|
|
29
38
|
* HEYSPEX_DEV_MODE = prod | dev | local | cloud (local/cloud imply dev)
|
|
30
39
|
* HEYSPEX_DEV_RELIANCE = local | cloud (refines HEYSPEX_DEV_MODE=dev)
|
|
40
|
+
* `HEYSPEX_DEV_MODE=dev` with no reliance still resolves local, but the
|
|
41
|
+
* envelope reports `sources.reliance: "assumed-local"` — never silently.
|
|
42
|
+
*
|
|
43
|
+
* Any active switch (env or file) is reported as ONE structured object by
|
|
44
|
+
* `modeOverrideOf(resolved)` — `hs dev --json`, `hs doctor --json` and the MCP
|
|
45
|
+
* `heyspex_dev_mode` all carry it as `mode_override`
|
|
46
|
+
* ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]).
|
|
31
47
|
*
|
|
32
48
|
* Zero deps beyond node builtins (same rule as console-url-resolver.mjs).
|
|
33
49
|
*/
|
|
@@ -35,6 +51,8 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
35
51
|
import { homedir } from 'node:os';
|
|
36
52
|
import { join } from 'node:path';
|
|
37
53
|
|
|
54
|
+
import { assertNotRealHomeUnderTest, readBearer } from '../../vendor/heyspex-mcp/credentials.mjs';
|
|
55
|
+
|
|
38
56
|
export const CLOUD_CONSOLE_URL = 'https://app.heyspex.com';
|
|
39
57
|
export const LOCAL_CONSOLE_URL = 'http://localhost:3000';
|
|
40
58
|
export const DEV_MODE_FILENAME = 'dev-mode.json';
|
|
@@ -70,14 +88,19 @@ export function deriveSemantics(mode, reliance) {
|
|
|
70
88
|
};
|
|
71
89
|
}
|
|
72
90
|
|
|
91
|
+
/** Default file reader — injectable so tests never touch the real HOME. */
|
|
92
|
+
const defaultReadFile = (p) => readFileSync(p, 'utf8');
|
|
93
|
+
|
|
73
94
|
/**
|
|
74
95
|
* Read + validate ~/.heyspex/dev-mode.json. Never throws:
|
|
75
96
|
* returns { record, parseError } — record null when missing/invalid.
|
|
97
|
+
* `readFile` is an injectable seam (same shape as the TS twin's) so the
|
|
98
|
+
* parity test can feed both implementations the SAME in-memory fixture.
|
|
76
99
|
*/
|
|
77
|
-
export function readLocalDevModeRecord({ homeDir = homedir() } = {}) {
|
|
100
|
+
export function readLocalDevModeRecord({ homeDir = homedir(), readFile = defaultReadFile } = {}) {
|
|
78
101
|
let raw;
|
|
79
102
|
try {
|
|
80
|
-
raw =
|
|
103
|
+
raw = readFile(devModeFilePath(homeDir));
|
|
81
104
|
} catch {
|
|
82
105
|
return { record: null, parseError: null }; // absent — not an error
|
|
83
106
|
}
|
|
@@ -113,9 +136,10 @@ export function readLocalDevModeRecord({ homeDir = homedir() } = {}) {
|
|
|
113
136
|
export function resolveDevMode({
|
|
114
137
|
env = process.env,
|
|
115
138
|
homeDir = homedir(),
|
|
139
|
+
readFile = defaultReadFile,
|
|
116
140
|
cloudSetting = undefined,
|
|
117
141
|
} = {}) {
|
|
118
|
-
const local = readLocalDevModeRecord({ homeDir });
|
|
142
|
+
const local = readLocalDevModeRecord({ homeDir, readFile });
|
|
119
143
|
const cloudKnown = cloudSetting !== undefined;
|
|
120
144
|
const cloudValue =
|
|
121
145
|
cloudKnown && cloudSetting
|
|
@@ -128,11 +152,19 @@ export function resolveDevMode({
|
|
|
128
152
|
let mode;
|
|
129
153
|
let reliance;
|
|
130
154
|
let source;
|
|
155
|
+
// Where the RELIANCE came from. Equals `source` except in one case:
|
|
156
|
+
// `HEYSPEX_DEV_MODE=dev` with no `HEYSPEX_DEV_RELIANCE` used to mean local
|
|
157
|
+
// silently; it still means local, but the envelope now says the reliance
|
|
158
|
+
// was ASSUMED ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]
|
|
159
|
+
// AC1, invariant 10 — no hidden mode switch).
|
|
160
|
+
let relianceSource;
|
|
131
161
|
const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
|
|
132
162
|
if (envParsed) {
|
|
133
163
|
mode = envParsed.mode;
|
|
134
|
-
|
|
164
|
+
const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
|
|
165
|
+
reliance = envReliance ?? 'local';
|
|
135
166
|
source = 'env';
|
|
167
|
+
relianceSource = envReliance ? 'env' : 'assumed-local';
|
|
136
168
|
} else if (local.record) {
|
|
137
169
|
mode = local.record.mode;
|
|
138
170
|
reliance = local.record.reliance;
|
|
@@ -167,7 +199,7 @@ export function resolveDevMode({
|
|
|
167
199
|
vaultWriteMode: semantics.vaultWriteMode,
|
|
168
200
|
sources: {
|
|
169
201
|
mode: source,
|
|
170
|
-
reliance: source,
|
|
202
|
+
reliance: relianceSource ?? source,
|
|
171
203
|
consoleUrl:
|
|
172
204
|
source === 'local-override' && local.record?.console_url ? 'local-override' : 'derived',
|
|
173
205
|
readTier: 'derived',
|
|
@@ -183,6 +215,131 @@ export function resolveDevMode({
|
|
|
183
215
|
};
|
|
184
216
|
}
|
|
185
217
|
|
|
218
|
+
/** The `source` values {@link modeOverrideOf} can report. */
|
|
219
|
+
export const MODE_OVERRIDE_SOURCE_ENV = 'env';
|
|
220
|
+
export const MODE_OVERRIDE_SOURCE_FILE = 'dev-mode.json';
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The one structured answer to "is a mode switch active on this machine?" —
|
|
224
|
+
* what `hs dev --json`, `hs doctor --json` and `heyspex_dev_mode` all report
|
|
225
|
+
* as `mode_override` ([[HS-EVVI-WW9R-TASK-show-any-dev-mode-override-in-hs-dev]]
|
|
226
|
+
* AC1/AC2; epic [[HS-P1AW-WW9R-EPIC-identity-threading-and-the-end-of-the]]
|
|
227
|
+
* criterion 4). Pure: reads only the resolved envelope, never the file.
|
|
228
|
+
*
|
|
229
|
+
* null — nothing overrides the cloud/default
|
|
230
|
+
* { source: "env", written_at: null, written_by: "HEYSPEX_DEV_MODE", disagrees_with_cloud }
|
|
231
|
+
* { source: "dev-mode.json", written_at: <ISO>, written_by: <record.source>, disagrees_with_cloud }
|
|
232
|
+
*
|
|
233
|
+
* `written_at` / `written_by` are the record's `updated_at` / `source` (the
|
|
234
|
+
* cross-component file contract keeps its own key names; the Companion reads
|
|
235
|
+
* that file). `disagrees_with_cloud` is only ever true when the cloud setting
|
|
236
|
+
* is KNOWN and differs — an unreachable cloud is never a disagreement.
|
|
237
|
+
*/
|
|
238
|
+
export function modeOverrideOf(resolved) {
|
|
239
|
+
if (!resolved) return null;
|
|
240
|
+
const cloud = resolved.cloudSetting?.value ?? null;
|
|
241
|
+
if (resolved.sources?.mode === 'env') {
|
|
242
|
+
const disagrees =
|
|
243
|
+
cloud !== null &&
|
|
244
|
+
(cloud.mode !== resolved.mode ||
|
|
245
|
+
(resolved.mode === 'dev' && cloud.reliance !== resolved.reliance));
|
|
246
|
+
return {
|
|
247
|
+
source: MODE_OVERRIDE_SOURCE_ENV,
|
|
248
|
+
written_at: null,
|
|
249
|
+
written_by: 'HEYSPEX_DEV_MODE',
|
|
250
|
+
disagrees_with_cloud: disagrees,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (resolved.localOverride?.present) {
|
|
254
|
+
const record = resolved.localOverride.record;
|
|
255
|
+
return {
|
|
256
|
+
source: MODE_OVERRIDE_SOURCE_FILE,
|
|
257
|
+
written_at: record?.updated_at ?? null,
|
|
258
|
+
written_by: record?.source ?? null,
|
|
259
|
+
disagrees_with_cloud: resolved.localOverride.staleAgainstCloud === true,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** `prod` / `dev+local` / `dev+cloud` — the label doctor and hs dev print. */
|
|
266
|
+
export function describeModeLabel(resolved) {
|
|
267
|
+
return `${resolved.mode}${resolved.mode === 'dev' ? `+${resolved.reliance}` : ''}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The `hs doctor` row for {@link modeOverrideOf} (row id `mode-override`).
|
|
272
|
+
* Pure — the caller resolves ONCE and passes the envelope in; this never
|
|
273
|
+
* re-reads the file. Shape matches every other doctor row
|
|
274
|
+
* (`{ id, label, status, detail, human? }`) plus the structured
|
|
275
|
+
* `mode_override` so `--json` carries the same object `hs dev --json` does.
|
|
276
|
+
*
|
|
277
|
+
* ok no override; or one that pins prod (the default) and does not
|
|
278
|
+
* disagree with a known cloud setting — the file is then just a
|
|
279
|
+
* cache of the normal mode, and the detail still names its date and
|
|
280
|
+
* writer so nothing on disk stays invisible.
|
|
281
|
+
* warn an override puts this machine in dev mode (cloud setting unknown
|
|
282
|
+
* or agreeing) — names the file, date and writer, repair `hs dev prod`.
|
|
283
|
+
* fail the override DISAGREES with a known cloud setting — same repair.
|
|
284
|
+
*
|
|
285
|
+
* `repairCommand` is rendered by the caller (`hsCommand('dev prod')`) so the
|
|
286
|
+
* advice is in the form THIS machine can run.
|
|
287
|
+
*/
|
|
288
|
+
export function modeOverrideDoctorRow(resolved, { recordPath, repairCommand = 'hs dev prod' } = {}) {
|
|
289
|
+
const id = 'mode-override';
|
|
290
|
+
const label = 'Dev-mode override (env / ~/.heyspex/dev-mode.json)';
|
|
291
|
+
const override = modeOverrideOf(resolved);
|
|
292
|
+
const modeLabel = describeModeLabel(resolved);
|
|
293
|
+
if (!override) {
|
|
294
|
+
return {
|
|
295
|
+
id,
|
|
296
|
+
label,
|
|
297
|
+
status: 'ok',
|
|
298
|
+
detail: `none — ${modeLabel} from ${resolved.sources.mode}`,
|
|
299
|
+
mode_override: null,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const where =
|
|
303
|
+
override.source === MODE_OVERRIDE_SOURCE_ENV
|
|
304
|
+
? `HEYSPEX_DEV_MODE env override`
|
|
305
|
+
: `${recordPath ?? devModeFilePath()}`;
|
|
306
|
+
const stamp =
|
|
307
|
+
override.source === MODE_OVERRIDE_SOURCE_ENV
|
|
308
|
+
? ''
|
|
309
|
+
: ` (written ${override.written_at ?? 'unknown date'} by ${override.written_by ?? 'unknown writer'})`;
|
|
310
|
+
const cloud = resolved.cloudSetting?.value;
|
|
311
|
+
const cloudLabel = cloud ? describeModeLabel({ mode: cloud.mode, reliance: cloud.reliance }) : null;
|
|
312
|
+
const repair = `Run \`${repairCommand}\` to put this machine back on the cloud console, or set the mode on purpose with \`${repairCommand.replace(/\bdev prod$/, 'dev <mode>')}\`.`;
|
|
313
|
+
|
|
314
|
+
if (override.disagrees_with_cloud) {
|
|
315
|
+
return {
|
|
316
|
+
id,
|
|
317
|
+
label,
|
|
318
|
+
status: 'fail',
|
|
319
|
+
detail: `${modeLabel} from ${where}${stamp} — DISAGREES with the cloud setting (${cloudLabel})`,
|
|
320
|
+
human: repair,
|
|
321
|
+
mode_override: override,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (resolved.mode === 'dev') {
|
|
325
|
+
return {
|
|
326
|
+
id,
|
|
327
|
+
label,
|
|
328
|
+
status: 'warn',
|
|
329
|
+
detail: `${modeLabel} from ${where}${stamp}${cloudLabel ? ` — matches the cloud setting (${cloudLabel})` : ' — cloud setting not checked (offline)'}`,
|
|
330
|
+
human: repair,
|
|
331
|
+
mode_override: override,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
id,
|
|
336
|
+
label,
|
|
337
|
+
status: 'ok',
|
|
338
|
+
detail: `${where}${stamp} pins ${modeLabel} — the default, nothing overridden`,
|
|
339
|
+
mode_override: override,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
186
343
|
/**
|
|
187
344
|
* Upsert top-level `key: value` lines in ~/.heyspex/credentials.yaml without a
|
|
188
345
|
* full YAML round-trip (same targeted approach as the vendor MCP `pair`
|
|
@@ -192,8 +349,11 @@ export function resolveDevMode({
|
|
|
192
349
|
*/
|
|
193
350
|
export function upsertCredentialsYamlKeys(homeDir, updates) {
|
|
194
351
|
const dir = join(homeDir, '.heyspex');
|
|
195
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
196
352
|
const filePath = join(dir, 'credentials.yaml');
|
|
353
|
+
// The real-home guard the credentials and registry writers carry: under a
|
|
354
|
+
// test runner, refuse to touch the REAL ~/.heyspex (checked before mkdir).
|
|
355
|
+
assertNotRealHomeUnderTest(filePath);
|
|
356
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
197
357
|
let content = '';
|
|
198
358
|
try {
|
|
199
359
|
content = readFileSync(filePath, 'utf8');
|
|
@@ -214,14 +374,57 @@ export function upsertCredentialsYamlKeys(homeDir, updates) {
|
|
|
214
374
|
return filePath;
|
|
215
375
|
}
|
|
216
376
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
377
|
+
/**
|
|
378
|
+
* The bearer for cloud calls — `HEYSPEX_MCP_TOKEN` env, then `mcp_token:` in
|
|
379
|
+
* credentials.yaml — via the ONE shared reader
|
|
380
|
+
* (`vendor/heyspex-mcp/credentials.mjs`,
|
|
381
|
+
* [[HS-HL0D-WW9R-TASK-share-one-bearer-reader-and-one-console]]). Kept under
|
|
382
|
+
* its historical name for the hook scripts that import it; the precedence is
|
|
383
|
+
* the shared module's, never re-implemented here.
|
|
384
|
+
*/
|
|
385
|
+
export function readCredentialsMcpToken({ env = process.env, homeDir = homedir() } = {}) {
|
|
386
|
+
return readBearer({ env, homeDir }).token;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Run `fn(signal)` under a request timeout that is ALWAYS cleared and can
|
|
391
|
+
* never fire after the request has settled.
|
|
392
|
+
*
|
|
393
|
+
* Why not `AbortSignal.timeout(ms)`: that signal owns a timer nothing can
|
|
394
|
+
* cancel — it stays armed for the whole window even once the response has
|
|
395
|
+
* resolved, so a late abort can land on an already-settled request. Here the
|
|
396
|
+
* controller is ours: the timer is cleared in `finally` (rule 1 of
|
|
397
|
+
* [[HS-SWW3-Z9WP-DIAG-hs-dev-crashes-with-a-libuv-assertion]]) and `unref`'d
|
|
398
|
+
* so it can never hold the event loop open on its own.
|
|
399
|
+
*/
|
|
400
|
+
async function withRequestTimeout(timeoutMs, fn) {
|
|
401
|
+
const controller = new AbortController();
|
|
402
|
+
const state = { timedOut: false };
|
|
403
|
+
const timer = setTimeout(() => {
|
|
404
|
+
state.timedOut = true;
|
|
405
|
+
controller.abort();
|
|
406
|
+
}, timeoutMs);
|
|
407
|
+
timer.unref?.();
|
|
408
|
+
try {
|
|
409
|
+
return await fn(controller.signal);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
if (state.timedOut) throw new Error(`timed out after ${timeoutMs}ms`);
|
|
412
|
+
throw err;
|
|
413
|
+
} finally {
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Release a response we are not going to read. An undrained body keeps its
|
|
420
|
+
* socket half-read in the keep-alive pool; cancelling hands it back so the
|
|
421
|
+
* process can tear down cleanly. Never throws (the body may already be gone).
|
|
422
|
+
*/
|
|
423
|
+
async function discardResponseBody(res) {
|
|
219
424
|
try {
|
|
220
|
-
|
|
221
|
-
const match = raw.match(/^mcp_token:\s*["']?([^"'\r\n#]+)/m);
|
|
222
|
-
return match?.[1]?.trim() || null;
|
|
425
|
+
await res?.body?.cancel();
|
|
223
426
|
} catch {
|
|
224
|
-
|
|
427
|
+
/* already consumed, closed, or bodyless — nothing to release */
|
|
225
428
|
}
|
|
226
429
|
}
|
|
227
430
|
|
|
@@ -235,11 +438,16 @@ export async function fetchCloudDevMode({
|
|
|
235
438
|
if (!consoleUrl) return { ok: false, setting: undefined, error: 'no console url' };
|
|
236
439
|
try {
|
|
237
440
|
const token = readCredentialsMcpToken({ homeDir });
|
|
238
|
-
const res = await
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
441
|
+
const res = await withRequestTimeout(timeoutMs, (signal) =>
|
|
442
|
+
fetchImpl(`${consoleUrl.replace(/\/+$/, '')}/api/admin/dev-mode`, {
|
|
443
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
444
|
+
signal,
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
if (!res.ok) {
|
|
448
|
+
await discardResponseBody(res);
|
|
449
|
+
return { ok: false, setting: undefined, error: `HTTP ${res.status}` };
|
|
450
|
+
}
|
|
243
451
|
const body = await res.json();
|
|
244
452
|
return { ok: true, setting: body?.cloud ?? null, error: null };
|
|
245
453
|
} catch (err) {
|
|
@@ -297,18 +505,24 @@ export async function applyDevMode(
|
|
|
297
505
|
parsed.reliance ?? normalizeReliance(target?.reliance) ?? 'local';
|
|
298
506
|
const mode = parsed.mode;
|
|
299
507
|
const semantics = deriveSemantics(mode, reliance);
|
|
508
|
+
// Under a test runner, refuse a real-home target BEFORE the cloud precheck
|
|
509
|
+
// or any write (the same guard the credentials and registry writers carry).
|
|
510
|
+
assertNotRealHomeUnderTest(devModeFilePath(homeDir));
|
|
300
511
|
|
|
512
|
+
// Timer-safe on purpose — never `AbortSignal.timeout` here
|
|
513
|
+
// ([[HS-SWW3-Z9WP-DIAG-hs-dev-crashes-with-a-libuv-assertion]]).
|
|
301
514
|
async function postCloud(token) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
515
|
+
return withRequestTimeout(5000, (signal) =>
|
|
516
|
+
fetchImpl(`${semantics.consoleUrl}/api/admin/dev-mode`, {
|
|
517
|
+
method: 'POST',
|
|
518
|
+
headers: {
|
|
519
|
+
'content-type': 'application/json',
|
|
520
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
521
|
+
},
|
|
522
|
+
body: JSON.stringify({ mode, reliance: semantics.reliance, source }),
|
|
523
|
+
signal,
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
312
526
|
}
|
|
313
527
|
|
|
314
528
|
// 0. Authorization precheck — see the doc comment above.
|
|
@@ -318,6 +532,7 @@ export async function applyDevMode(
|
|
|
318
532
|
if (token) {
|
|
319
533
|
try {
|
|
320
534
|
const res = await postCloud(token);
|
|
535
|
+
await discardResponseBody(res);
|
|
321
536
|
if (res.status === 403) throw new DevModeNotAdminError();
|
|
322
537
|
cloud = { attempted: true, updated: res.ok, error: res.ok ? null : `HTTP ${res.status}` };
|
|
323
538
|
} catch (err) {
|
|
@@ -336,8 +551,8 @@ export async function applyDevMode(
|
|
|
336
551
|
source,
|
|
337
552
|
};
|
|
338
553
|
const dir = join(homeDir, '.heyspex');
|
|
339
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
340
554
|
const recordPath = devModeFilePath(homeDir);
|
|
555
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
341
556
|
writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
342
557
|
|
|
343
558
|
// 2. credentials.yaml console_url (rung 2 of the shared resolver).
|
|
@@ -354,6 +569,7 @@ export async function applyDevMode(
|
|
|
354
569
|
const res = await postCloud(token);
|
|
355
570
|
if (res.ok) cloud.updated = true;
|
|
356
571
|
else cloud.error = `HTTP ${res.status}`;
|
|
572
|
+
await discardResponseBody(res);
|
|
357
573
|
} catch (err) {
|
|
358
574
|
cloud.error = String(err?.message ?? err);
|
|
359
575
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* heyspex-home.mjs — the ONE resolver for "which home directory does
|
|
4
|
+
* `.heyspex/` live under".
|
|
5
|
+
*
|
|
6
|
+
* Every machine-global HeySpex file (`credentials.yaml`, `registry.json`,
|
|
7
|
+
* `dev-mode.json`, `config.yaml`, `env.keys`, companion pairing state) lives at
|
|
8
|
+
* `<home>/.heyspex/…`, and before this module `<home>` was always a bare
|
|
9
|
+
* `os.homedir()`. That makes the machine single-tenant: one signed-in identity,
|
|
10
|
+
* one vault registry, one console URL. There is no way to run a second,
|
|
11
|
+
* isolated HeySpex identity on the same box — which is exactly what a
|
|
12
|
+
* fresh-customer onboarding rehearsal needs (see
|
|
13
|
+
* [[HS-ONB-customer-install-checklist]] and
|
|
14
|
+
* [[HS-BFXF-DIAG-hs-never-resolves-on-a-fresh-machine]]).
|
|
15
|
+
*
|
|
16
|
+
* `HEYSPEX_HOME` overrides that home directory. It names the directory that
|
|
17
|
+
* CONTAINS `.heyspex/`, not the `.heyspex/` directory itself — so every
|
|
18
|
+
* existing `join(homeDir, '.heyspex', …)` call site keeps its shape and only
|
|
19
|
+
* its default changes:
|
|
20
|
+
*
|
|
21
|
+
* HEYSPEX_HOME=C:\heyspex-test\paul-mg → C:\heyspex-test\paul-mg\.heyspex\credentials.yaml
|
|
22
|
+
*
|
|
23
|
+
* Deliberately NOT an override of the `.heyspex` directory name: the two
|
|
24
|
+
* conventions collide (some callers hold `homeDir`, some hold the full
|
|
25
|
+
* `.heyspex` path), and a home-dir override substitutes cleanly at both.
|
|
26
|
+
*
|
|
27
|
+
* Scope note: this covers HeySpex's own state. It does not redirect `~/.claude`,
|
|
28
|
+
* `~/.gitconfig`, or npm — a full-fidelity clean-machine test still wants a
|
|
29
|
+
* separate OS user profile. What it buys is a second HeySpex identity (account,
|
|
30
|
+
* registry, active vault, console URL) alongside the builder identity on one
|
|
31
|
+
* machine.
|
|
32
|
+
*
|
|
33
|
+
* Plain Node ESM, zero deps beyond `node:os` — importable from the vendored MCP
|
|
34
|
+
* server, the Claude hooks, and the dev-factory dispatchers with no build step,
|
|
35
|
+
* exactly like `console-url-resolver.mjs` next to it.
|
|
36
|
+
*/
|
|
37
|
+
import { homedir } from 'node:os';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The home directory `.heyspex/` lives under.
|
|
41
|
+
*
|
|
42
|
+
* Precedence:
|
|
43
|
+
* 1. `HEYSPEX_HOME` env — an absolute path to the directory containing `.heyspex/`
|
|
44
|
+
* 2. `os.homedir()`
|
|
45
|
+
*
|
|
46
|
+
* A blank or whitespace-only `HEYSPEX_HOME` is treated as unset rather than as
|
|
47
|
+
* an empty path, so `HEYSPEX_HOME=` in a shell profile fails safe to the real
|
|
48
|
+
* home instead of resolving `.heyspex/` against the process CWD.
|
|
49
|
+
*
|
|
50
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function heyspexHome(env = process.env) {
|
|
54
|
+
const override = env?.HEYSPEX_HOME;
|
|
55
|
+
if (typeof override === 'string' && override.trim().length > 0) {
|
|
56
|
+
return override.trim();
|
|
57
|
+
}
|
|
58
|
+
return homedir();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* True when the resolved home came from `HEYSPEX_HOME` rather than the real
|
|
63
|
+
* user profile. Used by `hs doctor` and the login banner so an isolated
|
|
64
|
+
* identity is never mistaken for the machine's default one.
|
|
65
|
+
*
|
|
66
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
export function isHeyspexHomeOverridden(env = process.env) {
|
|
70
|
+
const override = env?.HEYSPEX_HOME;
|
|
71
|
+
return typeof override === 'string' && override.trim().length > 0;
|
|
72
|
+
}
|