spexcode 0.6.1 → 0.6.5
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 +99 -149
- package/bin/spex.mjs +4 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +76 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +33 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.js +1859 -0
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +111 -0
- package/node_modules/@spexcode/spec-cli/dist/client.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
- package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +53 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +686 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +547 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +361 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +37 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +503 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +716 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +35 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
- package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
- package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +115 -22
- package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +325 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.js +3026 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
- package/node_modules/@spexcode/spec-cli/dist/help.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/help.js +574 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
- package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
- package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
- package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
- package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
- package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +7 -0
- package/{spec-cli/src/listen.ts → node_modules/@spexcode/spec-cli/dist/listen.js} +13 -17
- package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
- package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
- package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
- package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
- package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
- package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +4 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +208 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
- package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
- package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
- package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
- package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.js +82 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +51 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +233 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +412 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +4239 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
- package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +27 -7
- package/node_modules/@spexcode/spec-cli/package.json +47 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +22 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +166 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +2736 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
- package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
- package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
- package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
- package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +183 -0
- package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
- package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
- package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
- package/node_modules/@spexcode/spec-core/package.json +28 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
- package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
- package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
- package/node_modules/@spexcode/spec-eval/package.json +36 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
- package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
- package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
- package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
- package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
- package/node_modules/@spexcode/spec-forge/package.json +32 -0
- package/package.json +33 -19
- package/spec-cli/bin/spex.mjs +0 -69
- package/spec-cli/src/anchors.ts +0 -706
- package/spec-cli/src/attach.ts +0 -43
- package/spec-cli/src/claude-headless.ts +0 -321
- package/spec-cli/src/cli.ts +0 -1115
- package/spec-cli/src/client.ts +0 -315
- package/spec-cli/src/cockpit.ts +0 -43
- package/spec-cli/src/codex-runtime-generations.ts +0 -646
- package/spec-cli/src/commit-surgery.ts +0 -68
- package/spec-cli/src/contract-filter.ts +0 -202
- package/spec-cli/src/delivery-queue.ts +0 -107
- package/spec-cli/src/doctor.ts +0 -516
- package/spec-cli/src/file-write.ts +0 -22
- package/spec-cli/src/gateway-auth.ts +0 -181
- package/spec-cli/src/gateway-hub.ts +0 -356
- package/spec-cli/src/gateway.ts +0 -368
- package/spec-cli/src/git.ts +0 -2180
- package/spec-cli/src/graph.ts +0 -221
- package/spec-cli/src/graphCache.ts +0 -641
- package/spec-cli/src/graphDelta.ts +0 -77
- package/spec-cli/src/graphStream.ts +0 -1019
- package/spec-cli/src/harness-select.ts +0 -77
- package/spec-cli/src/harness.ts +0 -2747
- package/spec-cli/src/headless-controller.ts +0 -63
- package/spec-cli/src/help.ts +0 -480
- package/spec-cli/src/host-resources.ts +0 -707
- package/spec-cli/src/host.ts +0 -611
- package/spec-cli/src/identity-presets.d.ts +0 -13
- package/spec-cli/src/identity-presets.js +0 -128
- package/spec-cli/src/index.ts +0 -730
- package/spec-cli/src/init.ts +0 -258
- package/spec-cli/src/issues-cli.ts +0 -276
- package/spec-cli/src/issues.ts +0 -237
- package/spec-cli/src/layout.ts +0 -557
- package/spec-cli/src/lint.ts +0 -458
- package/spec-cli/src/localIssues.ts +0 -538
- package/spec-cli/src/materialize.ts +0 -412
- package/spec-cli/src/mentions.ts +0 -81
- package/spec-cli/src/opencode-headless.ts +0 -205
- package/spec-cli/src/pi-headless.ts +0 -153
- package/spec-cli/src/plugin-harness.ts +0 -157
- package/spec-cli/src/process-identity.ts +0 -207
- package/spec-cli/src/project-identity.ts +0 -73
- package/spec-cli/src/project-store.ts +0 -17
- package/spec-cli/src/pty-bridge.ts +0 -396
- package/spec-cli/src/pty-helper.mjs +0 -96
- package/spec-cli/src/pty-native-helper.mjs +0 -22
- package/spec-cli/src/ranker.ts +0 -191
- package/spec-cli/src/reaper.ts +0 -125
- package/spec-cli/src/resilience.ts +0 -41
- package/spec-cli/src/reviewSnapshot.ts +0 -28
- package/spec-cli/src/reviews.ts +0 -405
- package/spec-cli/src/runtime-guard.ts +0 -35
- package/spec-cli/src/runtime-ownership.ts +0 -88
- package/spec-cli/src/search.bench.mjs +0 -80
- package/spec-cli/src/search.ts +0 -63
- package/spec-cli/src/session-cursors.ts +0 -91
- package/spec-cli/src/session-follow.ts +0 -182
- package/spec-cli/src/session-timeline.ts +0 -98
- package/spec-cli/src/sessions.ts +0 -3403
- package/spec-cli/src/sh.ts +0 -3
- package/spec-cli/src/slash-commands.ts +0 -318
- package/spec-cli/src/source-files.ts +0 -104
- package/spec-cli/src/specs.ts +0 -498
- package/spec-cli/src/supervise.ts +0 -245
- package/spec-cli/src/tree.ts +0 -118
- package/spec-cli/src/tsx-bin.ts +0 -18
- package/spec-cli/src/uninstall.ts +0 -171
- package/spec-cli/src/uploads.ts +0 -253
- package/spec-cli/src/worktree-sources.ts +0 -42
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
- package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -28
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +0 -23
- package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -32
- package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -9
- package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -15
- package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -18
- package/spec-dashboard/dist/assets/App-F9uaAVcH.js +0 -2
- package/spec-dashboard/dist/assets/Dashboard-Ba_jhxp1.js +0 -27
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
- package/spec-dashboard/dist/assets/EvalsPage-FixoOg_n.js +0 -2
- package/spec-dashboard/dist/assets/IssuesPage-CuKLFhH3.js +0 -1
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/spec-dashboard/dist/assets/MobileApp-CHgEHORJ.js +0 -2
- package/spec-dashboard/dist/assets/Modal-CQgYymmr.js +0 -1
- package/spec-dashboard/dist/assets/PageScroll-hT7UTLvD.js +0 -1
- package/spec-dashboard/dist/assets/ProjectsPage-CtXxakF9.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
- package/spec-dashboard/dist/assets/SessionInterface-Bpie-9fs.js +0 -39
- package/spec-dashboard/dist/assets/SessionWindow-CixDi4PI.js +0 -1
- package/spec-dashboard/dist/assets/Settings-C2MsucfE.js +0 -1
- package/spec-dashboard/dist/assets/Thread-C6Go8HRh.js +0 -13
- package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
- package/spec-dashboard/dist/assets/data-B-RQmit6.js +0 -1
- package/spec-dashboard/dist/assets/index-CsI8DElI.css +0 -1
- package/spec-dashboard/dist/assets/index-DrVao0Ep.js +0 -41
- package/spec-dashboard/dist/assets/launch-BBH02b1v.js +0 -276
- package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
- package/spec-dashboard/dist/assets/terminalFont-C6oGNKrf.js +0 -1
- package/spec-dashboard/dist/index.html +0 -29
- package/spec-dashboard/src/reviewFilters.js +0 -322
- package/spec-dashboard/src/reviewQuery.js +0 -174
- package/spec-dashboard/src/session.js +0 -199
- package/spec-eval/src/cache.ts +0 -57
- package/spec-eval/src/cli.ts +0 -889
- package/spec-eval/src/evaltab.ts +0 -262
- package/spec-eval/src/filing.ts +0 -39
- package/spec-eval/src/freshness.ts +0 -557
- package/spec-eval/src/humanok.ts +0 -28
- package/spec-eval/src/scenariofresh.ts +0 -244
- package/spec-eval/src/scenarios.ts +0 -672
- package/spec-eval/src/sessioneval.ts +0 -2226
- package/spec-eval/src/sidecar.ts +0 -93
- package/spec-eval/src/timeline.ts +0 -54
- package/spec-forge/src/__fixtures__/github-forge.json +0 -63
- package/spec-forge/src/cache.ts +0 -66
- package/spec-forge/src/cli.ts +0 -99
- package/spec-forge/src/drivers/github.ts +0 -132
- package/spec-forge/src/drivers/gitlab.ts +0 -153
- package/spec-forge/src/drivers.ts +0 -73
- package/spec-forge/src/links.ts +0 -72
- package/spec-forge/src/needs-eval.ts +0 -29
- package/spec-forge/src/port.ts +0 -36
- package/spec-forge/src/resident.ts +0 -46
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
package/spec-cli/src/host.ts
DELETED
|
@@ -1,611 +0,0 @@
|
|
|
1
|
-
// @@@ host gateway ([[host-gateway]]) - the host-level face of every SpexCode project this user runs.
|
|
2
|
-
// Each `spex serve` stays scoped to ONE repo, loopback-only and auth-unaware; what it contributes to the
|
|
3
|
-
// host is a single instance-validated endpoint record in the per-user global store. THIS module is the
|
|
4
|
-
// other half: the durable known-project catalog, the reconciler that turns records into a validated live
|
|
5
|
-
// project list, portable-config editor, and host operations (register/init/doctor/start) — mounted as [[gateway-hub]]'s
|
|
6
|
-
// extension by `spex dashboard` (startHostDashboard below), so routing, /p/:projectId proxying, and
|
|
7
|
-
// authorization ([[gateway-auth]]) stay the hub's single seam and are never duplicated here. Backends
|
|
8
|
-
// never depend on the gateway: they publish records and serve loopback whether or not a gateway is
|
|
9
|
-
// running, and direct CLI discovery (sessions.ts's ladder) keeps reading the same records.
|
|
10
|
-
import http from 'node:http'
|
|
11
|
-
import net from 'node:net'
|
|
12
|
-
import { spawn } from 'node:child_process'
|
|
13
|
-
import { createHash } from 'node:crypto'
|
|
14
|
-
import { mkdirSync, writeFileSync, readFileSync, renameSync, rmSync, readdirSync, openSync, closeSync, existsSync, realpathSync, statSync } from 'node:fs'
|
|
15
|
-
import { homedir } from 'node:os'
|
|
16
|
-
import { dirname, join, basename, resolve } from 'node:path'
|
|
17
|
-
import { fileURLToPath } from 'node:url'
|
|
18
|
-
import { spexcodeHome, encodeProject } from './layout.js'
|
|
19
|
-
import { git } from './git.js'
|
|
20
|
-
import { serveStatic, resolveDistDir, ensureDashboardBuilt } from './gateway.js'
|
|
21
|
-
import { startHubGateway, type HubExtensions } from './gateway-hub.js'
|
|
22
|
-
import { tsxBin } from './tsx-bin.js'
|
|
23
|
-
import { DEFAULT_PROJECT_ICON, requireIdentityChoice } from './identity-presets.js'
|
|
24
|
-
import {
|
|
25
|
-
resolveProjectIdentity, writeGatewayIcon, type ResolvedIdentity,
|
|
26
|
-
} from './project-identity.js'
|
|
27
|
-
|
|
28
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
29
|
-
|
|
30
|
-
// ── the endpoint record ──────────────────────────────────────────────────────────────────────────────
|
|
31
|
-
// One record per project, written by that project's `spex serve` after its public bind succeeds and
|
|
32
|
-
// removed (only by its own writer) on a clean stop. The shape carries the serve's IDENTITY — url, pid,
|
|
33
|
-
// instanceId, root — so a reader can validate "the backend at this url is the serve that wrote this
|
|
34
|
-
// record, serving this root" instead of trusting a URL that a recycled port may have re-occupied.
|
|
35
|
-
export type EndpointRecord = {
|
|
36
|
-
version: 2; url: string; pid: number; instanceId: string; root: string
|
|
37
|
-
identity: ResolvedIdentity; startedAt: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const endpointRecordPath = (root: string): string =>
|
|
41
|
-
join(spexcodeHome(), 'projects', encodeProject(root), 'backend.json')
|
|
42
|
-
|
|
43
|
-
// atomic publish: tmp + rename, so a reader never sees a torn record (the old write-in-place could be
|
|
44
|
-
// caught mid-write by the reconciler or a bare `spex`'s discovery probe).
|
|
45
|
-
export function publishEndpoint(rec: EndpointRecord): void {
|
|
46
|
-
const file = endpointRecordPath(rec.root)
|
|
47
|
-
mkdirSync(dirname(file), { recursive: true })
|
|
48
|
-
const tmp = join(dirname(file), `.backend.json.${process.pid}.tmp`)
|
|
49
|
-
writeFileSync(tmp, JSON.stringify(rec, null, 2) + '\n')
|
|
50
|
-
renameSync(tmp, file)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// remove the record ONLY if it is ours (matched by instanceId): a newer serve that already overwrote it,
|
|
54
|
-
// or another project's record, is never deleted by a retiring process.
|
|
55
|
-
export function dropOwnEndpoint(instanceId: string, root: string): void {
|
|
56
|
-
const file = endpointRecordPath(root)
|
|
57
|
-
try { if (JSON.parse(readFileSync(file, 'utf8'))?.instanceId === instanceId) rmSync(file) } catch { /* not ours / already gone */ }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// a record is HOSTABLE only in the full identity shape; legacy {url,pid} records (pre-instance-identity)
|
|
61
|
-
// are ignored by the host — the direct CLI ladder still reads their url, and they are rewritten in the new
|
|
62
|
-
// shape the next time that serve restarts.
|
|
63
|
-
export function readEndpointRecord(file: string): EndpointRecord | null {
|
|
64
|
-
try {
|
|
65
|
-
const r = JSON.parse(readFileSync(file, 'utf8'))
|
|
66
|
-
if (r?.version === 2 && typeof r.url === 'string' && typeof r.instanceId === 'string' && typeof r.root === 'string' &&
|
|
67
|
-
typeof r.identity?.title === 'string' && typeof r.identity?.icon === 'string') return r as EndpointRecord
|
|
68
|
-
return null
|
|
69
|
-
} catch { return null }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── the durable known-project catalog ────────────────────────────────────────────────────────────────
|
|
73
|
-
// ~/.spexcode/projects.json — the host's memory of which projects exist, so /projects can list a project
|
|
74
|
-
// whose backend is OFFLINE (records vanish with their serve; the catalog does not). Populated two ways:
|
|
75
|
-
// explicitly (the add op) and by auto-adoption of any validated live record, so every project ever served
|
|
76
|
-
// under this user shows up without a registration ceremony.
|
|
77
|
-
export type CatalogEntry = { root: string; addedAt: string }
|
|
78
|
-
export const catalogPath = (): string => join(spexcodeHome(), 'projects.json')
|
|
79
|
-
|
|
80
|
-
let catalogWarned = false
|
|
81
|
-
export function readCatalog(): CatalogEntry[] {
|
|
82
|
-
let raw: string
|
|
83
|
-
try { raw = readFileSync(catalogPath(), 'utf8') } catch { return [] }
|
|
84
|
-
try {
|
|
85
|
-
const parsed = JSON.parse(raw)
|
|
86
|
-
const list = Array.isArray(parsed?.projects) ? parsed.projects : []
|
|
87
|
-
return list.filter((e: any): e is CatalogEntry => e && typeof e.root === 'string')
|
|
88
|
-
} catch (e) {
|
|
89
|
-
// reads degrade loud-but-alive (the reconciler must keep serving live records); WRITES refuse below,
|
|
90
|
-
// so a malformed catalog is never silently clobbered.
|
|
91
|
-
if (!catalogWarned) { catalogWarned = true; console.error(`[host] malformed ${catalogPath()} (${(e as Error).message}) — known-project catalog ignored until fixed`) }
|
|
92
|
-
return []
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function writeCatalog(entries: CatalogEntry[]): void {
|
|
97
|
-
if (existsSync(catalogPath())) {
|
|
98
|
-
try { JSON.parse(readFileSync(catalogPath(), 'utf8')) }
|
|
99
|
-
catch { throw new Error(`refusing to overwrite malformed ${catalogPath()} — fix or remove it first`) }
|
|
100
|
-
}
|
|
101
|
-
mkdirSync(dirname(catalogPath()), { recursive: true })
|
|
102
|
-
const tmp = join(dirname(catalogPath()), `.projects.json.${process.pid}.tmp`)
|
|
103
|
-
writeFileSync(tmp, JSON.stringify({ projects: entries }, null, 2) + '\n')
|
|
104
|
-
renameSync(tmp, catalogPath())
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function existingDirectory(dir: string): string {
|
|
108
|
-
let path: string
|
|
109
|
-
try { path = realpathSync(resolve(dir)) }
|
|
110
|
-
catch { throw new Error(`${dir} is not an existing directory`) }
|
|
111
|
-
try { if (!statSync(path).isDirectory()) throw new Error('not a directory') }
|
|
112
|
-
catch { throw new Error(`${dir} is not an existing directory`) }
|
|
113
|
-
return path
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function gitProjectRoot(dir: string): string | null {
|
|
117
|
-
try { return dirname(git(['-C', dir, 'rev-parse', '--path-format=absolute', '--git-common-dir']).trim()) }
|
|
118
|
-
catch { return null }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// The admin folder picker reads directory NAMES only. Its selected-directory projection lets the UI ask
|
|
122
|
-
// before either side effect instead of discovering the Git precondition after submit.
|
|
123
|
-
export type ProjectDirectoryListing = {
|
|
124
|
-
path: string; parent: string | null; home: string; gitRoot: string | null
|
|
125
|
-
initialized: boolean; cataloged: boolean
|
|
126
|
-
entries: Array<{ name: string; path: string; git: boolean; initialized: boolean }>
|
|
127
|
-
}
|
|
128
|
-
export function browseProjectDirectories(dir?: string): ProjectDirectoryListing {
|
|
129
|
-
const path = existingDirectory((dir ?? '').trim() || homedir())
|
|
130
|
-
const gitRoot = gitProjectRoot(path)
|
|
131
|
-
const entries = readdirSync(path, { withFileTypes: true })
|
|
132
|
-
.filter((entry) => {
|
|
133
|
-
if (entry.isDirectory()) return true
|
|
134
|
-
if (!entry.isSymbolicLink()) return false
|
|
135
|
-
try { return statSync(join(path, entry.name)).isDirectory() } catch { return false }
|
|
136
|
-
})
|
|
137
|
-
.map((entry) => {
|
|
138
|
-
const child = join(path, entry.name)
|
|
139
|
-
const isGit = existsSync(join(child, '.git'))
|
|
140
|
-
return {
|
|
141
|
-
name: entry.name,
|
|
142
|
-
path: child,
|
|
143
|
-
git: isGit,
|
|
144
|
-
initialized: isGit && existsSync(join(child, '.spec')),
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
148
|
-
return {
|
|
149
|
-
path,
|
|
150
|
-
parent: dirname(path) === path ? null : dirname(path),
|
|
151
|
-
home: homedir(),
|
|
152
|
-
gitRoot,
|
|
153
|
-
initialized: !!gitRoot && existsSync(join(gitRoot, '.spec')),
|
|
154
|
-
cataloged: !!gitRoot && readCatalog().some((entry) => entry.root === gitRoot),
|
|
155
|
-
entries,
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Register a repo by normalizing any path inside it to its MAIN checkout (the identity every record and
|
|
160
|
-
// store key uses), then dedupe + persist it.
|
|
161
|
-
export function addKnownProject(dir: string): string {
|
|
162
|
-
const path = existingDirectory(dir)
|
|
163
|
-
const root = gitProjectRoot(path)
|
|
164
|
-
if (!root) throw new Error(`${dir} is not a git repository — SpexCode projects are git-backed (run \`git init\` there first)`)
|
|
165
|
-
catalogAdd(root)
|
|
166
|
-
return root
|
|
167
|
-
}
|
|
168
|
-
function catalogAdd(root: string): void {
|
|
169
|
-
const entries = readCatalog()
|
|
170
|
-
if (entries.some((e) => e.root === root)) return
|
|
171
|
-
writeCatalog([...entries, { root, addedAt: new Date().toISOString() }])
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ── the reconciler ───────────────────────────────────────────────────────────────────────────────────
|
|
175
|
-
// Turn the global store's endpoint records + the catalog into ONE validated project list. A record counts
|
|
176
|
-
// as ONLINE only when (a) it sits in the store slot its own root encodes to, and (b) the live backend at
|
|
177
|
-
// its url answers /api/instance with the SAME instanceId and root — anything else (dead process, recycled
|
|
178
|
-
// port, copied record, another project's serve) is just an offline project, never a proxy target.
|
|
179
|
-
export type ProjectEntry = {
|
|
180
|
-
projectId: string; root: string; identity: ResolvedIdentity; configRevision: string
|
|
181
|
-
online: boolean; url: string | null; pid?: number; startedAt?: string
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
type LiveInstance = { instanceId?: string; root?: string; identity?: ResolvedIdentity }
|
|
185
|
-
async function fetchInstance(url: string): Promise<LiveInstance | null> {
|
|
186
|
-
const ctrl = new AbortController()
|
|
187
|
-
const t = setTimeout(() => ctrl.abort(), 900)
|
|
188
|
-
try {
|
|
189
|
-
const r = await fetch(`${url}/api/instance`, { signal: ctrl.signal })
|
|
190
|
-
if (!r.ok) return null
|
|
191
|
-
return await r.json() as LiveInstance
|
|
192
|
-
} catch { return null }
|
|
193
|
-
finally { clearTimeout(t) }
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export async function reconcileProjects(): Promise<ProjectEntry[]> {
|
|
197
|
-
const projectsDir = join(spexcodeHome(), 'projects')
|
|
198
|
-
let dirs: string[] = []
|
|
199
|
-
try { dirs = readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name) }
|
|
200
|
-
catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e }
|
|
201
|
-
|
|
202
|
-
const live = new Map<string, { rec: EndpointRecord; identity: ResolvedIdentity }>()
|
|
203
|
-
const claimed = new Set<string>() // roots with a slot-matched record, validated or not
|
|
204
|
-
await Promise.all(dirs.map(async (d) => {
|
|
205
|
-
const rec = readEndpointRecord(join(projectsDir, d, 'backend.json'))
|
|
206
|
-
if (!rec) return
|
|
207
|
-
if (encodeProject(rec.root) !== d) return
|
|
208
|
-
claimed.add(rec.root) // a dead/mismatched record still NAMES a project — listed offline
|
|
209
|
-
const inst = await fetchInstance(rec.url)
|
|
210
|
-
if (inst && inst.instanceId === rec.instanceId && inst.root === rec.root &&
|
|
211
|
-
typeof inst.identity?.title === 'string' && typeof inst.identity?.icon === 'string') {
|
|
212
|
-
live.set(rec.root, { rec, identity: inst.identity })
|
|
213
|
-
}
|
|
214
|
-
}))
|
|
215
|
-
|
|
216
|
-
// auto-adopt: a VALIDATED live root becomes durable catalog knowledge, so it stays listed after its
|
|
217
|
-
// serve stops. Unvalidated claims are listed this pass but never written durable — a stale or wrong
|
|
218
|
-
// record must not pollute the catalog. Best-effort — a refused catalog write (malformed file) must not
|
|
219
|
-
// hide live backends.
|
|
220
|
-
for (const root of live.keys()) {
|
|
221
|
-
try { catalogAdd(root) } catch (e) { if (!catalogWarned) { catalogWarned = true; console.error(`[host] ${(e as Error).message}`) } }
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const byId = new Map<string, ProjectEntry>()
|
|
225
|
-
const push = (root: string) => {
|
|
226
|
-
if (!existsSync(root)) return
|
|
227
|
-
const projectId = encodeProject(root)
|
|
228
|
-
if (byId.has(projectId)) return // encodeProject is lossy; first root wins a (pathological) collision
|
|
229
|
-
const active = live.get(root) ?? null
|
|
230
|
-
const source = readProjectConfig(root)
|
|
231
|
-
let identity: ResolvedIdentity
|
|
232
|
-
try { identity = active?.identity ?? resolveProjectIdentity(root, root) }
|
|
233
|
-
catch (e) {
|
|
234
|
-
console.error(`[host] cannot resolve identity for ${root}: ${(e as Error).message}`)
|
|
235
|
-
identity = { title: basename(root), icon: DEFAULT_PROJECT_ICON }
|
|
236
|
-
}
|
|
237
|
-
byId.set(projectId, {
|
|
238
|
-
projectId, root, identity, configRevision: source.revision,
|
|
239
|
-
online: !!active, url: active?.rec.url ?? null,
|
|
240
|
-
...(active ? { pid: active.rec.pid, startedAt: active.rec.startedAt } : {}),
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
for (const e of readCatalog()) push(e.root)
|
|
244
|
-
for (const root of claimed) push(root)
|
|
245
|
-
return [...byId.values()].sort((a, b) => a.identity.title.localeCompare(b.identity.title) || a.root.localeCompare(b.root))
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// single-flight + last-snapshot: every reader (GET, the stream loop, the per-request proxy resolution)
|
|
249
|
-
// shares one in-flight reconcile instead of stampeding probes.
|
|
250
|
-
let snapshot: ProjectEntry[] = []
|
|
251
|
-
let inflight: Promise<ProjectEntry[]> | null = null
|
|
252
|
-
export function reconcileNow(): Promise<ProjectEntry[]> {
|
|
253
|
-
return (inflight ??= reconcileProjects()
|
|
254
|
-
.then((list) => { snapshot = list; return list })
|
|
255
|
-
.finally(() => { inflight = null }))
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── host operations (spawned `spex`, never forked logic) ─────────────────────────────────────────────
|
|
259
|
-
// init / doctor / serve run the EXISTING CLI implementations as child processes with cwd = the project
|
|
260
|
-
// root: the same git/harness/additive guarantees `spex init` gives at a terminal, the same doctor report,
|
|
261
|
-
// the same supervisor `spex serve` boots — no second implementation of any domain semantics. The spawned
|
|
262
|
-
// env is scrubbed of routing state (SPEXCODE_API_URL/PORT/session/instance ids) so a child never inherits
|
|
263
|
-
// the gateway's — or another project's — backend routing.
|
|
264
|
-
const cliEntry = join(here, 'cli.ts')
|
|
265
|
-
function scrubbedEnv(): NodeJS.ProcessEnv {
|
|
266
|
-
const env = { ...process.env }
|
|
267
|
-
delete env.SPEXCODE_API_URL; delete env.PORT
|
|
268
|
-
delete env.SPEXCODE_SESSION_ID; delete env.SPEXCODE_INSTANCE_ID
|
|
269
|
-
return env
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export function runSpex(root: string, args: string[], timeoutMs = 120_000): Promise<{ code: number | null; output: string }> {
|
|
273
|
-
return new Promise((resolve) => {
|
|
274
|
-
const child = spawn(process.execPath, [tsxBin(join(here, '..')), cliEntry, ...args], { cwd: root, env: scrubbedEnv() })
|
|
275
|
-
let output = ''
|
|
276
|
-
child.stdout.on('data', (d) => { output += d })
|
|
277
|
-
child.stderr.on('data', (d) => { output += d })
|
|
278
|
-
const t = setTimeout(() => { try { child.kill('SIGKILL') } catch { /* gone */ } }, timeoutMs)
|
|
279
|
-
child.on('close', (code) => { clearTimeout(t); resolve({ code, output }) })
|
|
280
|
-
child.on('error', (e) => { clearTimeout(t); resolve({ code: null, output: `${output}\nspawn failed: ${e.message}` }) })
|
|
281
|
-
})
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export type AddProjectSetup = {
|
|
285
|
-
initGit?: boolean
|
|
286
|
-
init?: { harness: string; preset?: string }
|
|
287
|
-
}
|
|
288
|
-
export type AddProjectSetupResult = {
|
|
289
|
-
ok: boolean; root: string; gitInitialized: boolean
|
|
290
|
-
init?: { code: number | null; output: string }
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// One ordered add transaction: select an existing directory, explicitly create Git when requested, run
|
|
294
|
-
// the real CLI initializer when requested, and only then claim catalog success.
|
|
295
|
-
export async function addKnownProjectWithSetup(dir: string, setup: AddProjectSetup = {}): Promise<AddProjectSetupResult> {
|
|
296
|
-
const path = existingDirectory(dir)
|
|
297
|
-
let root = gitProjectRoot(path)
|
|
298
|
-
let gitInitialized = false
|
|
299
|
-
if (!root) {
|
|
300
|
-
if (!setup.initGit) throw new Error(`${dir} is not a git repository — enable Git initialization to add it`)
|
|
301
|
-
git(['init', '--quiet', '--', path])
|
|
302
|
-
gitInitialized = true
|
|
303
|
-
root = gitProjectRoot(path)
|
|
304
|
-
if (!root) throw new Error(`git init completed but ${path} is still not a Git repository`)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
let init: AddProjectSetupResult['init']
|
|
308
|
-
if (setup.init) {
|
|
309
|
-
const harness = typeof setup.init.harness === 'string' ? setup.init.harness.trim() : ''
|
|
310
|
-
if (!harness) throw new Error('SpexCode initialization requires at least one explicit harness target')
|
|
311
|
-
const preset = typeof setup.init.preset === 'string' ? setup.init.preset.trim() : ''
|
|
312
|
-
const result = await runSpex(root, ['init', '--harness', harness, ...(preset ? ['--preset', preset] : [])])
|
|
313
|
-
init = result
|
|
314
|
-
if (result.code !== 0) return { ok: false, root, gitInitialized, init }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
catalogAdd(root)
|
|
318
|
-
return { ok: true, root, gitInitialized, ...(init ? { init } : {}) }
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// The dashboard edits the committed, portable source file verbatim. The host fixes the filename (there
|
|
322
|
-
// is no browser-supplied path), works for offline projects, and uses a content revision so a save cannot
|
|
323
|
-
// clobber a concurrent agent/user edit. spexcode.local.json stays outside this surface by contract.
|
|
324
|
-
type ProjectConfigSource = { content: string; revision: string }
|
|
325
|
-
let configWriteSeq = 0
|
|
326
|
-
const configRevision = (raw: string | null): string => createHash('sha256').update(raw === null ? 'missing' : `present\0${raw}`).digest('hex')
|
|
327
|
-
|
|
328
|
-
function readProjectConfig(root: string): ProjectConfigSource {
|
|
329
|
-
try {
|
|
330
|
-
const content = readFileSync(join(root, 'spexcode.json'), 'utf8')
|
|
331
|
-
return { content, revision: configRevision(content) }
|
|
332
|
-
} catch (e) {
|
|
333
|
-
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return { content: '{}\n', revision: configRevision(null) }
|
|
334
|
-
throw e
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function writeProjectConfig(root: string, content: string, revision: string): ProjectConfigSource {
|
|
339
|
-
const current = readProjectConfig(root)
|
|
340
|
-
if (revision !== current.revision) {
|
|
341
|
-
const e = new Error('spexcode.json changed on disk — reload before saving') as Error & { status?: number }
|
|
342
|
-
e.status = 409
|
|
343
|
-
throw e
|
|
344
|
-
}
|
|
345
|
-
let parsed: unknown
|
|
346
|
-
try { parsed = JSON.parse(content) }
|
|
347
|
-
catch (e) {
|
|
348
|
-
const err = new Error(`spexcode.json is not valid JSON: ${(e as Error).message}`) as Error & { status?: number }
|
|
349
|
-
err.status = 400
|
|
350
|
-
throw err
|
|
351
|
-
}
|
|
352
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
353
|
-
const e = new Error('spexcode.json must contain one top-level JSON object') as Error & { status?: number }
|
|
354
|
-
e.status = 400
|
|
355
|
-
throw e
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const normalized = content.endsWith('\n') ? content : `${content}\n`
|
|
359
|
-
const file = join(root, 'spexcode.json')
|
|
360
|
-
const tmp = join(root, `.spexcode.json.${process.pid}.${++configWriteSeq}.tmp`)
|
|
361
|
-
try {
|
|
362
|
-
writeFileSync(tmp, normalized)
|
|
363
|
-
renameSync(tmp, file)
|
|
364
|
-
} finally {
|
|
365
|
-
try { rmSync(tmp) } catch { /* rename consumed it / write never created it */ }
|
|
366
|
-
}
|
|
367
|
-
return { content: normalized, revision: configRevision(normalized) }
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function writeProjectIcon(root: string, icon: unknown, revision: string): ProjectConfigSource & { identity: ResolvedIdentity } {
|
|
371
|
-
const canonical = requireIdentityChoice(icon)
|
|
372
|
-
const current = readProjectConfig(root)
|
|
373
|
-
let config: Record<string, any>
|
|
374
|
-
try { config = JSON.parse(current.content) }
|
|
375
|
-
catch (e) {
|
|
376
|
-
const error = new Error(`spexcode.json is not valid JSON: ${(e as Error).message}`) as Error & { status?: number }
|
|
377
|
-
error.status = 400
|
|
378
|
-
throw error
|
|
379
|
-
}
|
|
380
|
-
const dashboard = config.dashboard === undefined
|
|
381
|
-
? {}
|
|
382
|
-
: config.dashboard && typeof config.dashboard === 'object' && !Array.isArray(config.dashboard)
|
|
383
|
-
? config.dashboard
|
|
384
|
-
: null
|
|
385
|
-
if (!dashboard) {
|
|
386
|
-
const error = new Error('spexcode.json dashboard must be an object before its icon can be changed') as Error & { status?: number }
|
|
387
|
-
error.status = 400
|
|
388
|
-
throw error
|
|
389
|
-
}
|
|
390
|
-
const saved = writeProjectConfig(root, `${JSON.stringify({ ...config, dashboard: { ...dashboard, icon: canonical } }, null, 2)}\n`, revision)
|
|
391
|
-
return { ...saved, identity: resolveProjectIdentity(root, root) }
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function freeTcpPort(): Promise<number> {
|
|
395
|
-
return new Promise((res, rej) => {
|
|
396
|
-
const s = net.createServer()
|
|
397
|
-
s.once('error', rej)
|
|
398
|
-
s.listen(0, '127.0.0.1', () => { const p = (s.address() as net.AddressInfo).port; s.close(() => res(p)) })
|
|
399
|
-
})
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// start an OFFLINE project's backend: spawn a detached `spex serve --port <free>` whose lifetime is
|
|
403
|
-
// independent of the gateway (it owns its record exactly like a hand-run serve), log to the project's
|
|
404
|
-
// runtime tier, and wait for its instance-validated record to reconcile online.
|
|
405
|
-
export async function startBackend(root: string, waitMs = 45_000): Promise<ProjectEntry> {
|
|
406
|
-
const port = await freeTcpPort()
|
|
407
|
-
const logDir = join(spexcodeHome(), 'projects', encodeProject(root))
|
|
408
|
-
mkdirSync(logDir, { recursive: true })
|
|
409
|
-
const logFile = join(logDir, 'serve.log')
|
|
410
|
-
const log = openSync(logFile, 'a')
|
|
411
|
-
const child = spawn(process.execPath, [tsxBin(join(here, '..')), cliEntry, 'serve', '--port', String(port)],
|
|
412
|
-
{ cwd: root, env: scrubbedEnv(), detached: true, stdio: ['ignore', log, log] })
|
|
413
|
-
child.unref()
|
|
414
|
-
closeSync(log) // the child holds its own copy; keep no fd open in the gateway
|
|
415
|
-
const deadline = Date.now() + waitMs
|
|
416
|
-
while (Date.now() < deadline) {
|
|
417
|
-
await new Promise((r) => setTimeout(r, 500))
|
|
418
|
-
const entry = (await reconcileNow()).find((p) => p.root === root && p.online)
|
|
419
|
-
if (entry) return entry
|
|
420
|
-
}
|
|
421
|
-
throw new Error(`backend for ${root} did not come online within ${Math.round(waitMs / 1000)}s — see ${logFile}`)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// ── the operator verb: `spex dashboard` = the hub + the host extensions ─────────────────────────────
|
|
426
|
-
// The hub ([[gateway-hub]]) is the ONE routing + authorization server; this layer never runs a second
|
|
427
|
-
// gateway or a second auth check beside it. What the host adds rides the hub's extension seam:
|
|
428
|
-
// listProjects — GET /projects rows come from the instance-validated reconciler + the durable catalog
|
|
429
|
-
// (online/offline/root), each carrying the hub's gating state.
|
|
430
|
-
// adminRoute — /projects/stream (SSE), GET /projects/browse + POST /projects (select/setup/register), raw spexcode.json
|
|
431
|
-
// GET|PUT /projects/:id/config, and POST /projects/:id/(init|doctor|serve) — all
|
|
432
|
-
// behind the hub's admin scope
|
|
433
|
-
// ([[gateway-auth]]: implicit from loopback until an admin password exists).
|
|
434
|
-
// fallback — the dashboard SPA shell + assets for paths the hub doesn't own.
|
|
435
|
-
// /p/:projectId/* routing (HTTP, SSE, WS; prefix strip; cookie strip) belongs entirely to the hub.
|
|
436
|
-
|
|
437
|
-
// tls is the HUB's transport option passed through unchanged ([[gateway-hub]] terminates HTTPS itself),
|
|
438
|
-
// so an operator deployment runs the ONE host gateway directly over TLS — no second proxy in front.
|
|
439
|
-
// Absent tls = the default plain-HTTP loopback `spex dashboard` serves.
|
|
440
|
-
export type HostDashboardOpts = { port: number; host?: string; distDir?: string; tls?: { cert: string; key: string } | null }
|
|
441
|
-
export type HostDashboard = { server: http.Server; close: () => Promise<void> }
|
|
442
|
-
|
|
443
|
-
const json = (res: http.ServerResponse, status: number, body: unknown) => {
|
|
444
|
-
res.writeHead(status, { 'Content-Type': 'application/json' })
|
|
445
|
-
res.end(JSON.stringify(body))
|
|
446
|
-
}
|
|
447
|
-
function readBody(req: http.IncomingMessage): Promise<string> {
|
|
448
|
-
return new Promise((resolve) => {
|
|
449
|
-
let body = ''
|
|
450
|
-
req.on('data', (d) => { body += d; if (body.length > 65536) req.destroy() })
|
|
451
|
-
req.on('end', () => resolve(body))
|
|
452
|
-
})
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export function startHostDashboard(opts: HostDashboardOpts): HostDashboard {
|
|
456
|
-
const distDir = opts.distDir ?? resolveDistDir()
|
|
457
|
-
if (!opts.distDir) ensureDashboardBuilt(join(here, '..', '..'), distDir)
|
|
458
|
-
|
|
459
|
-
const sseClients = new Set<http.ServerResponse>()
|
|
460
|
-
let lastBroadcast = ''
|
|
461
|
-
// reconcile → push the fresh list to every /projects/stream subscriber when it changed.
|
|
462
|
-
async function tick(): Promise<ProjectEntry[]> {
|
|
463
|
-
const list = await reconcileNow()
|
|
464
|
-
const j = JSON.stringify(list)
|
|
465
|
-
if (j !== lastBroadcast) { lastBroadcast = j; for (const c of sseClients) c.write(`data: ${j}\n\n`) }
|
|
466
|
-
return list
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const extensions: HubExtensions = {
|
|
470
|
-
// the hub's GET /projects rows, host-enriched: every cataloged/claimed project (not only live
|
|
471
|
-
// records), instance-validated online state, and the hub's own gating flag per row. `id` mirrors
|
|
472
|
-
// projectId — the hub's documented row key, kept so both shapes read the same list.
|
|
473
|
-
listProjects: async (store) => (await tick()).map((p) => ({ id: p.projectId, ...p, gated: !!store.projects[p.projectId] })),
|
|
474
|
-
|
|
475
|
-
adminRoute: async (req, res, path) => {
|
|
476
|
-
if (path === '/projects/stream') {
|
|
477
|
-
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' })
|
|
478
|
-
res.write(`data: ${JSON.stringify(await tick())}\n\n`)
|
|
479
|
-
sseClients.add(res)
|
|
480
|
-
req.on('close', () => sseClients.delete(res))
|
|
481
|
-
return true
|
|
482
|
-
}
|
|
483
|
-
if (path === '/projects/browse' && req.method === 'GET') {
|
|
484
|
-
const requested = new URL(req.url ?? path, 'http://localhost').searchParams.get('path') ?? ''
|
|
485
|
-
try { json(res, 200, browseProjectDirectories(requested)) }
|
|
486
|
-
catch (e) { json(res, 400, { error: (e as Error).message }) }
|
|
487
|
-
return true
|
|
488
|
-
}
|
|
489
|
-
if (path === '/projects' && req.method === 'POST') {
|
|
490
|
-
let body: any = {}
|
|
491
|
-
try { body = JSON.parse(await readBody(req) || '{}') } catch { /* malformed body */ }
|
|
492
|
-
const root = String(body?.root ?? '').trim()
|
|
493
|
-
if (!root) { json(res, 400, { error: 'body must be {"root": "/abs/path/to/repo"}' }); return true }
|
|
494
|
-
if (body.init !== undefined && (!body.init || typeof body.init !== 'object' || Array.isArray(body.init))) {
|
|
495
|
-
json(res, 400, { error: 'init must be {"harness":"<ids>","preset":"<optional>"}' })
|
|
496
|
-
return true
|
|
497
|
-
}
|
|
498
|
-
try {
|
|
499
|
-
const setup = await addKnownProjectWithSetup(root, {
|
|
500
|
-
initGit: body.initGit === true,
|
|
501
|
-
...(body.init ? { init: { harness: body.init.harness, ...(body.init.preset !== undefined ? { preset: body.init.preset } : {}) } } : {}),
|
|
502
|
-
})
|
|
503
|
-
if (!setup.ok) {
|
|
504
|
-
json(res, 422, { error: 'spex init failed', ...setup })
|
|
505
|
-
return true
|
|
506
|
-
}
|
|
507
|
-
const entry = (await tick()).find((p) => p.root === setup.root)
|
|
508
|
-
json(res, 200, { ...(entry ?? {
|
|
509
|
-
projectId: encodeProject(setup.root), root: setup.root,
|
|
510
|
-
identity: resolveProjectIdentity(setup.root, setup.root), configRevision: readProjectConfig(setup.root).revision,
|
|
511
|
-
online: false, url: null,
|
|
512
|
-
}), setup })
|
|
513
|
-
} catch (e) { json(res, 400, { error: (e as Error).message }) }
|
|
514
|
-
return true
|
|
515
|
-
}
|
|
516
|
-
if (path === '/projects/icon' && req.method === 'PUT') {
|
|
517
|
-
let body: any
|
|
518
|
-
try { body = JSON.parse(await readBody(req) || '{}') }
|
|
519
|
-
catch { json(res, 400, { error: 'body must be {"icon":"<choice>","revision":"..."}' }); return true }
|
|
520
|
-
if (typeof body?.icon !== 'string' || typeof body?.revision !== 'string') {
|
|
521
|
-
json(res, 400, { error: 'body must be {"icon":"<choice>","revision":"..."}' })
|
|
522
|
-
return true
|
|
523
|
-
}
|
|
524
|
-
try { json(res, 200, { ok: true, gateway: writeGatewayIcon(body.icon, body.revision) }) }
|
|
525
|
-
catch (e) { json(res, (e as any).status ?? 400, { error: (e as Error).message }) }
|
|
526
|
-
return true
|
|
527
|
-
}
|
|
528
|
-
const icon = path.match(/^\/projects\/([^/]+)\/icon$/)
|
|
529
|
-
if (icon && req.method === 'PUT') {
|
|
530
|
-
const projectId = decodeURIComponent(icon[1])
|
|
531
|
-
const entry = (await reconcileNow()).find((p) => p.projectId === projectId)
|
|
532
|
-
if (!entry) { json(res, 404, { error: `unknown project '${projectId}' — add it first (POST /projects)` }); return true }
|
|
533
|
-
let body: any
|
|
534
|
-
try { body = JSON.parse(await readBody(req) || '{}') }
|
|
535
|
-
catch { json(res, 400, { error: 'body must be {"icon":"<choice>","revision":"..."}' }); return true }
|
|
536
|
-
if (typeof body?.icon !== 'string' || typeof body?.revision !== 'string') {
|
|
537
|
-
json(res, 400, { error: 'body must be {"icon":"<choice>","revision":"..."}' })
|
|
538
|
-
return true
|
|
539
|
-
}
|
|
540
|
-
try { json(res, 200, { ok: true, ...writeProjectIcon(entry.root, body.icon, body.revision) }) }
|
|
541
|
-
catch (e) { json(res, (e as any).status ?? 400, { error: (e as Error).message }) }
|
|
542
|
-
return true
|
|
543
|
-
}
|
|
544
|
-
const config = path.match(/^\/projects\/([^/]+)\/config$/)
|
|
545
|
-
if (config && (req.method === 'GET' || req.method === 'PUT')) {
|
|
546
|
-
const projectId = decodeURIComponent(config[1])
|
|
547
|
-
const entry = (await reconcileNow()).find((p) => p.projectId === projectId)
|
|
548
|
-
if (!entry) { json(res, 404, { error: `unknown project '${projectId}' — add it first (POST /projects)` }); return true }
|
|
549
|
-
if (req.method === 'GET') {
|
|
550
|
-
try { json(res, 200, readProjectConfig(entry.root)) }
|
|
551
|
-
catch (e) { json(res, 500, { error: `cannot read spexcode.json: ${(e as Error).message}` }) }
|
|
552
|
-
return true
|
|
553
|
-
}
|
|
554
|
-
let body: any
|
|
555
|
-
try { body = JSON.parse(await readBody(req) || '{}') }
|
|
556
|
-
catch { json(res, 400, { error: 'body must be {"content":"...","revision":"..."}' }); return true }
|
|
557
|
-
if (typeof body?.content !== 'string' || typeof body?.revision !== 'string') {
|
|
558
|
-
json(res, 400, { error: 'body must be {"content":"...","revision":"..."}' })
|
|
559
|
-
return true
|
|
560
|
-
}
|
|
561
|
-
try { json(res, 200, { ok: true, ...writeProjectConfig(entry.root, body.content, body.revision) }) }
|
|
562
|
-
catch (e) { json(res, (e as any).status ?? 500, { error: (e as Error).message }) }
|
|
563
|
-
return true
|
|
564
|
-
}
|
|
565
|
-
const op = path.match(/^\/projects\/([^/]+)\/(init|doctor|serve)$/)
|
|
566
|
-
if (op && req.method === 'POST') {
|
|
567
|
-
const projectId = decodeURIComponent(op[1])
|
|
568
|
-
const entry = (await reconcileNow()).find((p) => p.projectId === projectId)
|
|
569
|
-
if (!entry) { json(res, 404, { error: `unknown project '${projectId}' — add it first (POST /projects)` }); return true }
|
|
570
|
-
if (op[2] === 'serve') {
|
|
571
|
-
if (entry.online) { json(res, 409, { error: `backend already online at ${entry.url}`, project: entry }); return true }
|
|
572
|
-
try { json(res, 200, { ok: true, project: await startBackend(entry.root) }) }
|
|
573
|
-
catch (e) { json(res, 502, { error: (e as Error).message }) }
|
|
574
|
-
return true
|
|
575
|
-
}
|
|
576
|
-
let body: any = {}
|
|
577
|
-
try { body = JSON.parse(await readBody(req) || '{}') } catch { /* malformed body → defaults */ }
|
|
578
|
-
const args = op[2] === 'init'
|
|
579
|
-
? ['init', ...(body?.harness ? ['--harness', String(body.harness)] : []), ...(body?.preset ? ['--preset', String(body.preset)] : [])]
|
|
580
|
-
: ['doctor']
|
|
581
|
-
const r = await runSpex(entry.root, args)
|
|
582
|
-
json(res, 200, { ok: r.code === 0, code: r.code, output: r.output })
|
|
583
|
-
return true
|
|
584
|
-
}
|
|
585
|
-
return false
|
|
586
|
-
},
|
|
587
|
-
|
|
588
|
-
fallback: (req, res, path) => serveStatic(req, res, distDir, path),
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const server = startHubGateway({ port: opts.port, host: opts.host ?? '127.0.0.1', tls: opts.tls ?? null, extensions })
|
|
592
|
-
|
|
593
|
-
// continuous reconciliation: the stream stays live without a client-side poll and the list the hub
|
|
594
|
-
// serves stays fresh. Heartbeat comments keep intermediaries from timing the stream out. Both timers
|
|
595
|
-
// unref'd — the SERVER holds the process open, not the loops.
|
|
596
|
-
const loop = setInterval(() => void tick().catch((e) => console.error(`[host] reconcile failed: ${(e as Error).message}`)), 2500)
|
|
597
|
-
loop.unref()
|
|
598
|
-
const ping = setInterval(() => { for (const c of sseClients) c.write(': ping\n\n') }, 10_000)
|
|
599
|
-
ping.unref()
|
|
600
|
-
|
|
601
|
-
return {
|
|
602
|
-
server,
|
|
603
|
-
close: () => new Promise<void>((resolve) => {
|
|
604
|
-
clearInterval(loop); clearInterval(ping)
|
|
605
|
-
for (const c of sseClients) c.destroy()
|
|
606
|
-
sseClients.clear()
|
|
607
|
-
server.close(() => resolve())
|
|
608
|
-
server.closeAllConnections?.()
|
|
609
|
-
}),
|
|
610
|
-
}
|
|
611
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export type IdentityShape = { tag: string; [key: string]: string | number }
|
|
2
|
-
export type IdentityPreset = { id: string; label: string; bg: string; fg: string; shapes: IdentityShape[] }
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_PROJECT_ICON: string
|
|
5
|
-
export const DEFAULT_GATEWAY_ICON: string
|
|
6
|
-
export const IDENTITY_PRESETS: readonly IdentityPreset[]
|
|
7
|
-
export const IDENTITY_PRESET_IDS: readonly string[]
|
|
8
|
-
export function resolvedIdentityIcon(value: unknown, fallback?: string): string
|
|
9
|
-
export function identityPreset(value: unknown): IdentityPreset | null
|
|
10
|
-
export function isIconifyIcon(value: unknown): boolean
|
|
11
|
-
export function requireIdentityChoice(value: unknown): string
|
|
12
|
-
export function identitySvg(value: unknown, fallback?: string): string
|
|
13
|
-
export function identityFaviconHref(value: unknown, fallback?: string): string
|