@blackbelt-technology/pi-agent-dashboard 0.2.9 โ 0.4.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/AGENTS.md +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/prompt-expander.ts +25 -4
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/cors.test.ts +34 -2
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +108 -9
- package/packages/server/src/browser-gateway.ts +16 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +363 -26
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts โ binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +59 -3
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +93 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +71 -49
- package/packages/shared/src/{tool-resolver.ts โ platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
- package/packages/shared/src/types.ts +7 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import fastifyStatic from "@fastify/static";
|
|
6
6
|
import cors from "@fastify/cors";
|
|
7
|
+
import compress from "@fastify/compress";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import os from "node:os";
|
|
@@ -29,7 +30,7 @@ import { createIdleTimer } from "./idle-timer.js";
|
|
|
29
30
|
import { discoverAndBroadcastSessions } from "./session-bootstrap.js";
|
|
30
31
|
import { scanAllSessions } from "./session-scanner.js";
|
|
31
32
|
import { needsMigration, runMigration } from "./migrate-persistence.js";
|
|
32
|
-
import { detectZrokBinary, cleanupStaleZrok, createTunnel, deleteTunnel } from "./tunnel.js";
|
|
33
|
+
import { detectZrokBinary, cleanupStaleZrok, createTunnel, deleteTunnel, scavengeOrphanZrokProcesses, getTunnelUrl } from "./tunnel.js";
|
|
33
34
|
import { registerAuthPlugin, validateWsUpgrade } from "./auth-plugin.js";
|
|
34
35
|
import { findBundledExtension, registerBridgeExtension } from "@blackbelt-technology/pi-dashboard-shared/bridge-register.js";
|
|
35
36
|
import { createNetworkGuard, isLoopback, isBypassedHost } from "./localhost-guard.js";
|
|
@@ -42,9 +43,20 @@ import { registerOpenSpecRoutes } from "./routes/openspec-routes.js";
|
|
|
42
43
|
import { registerSystemRoutes } from "./routes/system-routes.js";
|
|
43
44
|
import { registerProviderAuthRoutes } from "./routes/provider-auth-routes.js";
|
|
44
45
|
import { registerPackageRoutes } from "./routes/package-routes.js";
|
|
46
|
+
import { registerRecommendedRoutes, invalidateRecommendedCache } from "./routes/recommended-routes.js";
|
|
47
|
+
import { registerPiCoreRoutes } from "./routes/pi-core-routes.js";
|
|
48
|
+
import { PiCoreChecker } from "./pi-core-checker.js";
|
|
49
|
+
import { PiCoreUpdater } from "./pi-core-updater.js";
|
|
50
|
+
import { registerToolRoutes } from "./routes/tool-routes.js";
|
|
51
|
+
import { registerBootstrapRoutes } from "./routes/bootstrap-routes.js";
|
|
52
|
+
import { createBootstrapState, type BootstrapStateStore } from "./bootstrap-state.js";
|
|
53
|
+
import { createBootstrapQueue } from "./bootstrap-queue.js";
|
|
54
|
+
import { bootstrapInstall } from "@blackbelt-technology/pi-dashboard-shared/bootstrap-install.js";
|
|
55
|
+
import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
|
|
45
56
|
import { registerProviderRoutes } from "./routes/provider-routes.js";
|
|
46
57
|
import { PackageManagerWrapper } from "./package-manager-wrapper.js";
|
|
47
58
|
import { createEditorManager, type EditorManager } from "./editor-manager.js";
|
|
59
|
+
import { createEditorPidRegistry } from "./editor-pid-registry.js";
|
|
48
60
|
import { registerEditorRoutes } from "./routes/editor-routes.js";
|
|
49
61
|
import { registerKnownServersRoutes } from "./routes/known-servers-routes.js";
|
|
50
62
|
import { registerEditorProxy, handleEditorUpgrade } from "./editor-proxy.js";
|
|
@@ -67,6 +79,8 @@ export interface ServerConfig {
|
|
|
67
79
|
maxWsBufferBytes?: number;
|
|
68
80
|
/** Editor (code-server) config */
|
|
69
81
|
editor: import("@blackbelt-technology/pi-dashboard-shared/config.js").EditorConfig;
|
|
82
|
+
/** OpenSpec polling config (interval, concurrency, change detection, jitter) */
|
|
83
|
+
openspec?: import("@blackbelt-technology/pi-dashboard-shared/config.js").OpenSpecPollConfig;
|
|
70
84
|
/** Merged trusted networks from config */
|
|
71
85
|
resolvedTrustedNetworks?: string[];
|
|
72
86
|
/** CORS allowed origins from config */
|
|
@@ -79,14 +93,37 @@ export interface DashboardServer {
|
|
|
79
93
|
sessionManager: SessionManager;
|
|
80
94
|
eventStore: EventStore;
|
|
81
95
|
browserGateway: BrowserGateway;
|
|
96
|
+
/**
|
|
97
|
+
* Bootstrap state store. Exposed so the CLI can flip status during
|
|
98
|
+
* degraded-mode first-run (`pi-dashboard` without pi installed) and
|
|
99
|
+
* so the REST handler for `/api/bootstrap/upgrade-pi` can orchestrate
|
|
100
|
+
* async installs without reaching back through closures.
|
|
101
|
+
* See change: unified-bootstrap-install.
|
|
102
|
+
*/
|
|
103
|
+
bootstrapState: BootstrapStateStore;
|
|
104
|
+
/** Resolved HTTP port after start() (useful for port:0 in tests). Returns null if not listening. */
|
|
105
|
+
httpPort(): number | null;
|
|
106
|
+
/** Resolved pi gateway port after start(). Returns null if not listening. */
|
|
107
|
+
piPort(): number | null;
|
|
82
108
|
}
|
|
83
109
|
|
|
84
110
|
export async function createServer(config: ServerConfig): Promise<DashboardServer> {
|
|
85
111
|
// Ensure bridge extension is registered in pi's global settings
|
|
86
112
|
// (needed for bundled installs where pi can't discover it from package.json)
|
|
113
|
+
//
|
|
114
|
+
// __serverDir = <repo>/packages/server/src
|
|
115
|
+
// baseDir MUST be <repo>/ so findBundledExtension resolves
|
|
116
|
+
// <repo>/packages/extension. Three levels up, not two.
|
|
87
117
|
const __serverDir = path.dirname(fileURLToPath(import.meta.url));
|
|
88
|
-
const extPath = findBundledExtension(path.resolve(__serverDir, "..", ".."));
|
|
89
|
-
if (extPath)
|
|
118
|
+
const extPath = findBundledExtension(path.resolve(__serverDir, "..", "..", ".."));
|
|
119
|
+
if (extPath) {
|
|
120
|
+
registerBridgeExtension(extPath);
|
|
121
|
+
console.log(`[dashboard] Bridge extension registered: ${extPath}`);
|
|
122
|
+
} else {
|
|
123
|
+
console.warn(`[dashboard] Bridge extension NOT found (searched from ${__serverDir}). ` +
|
|
124
|
+
`Sessions will spawn but never connect to the gateway. ` +
|
|
125
|
+
`Manually add the extension path to ~/.pi/agent/settings.json packages[] as a workaround.`);
|
|
126
|
+
}
|
|
90
127
|
|
|
91
128
|
// Run migration from sessions.json + state.json if needed
|
|
92
129
|
if (needsMigration()) {
|
|
@@ -151,7 +188,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
151
188
|
knownSessionIds.add(s.id);
|
|
152
189
|
}
|
|
153
190
|
|
|
154
|
-
const directoryService = createDirectoryService(preferencesStore, sessionManager);
|
|
191
|
+
const directoryService = createDirectoryService(preferencesStore, sessionManager, config.openspec);
|
|
155
192
|
|
|
156
193
|
// mDNS peer discovery state
|
|
157
194
|
let mdnsBrowser: DashboardBrowser | null = null;
|
|
@@ -190,9 +227,11 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
190
227
|
|
|
191
228
|
// Create editor manager for code-server instances
|
|
192
229
|
const editorDetection = detectCodeServerBinary(config.editor);
|
|
230
|
+
const editorPidRegistry = createEditorPidRegistry();
|
|
193
231
|
const editorManager = createEditorManager({
|
|
194
232
|
config: config.editor,
|
|
195
233
|
detection: editorDetection,
|
|
234
|
+
pidRegistry: editorPidRegistry,
|
|
196
235
|
onStatusChange: (cwd, id, status) => {
|
|
197
236
|
browserGateway.broadcastToAll({ type: "editor_status", cwd, id, status });
|
|
198
237
|
},
|
|
@@ -243,23 +282,62 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
243
282
|
connectionTimeout: 10_000,
|
|
244
283
|
});
|
|
245
284
|
|
|
246
|
-
//
|
|
285
|
+
// Compression: gzip/deflate for HTTP responses. Critical for large client
|
|
286
|
+
// bundles (~3 MB JS) served over tunnels like zrok which abort big transfers.
|
|
287
|
+
// Brotli is intentionally disabled โ zrok's free public proxy has been
|
|
288
|
+
// observed to truncate/stream-reset `content-encoding: br` responses under
|
|
289
|
+
// parallel browser load (curl succeeds, Chrome reports ERR_ABORTED 500).
|
|
290
|
+
// gzip is universally supported and round-trips cleanly through zrok.
|
|
291
|
+
// threshold=1024 skips tiny responses; global=true compresses all routes.
|
|
292
|
+
await fastify.register(compress, {
|
|
293
|
+
global: true,
|
|
294
|
+
threshold: 1024,
|
|
295
|
+
encodings: ["gzip", "deflate"],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// CORS: allow localhost, the active zrok tunnel URL, any *.share.zrok.io
|
|
299
|
+
// host (so tunnel URL rotation doesn't break loads), and configured origins.
|
|
300
|
+
//
|
|
301
|
+
// Two critical correctness notes:
|
|
302
|
+
// (1) Vite emits `<script type="module" crossorigin>` tags, which browsers
|
|
303
|
+
// always request in CORS mode โ even when same-origin. If the server
|
|
304
|
+
// doesn't emit `Access-Control-Allow-Origin` for the request's own
|
|
305
|
+
// origin, the browser aborts the script with ERR_ABORTED 500. So when
|
|
306
|
+
// accessed via a tunnel URL, that URL MUST be in the allow list or all
|
|
307
|
+
// asset loads fail in the browser (while curl โ which sends no Origin
|
|
308
|
+
// header โ works fine). This is the exact failure mode that looked
|
|
309
|
+
// like a zrok problem for hours of debugging.
|
|
310
|
+
// (2) On origin mismatch, return `cb(null, false)` (no CORS headers) rather
|
|
311
|
+
// than `cb(new Error(โฆ), false)`. The latter causes @fastify/cors to
|
|
312
|
+
// surface the error as HTTP 500 on every asset โ far worse than
|
|
313
|
+
// silently omitting CORS headers and letting the browser enforce its
|
|
314
|
+
// own same-origin policy.
|
|
247
315
|
const corsAllowedOrigins = config.corsAllowedOrigins ?? [];
|
|
248
316
|
await fastify.register(cors, {
|
|
249
317
|
origin: (origin, cb) => {
|
|
250
|
-
// Same-origin (no Origin header) โ always allow
|
|
318
|
+
// Same-origin navigation (no Origin header) โ always allow.
|
|
251
319
|
if (!origin) return cb(null, true);
|
|
252
|
-
// Localhost / 127.0.0.1 / [::1] โ any port
|
|
253
320
|
try {
|
|
254
321
|
const u = new URL(origin);
|
|
255
322
|
const host = u.hostname;
|
|
323
|
+
// Loopback โ any port.
|
|
256
324
|
if (host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1") {
|
|
257
325
|
return cb(null, true);
|
|
258
326
|
}
|
|
259
|
-
|
|
260
|
-
|
|
327
|
+
// Active zrok tunnel URL โ checked dynamically so URL rotation is
|
|
328
|
+
// picked up without a server restart.
|
|
329
|
+
const tunnelUrl = getTunnelUrl();
|
|
330
|
+
if (tunnelUrl && origin === tunnelUrl) return cb(null, true);
|
|
331
|
+
// Any *.share.zrok.io host โ covers the brief window between a new
|
|
332
|
+
// reservation being created and the in-memory `activeTunnelUrl`
|
|
333
|
+
// being populated, plus any other zrok share the user points at us.
|
|
334
|
+
if (host.endsWith(".share.zrok.io")) return cb(null, true);
|
|
335
|
+
} catch { /* ignore URL parse errors */ }
|
|
336
|
+
// Explicitly configured origins.
|
|
261
337
|
if (corsAllowedOrigins.includes(origin)) return cb(null, true);
|
|
262
|
-
|
|
338
|
+
// Unknown cross-origin request โ don't emit CORS headers, but don't
|
|
339
|
+
// 500 either. Browser will block the request for us.
|
|
340
|
+
cb(null, false);
|
|
263
341
|
},
|
|
264
342
|
credentials: true,
|
|
265
343
|
});
|
|
@@ -278,6 +356,33 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
278
356
|
fastify.get("/auth/status", async () => ({ authenticated: true, authEnabled: false }));
|
|
279
357
|
}
|
|
280
358
|
|
|
359
|
+
// โโ Bootstrap state + queue โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
360
|
+
// Declared here (before session-api registration) so the session
|
|
361
|
+
// routes can gate spawn operations on bootstrap status.
|
|
362
|
+
// See change: unified-bootstrap-install.
|
|
363
|
+
const bootstrapState = createBootstrapState();
|
|
364
|
+
const bootstrapQueue = createBootstrapQueue();
|
|
365
|
+
let lastBootstrapStatus: "ready" | "installing" | "failed" = "ready";
|
|
366
|
+
const unsubscribeBootstrap = bootstrapState.subscribe((snapshot) => {
|
|
367
|
+
browserGateway.broadcastToAll({
|
|
368
|
+
type: "bootstrap_status_update",
|
|
369
|
+
state: snapshot,
|
|
370
|
+
});
|
|
371
|
+
// Flush queued pi-dependent operations on ready transition.
|
|
372
|
+
if (lastBootstrapStatus !== "ready" && snapshot.status === "ready") {
|
|
373
|
+
void bootstrapQueue.flushAll();
|
|
374
|
+
}
|
|
375
|
+
lastBootstrapStatus = snapshot.status;
|
|
376
|
+
});
|
|
377
|
+
const unsubscribeQueueComplete = bootstrapQueue.onTicketComplete((evt) => {
|
|
378
|
+
browserGateway.broadcastToAll({
|
|
379
|
+
type: "bootstrap_ticket_complete",
|
|
380
|
+
ticketId: evt.ticketId,
|
|
381
|
+
success: evt.success,
|
|
382
|
+
error: evt.error,
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
281
386
|
// Session control REST API (wraps WebSocket-only operations)
|
|
282
387
|
registerSessionApi(fastify, {
|
|
283
388
|
sessionManager,
|
|
@@ -285,6 +390,8 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
285
390
|
browserGateway,
|
|
286
391
|
pendingForkRegistry,
|
|
287
392
|
pendingDashboardSpawns,
|
|
393
|
+
bootstrapState,
|
|
394
|
+
bootstrapQueue,
|
|
288
395
|
});
|
|
289
396
|
|
|
290
397
|
// Register route modules
|
|
@@ -294,8 +401,123 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
294
401
|
registerSessionRoutes(fastify, { sessionManager, eventStore, networkGuard });
|
|
295
402
|
registerGitRoutes(fastify, { networkGuard });
|
|
296
403
|
registerFileRoutes(fastify, { sessionManager, preferencesStore, networkGuard });
|
|
297
|
-
registerOpenSpecRoutes(fastify, {
|
|
298
|
-
|
|
404
|
+
registerOpenSpecRoutes(fastify, {
|
|
405
|
+
sessionManager,
|
|
406
|
+
preferencesStore,
|
|
407
|
+
directoryService,
|
|
408
|
+
networkGuard,
|
|
409
|
+
bootstrapState,
|
|
410
|
+
onOpenSpecChanged: (cwd) => {
|
|
411
|
+
const data = directoryService.getOpenSpecData(cwd);
|
|
412
|
+
if (data) browserGateway.broadcastToAll({ type: "openspec_update", cwd, data });
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
registerSystemRoutes(fastify, { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version: pkgVersion, directoryService });
|
|
416
|
+
registerToolRoutes(fastify, { registry: getDefaultRegistry(), networkGuard });
|
|
417
|
+
|
|
418
|
+
// โโ Bootstrap REST routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
419
|
+
// The routes module is registered here; state + queue are declared
|
|
420
|
+
// above (before session-api) so session routes can see them.
|
|
421
|
+
registerBootstrapRoutes(fastify, {
|
|
422
|
+
bootstrapState,
|
|
423
|
+
networkGuard,
|
|
424
|
+
triggerUpgradePi: async () => {
|
|
425
|
+
const packages = ["@mariozechner/pi-coding-agent"];
|
|
426
|
+
bootstrapState.setLastInstallPackages(packages);
|
|
427
|
+
bootstrapState.set({
|
|
428
|
+
status: "installing",
|
|
429
|
+
progress: { step: "@mariozechner/pi-coding-agent", output: "starting upgradeโฆ" },
|
|
430
|
+
error: undefined,
|
|
431
|
+
});
|
|
432
|
+
try {
|
|
433
|
+
const res = await bootstrapInstall({
|
|
434
|
+
packages,
|
|
435
|
+
progress: (p) => {
|
|
436
|
+
bootstrapState.set({
|
|
437
|
+
progress: { step: p.step, output: p.output },
|
|
438
|
+
});
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
if (res.ok) {
|
|
442
|
+
bootstrapState.set({
|
|
443
|
+
status: "ready",
|
|
444
|
+
progress: undefined,
|
|
445
|
+
error: undefined,
|
|
446
|
+
});
|
|
447
|
+
// Broadcast /reload to connected sessions so they pick up the
|
|
448
|
+
// new pi version. Mirrors the pi-core update pattern above.
|
|
449
|
+
const connectedIds = piGateway.getConnectedSessionIds();
|
|
450
|
+
for (const sid of connectedIds) {
|
|
451
|
+
const session = sessionManager.get(sid);
|
|
452
|
+
if (session && session.status !== "ended") {
|
|
453
|
+
piGateway.sendToSession(sid, {
|
|
454
|
+
type: "send_prompt",
|
|
455
|
+
sessionId: sid,
|
|
456
|
+
text: "/reload",
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
bootstrapState.set({
|
|
462
|
+
status: "failed",
|
|
463
|
+
error: { message: res.error },
|
|
464
|
+
progress: undefined,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
} catch (err) {
|
|
468
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
469
|
+
bootstrapState.set({
|
|
470
|
+
status: "failed",
|
|
471
|
+
error: { message },
|
|
472
|
+
progress: undefined,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
triggerRetry: async () => {
|
|
477
|
+
// Retry re-runs the EXACT package set from the last failed install.
|
|
478
|
+
// Falls back to the default first-run set if no prior install was
|
|
479
|
+
// recorded (edge case: manual retry before any install attempt).
|
|
480
|
+
const prev = bootstrapState.getLastInstallPackages();
|
|
481
|
+
const packages = prev.length > 0
|
|
482
|
+
? prev
|
|
483
|
+
: ["@mariozechner/pi-coding-agent", "@fission-ai/openspec", "tsx"];
|
|
484
|
+
bootstrapState.set({
|
|
485
|
+
status: "installing",
|
|
486
|
+
progress: { step: "retry", output: `restarting install (${packages.length} pkg${packages.length === 1 ? "" : "s"})โฆ` },
|
|
487
|
+
error: undefined,
|
|
488
|
+
});
|
|
489
|
+
try {
|
|
490
|
+
const res = await bootstrapInstall({
|
|
491
|
+
packages,
|
|
492
|
+
progress: (p) => {
|
|
493
|
+
bootstrapState.set({
|
|
494
|
+
progress: { step: p.step, output: p.output },
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
if (res.ok) {
|
|
499
|
+
bootstrapState.set({
|
|
500
|
+
status: "ready",
|
|
501
|
+
progress: undefined,
|
|
502
|
+
error: undefined,
|
|
503
|
+
});
|
|
504
|
+
} else {
|
|
505
|
+
bootstrapState.set({
|
|
506
|
+
status: "failed",
|
|
507
|
+
error: { message: res.error },
|
|
508
|
+
progress: undefined,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
} catch (err) {
|
|
512
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
513
|
+
bootstrapState.set({
|
|
514
|
+
status: "failed",
|
|
515
|
+
error: { message },
|
|
516
|
+
progress: undefined,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
});
|
|
299
521
|
// Package management
|
|
300
522
|
const packageManagerWrapper = new PackageManagerWrapper();
|
|
301
523
|
|
|
@@ -304,7 +526,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
304
526
|
browserGateway.broadcastToAll({ type: "package_progress", operationId, event } as any);
|
|
305
527
|
});
|
|
306
528
|
|
|
307
|
-
// On completion: broadcast to browsers
|
|
529
|
+
// On completion: broadcast to browsers + invalidate the recommended cache
|
|
308
530
|
packageManagerWrapper.setCompleteListener((result) => {
|
|
309
531
|
browserGateway.broadcastToAll({
|
|
310
532
|
type: "package_operation_complete",
|
|
@@ -314,8 +536,10 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
314
536
|
scope: result.scope,
|
|
315
537
|
success: result.success,
|
|
316
538
|
error: result.error,
|
|
539
|
+
diagnostics: result.diagnostics,
|
|
317
540
|
sessionsReloaded: (result as any).sessionsReloaded,
|
|
318
541
|
} as any);
|
|
542
|
+
if (result.success) invalidateRecommendedCache();
|
|
319
543
|
});
|
|
320
544
|
|
|
321
545
|
// Reload all active sessions after a successful package operation
|
|
@@ -337,26 +561,105 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
337
561
|
});
|
|
338
562
|
|
|
339
563
|
registerPackageRoutes(fastify, { packageManagerWrapper });
|
|
564
|
+
registerRecommendedRoutes(fastify, { packageManagerWrapper });
|
|
565
|
+
|
|
566
|
+
// Pi core version check + update (complements the extension package manager).
|
|
567
|
+
const piCoreChecker = new PiCoreChecker();
|
|
568
|
+
const piCoreUpdater = new PiCoreUpdater({
|
|
569
|
+
packageManagerWrapper,
|
|
570
|
+
onAllComplete: async () => {
|
|
571
|
+
const connectedIds = piGateway.getConnectedSessionIds();
|
|
572
|
+
let count = 0;
|
|
573
|
+
for (const sid of connectedIds) {
|
|
574
|
+
const session = sessionManager.get(sid);
|
|
575
|
+
if (session && session.status !== "ended") {
|
|
576
|
+
piGateway.sendToSession(sid, {
|
|
577
|
+
type: "send_prompt",
|
|
578
|
+
sessionId: sid,
|
|
579
|
+
text: "/reload",
|
|
580
|
+
});
|
|
581
|
+
count++;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return count;
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
piCoreUpdater.setProgressListener((event) => {
|
|
588
|
+
browserGateway.broadcastToAll({
|
|
589
|
+
type: "pi_core_update_progress",
|
|
590
|
+
name: event.name,
|
|
591
|
+
phase: event.phase,
|
|
592
|
+
message: event.message,
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
registerPiCoreRoutes(fastify, {
|
|
596
|
+
piCoreChecker,
|
|
597
|
+
piCoreUpdater,
|
|
598
|
+
bootstrapState,
|
|
599
|
+
onUpdateComplete: (payload) => {
|
|
600
|
+
browserGateway.broadcastToAll({
|
|
601
|
+
type: "pi_core_update_complete",
|
|
602
|
+
results: payload.results,
|
|
603
|
+
sessionsReloaded: payload.sessionsReloaded,
|
|
604
|
+
});
|
|
605
|
+
},
|
|
606
|
+
});
|
|
340
607
|
|
|
341
|
-
//
|
|
608
|
+
// Warm pi-coding-agent module import + DefaultPackageManager instances
|
|
609
|
+
// on startup so the first user request to /api/packages/* doesn't pay
|
|
610
|
+
// the 3-5s cold-load cost. Runs in background; errors are swallowed
|
|
611
|
+
// (user-visible flow surfaces any real problem with the full diagnostic
|
|
612
|
+
// trail via the OperationResult.diagnostics field).
|
|
613
|
+
// See change: consolidate-tool-resolution.
|
|
614
|
+
void Promise.allSettled([
|
|
615
|
+
packageManagerWrapper.listInstalled("global"),
|
|
616
|
+
packageManagerWrapper.listInstalled("local"),
|
|
617
|
+
]);
|
|
618
|
+
|
|
619
|
+
// Editor (code-server) routes and proxy.
|
|
620
|
+
// NOTE: routes are *registered* here but cannot dispatch until fastify.listen runs
|
|
621
|
+
// inside server.start(). The orphan sweep in editorPidRegistry.cleanupOrphans()
|
|
622
|
+
// runs at the top of server.start() BEFORE fastify.listen, so any
|
|
623
|
+
// POST /api/editor/start call is guaranteed to see a post-sweep clean state.
|
|
342
624
|
registerEditorRoutes(fastify, editorManager, { networkGuard });
|
|
343
625
|
registerEditorProxy(fastify, editorManager);
|
|
344
626
|
|
|
345
|
-
registerProviderAuthRoutes(fastify, { piGateway });
|
|
627
|
+
registerProviderAuthRoutes(fastify, { piGateway, browserGateway });
|
|
346
628
|
registerKnownServersRoutes(fastify, { networkGuard, getPeerServers: () => peerServers });
|
|
347
|
-
registerProviderRoutes(fastify, { networkGuard });
|
|
348
|
-
|
|
349
|
-
// Serve static files / SPA fallback
|
|
350
|
-
//
|
|
629
|
+
registerProviderRoutes(fastify, { networkGuard, piGateway, browserGateway });
|
|
630
|
+
|
|
631
|
+
// Serve static files / SPA fallback.
|
|
632
|
+
//
|
|
633
|
+
// Resolution strategies, in order:
|
|
634
|
+
// 1. Node module resolver โ works in ANY install layout
|
|
635
|
+
// (flat `node_modules/`, scoped, nested, pnpm, whatever).
|
|
636
|
+
// 2. Sibling-to-server in the installed @scope layout.
|
|
637
|
+
// 3. Monorepo workspace sibling.
|
|
638
|
+
// 4. Legacy dist/client.
|
|
639
|
+
//
|
|
640
|
+
// Same class of bug as commits 40a1319 (bridge auto-registration)
|
|
641
|
+
// and e11f5eb (server-launcher.ts resolve): sibling-path arithmetic
|
|
642
|
+
// that works in the dev repo silently returns wrong paths in the
|
|
643
|
+
// installed node_modules layout. require.resolve identifies packages
|
|
644
|
+
// by name, which is the only canonical identity across layouts.
|
|
351
645
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
352
|
-
const clientSearchPaths = [
|
|
353
|
-
|
|
354
|
-
|
|
646
|
+
const clientSearchPaths: string[] = [];
|
|
647
|
+
try {
|
|
648
|
+
const webPkgJson = createRequire(import.meta.url).resolve("@blackbelt-technology/pi-dashboard-web/package.json");
|
|
649
|
+
clientSearchPaths.push(path.join(path.dirname(webPkgJson), "dist"));
|
|
650
|
+
} catch {
|
|
651
|
+
// Web package not resolvable โ fall through to path-based search.
|
|
652
|
+
}
|
|
653
|
+
clientSearchPaths.push(
|
|
654
|
+
// Installed as scoped sibling of server
|
|
655
|
+
path.join(__dirname, "..", "..", "pi-dashboard-web", "dist"),
|
|
656
|
+
// Installed in a parent node_modules (hoisted)
|
|
657
|
+
path.join(__dirname, "..", "..", "..", "@blackbelt-technology", "pi-dashboard-web", "dist"),
|
|
355
658
|
// Monorepo workspace sibling
|
|
356
659
|
path.join(__dirname, "../../client/dist"),
|
|
357
660
|
// Legacy path
|
|
358
661
|
path.join(__dirname, "../../dist/client"),
|
|
359
|
-
|
|
662
|
+
);
|
|
360
663
|
const clientDir = clientSearchPaths.find(p => existsSync(path.join(p, "index.html"))) ?? "";
|
|
361
664
|
const hasProductionBuild = !!clientDir;
|
|
362
665
|
if (!hasProductionBuild) {
|
|
@@ -371,6 +674,13 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
371
674
|
await fastify.register(fastifyStatic, {
|
|
372
675
|
root: clientDir,
|
|
373
676
|
prefix: "/",
|
|
677
|
+
// Serve pre-compressed sibling files (assets/foo.js.gz alongside foo.js)
|
|
678
|
+
// directly when the client accepts gzip. This gives every compressed
|
|
679
|
+
// response a stable Content-Length header โ dynamic compression via
|
|
680
|
+
// @fastify/compress streams responses without Content-Length, which
|
|
681
|
+
// some HTTP/2 proxy chains (notably zrok free-tier) occasionally
|
|
682
|
+
// stream-reset as ERR_ABORTED 500 in browsers.
|
|
683
|
+
preCompressed: true,
|
|
374
684
|
setHeaders: (res, filePath) => {
|
|
375
685
|
if (filePath.endsWith(".html")) {
|
|
376
686
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -434,11 +744,25 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
434
744
|
sessionManager,
|
|
435
745
|
eventStore,
|
|
436
746
|
browserGateway,
|
|
747
|
+
bootstrapState,
|
|
748
|
+
|
|
749
|
+
httpPort() {
|
|
750
|
+
const addr = fastify.server.address();
|
|
751
|
+
if (addr && typeof addr === "object") return addr.port;
|
|
752
|
+
return null;
|
|
753
|
+
},
|
|
754
|
+
piPort() {
|
|
755
|
+
return piGateway.address();
|
|
756
|
+
},
|
|
437
757
|
|
|
438
758
|
async start() {
|
|
439
759
|
// Clean up orphan headless processes from a previous server instance
|
|
440
760
|
browserGateway.headlessPidRegistry.cleanupOrphans();
|
|
441
761
|
|
|
762
|
+
// Clean up orphan code-server processes from a previous server instance.
|
|
763
|
+
// Runs before fastify.listen, so no editor start request can race with the sweep.
|
|
764
|
+
await editorPidRegistry.cleanupOrphans();
|
|
765
|
+
|
|
442
766
|
piGateway.start(config.piPort);
|
|
443
767
|
|
|
444
768
|
fastify.server.on("upgrade", (request, socket, head) => {
|
|
@@ -501,10 +825,19 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
501
825
|
console.warn(`mDNS browser failed (peer discovery disabled):`, err);
|
|
502
826
|
}
|
|
503
827
|
|
|
828
|
+
// Always sweep leftover zrok processes on startup, even when tunnel is
|
|
829
|
+
// disabled (--no-tunnel). Orphans from a previous run hold reservations
|
|
830
|
+
// on the zrok edge and keep old URLs "alive but broken" until their
|
|
831
|
+
// agents are killed. Scavenge runs unconditionally when the binary is
|
|
832
|
+
// present; the tunnel-creation branch below is gated separately.
|
|
833
|
+
const hasZrok = detectZrokBinary();
|
|
834
|
+
if (hasZrok) {
|
|
835
|
+
cleanupStaleZrok();
|
|
836
|
+
scavengeOrphanZrokProcesses(config.port);
|
|
837
|
+
}
|
|
838
|
+
|
|
504
839
|
if (config.tunnel) {
|
|
505
|
-
const hasZrok = detectZrokBinary();
|
|
506
840
|
if (hasZrok) {
|
|
507
|
-
cleanupStaleZrok();
|
|
508
841
|
const tunnelUrl = await createTunnel(config.port, config.tunnelReservedToken);
|
|
509
842
|
if (tunnelUrl) {
|
|
510
843
|
console.log(`๐ Tunnel: ${tunnelUrl}`);
|
|
@@ -534,7 +867,11 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
534
867
|
preferencesStore.flush();
|
|
535
868
|
preferencesStore.dispose();
|
|
536
869
|
|
|
537
|
-
|
|
870
|
+
unsubscribeBootstrap();
|
|
871
|
+
unsubscribeQueueComplete();
|
|
872
|
+
bootstrapState.dispose();
|
|
873
|
+
bootstrapQueue.clear("server shutting down");
|
|
874
|
+
await deleteTunnel(config.port);
|
|
538
875
|
piGateway.stop();
|
|
539
876
|
for (const client of browserGateway.wss.clients) {
|
|
540
877
|
client.terminate();
|
|
@@ -11,6 +11,8 @@ import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/type
|
|
|
11
11
|
import { spawnPiSession } from "./process-manager.js";
|
|
12
12
|
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
13
13
|
import type { PendingForkRegistry } from "./pending-fork-registry.js";
|
|
14
|
+
import type { BootstrapStateStore } from "./bootstrap-state.js";
|
|
15
|
+
import type { BootstrapQueue } from "./bootstrap-queue.js";
|
|
14
16
|
|
|
15
17
|
export interface SessionApiDeps {
|
|
16
18
|
sessionManager: SessionManager;
|
|
@@ -18,6 +20,13 @@ export interface SessionApiDeps {
|
|
|
18
20
|
browserGateway: BrowserGateway;
|
|
19
21
|
pendingForkRegistry?: PendingForkRegistry;
|
|
20
22
|
pendingDashboardSpawns?: Map<string, number>;
|
|
23
|
+
/**
|
|
24
|
+
* Bootstrap state + queue for degraded-mode gating. When omitted,
|
|
25
|
+
* session operations run normally (legacy behavior for tests that
|
|
26
|
+
* don't exercise the bootstrap flow). See change: unified-bootstrap-install.
|
|
27
|
+
*/
|
|
28
|
+
bootstrapState?: BootstrapStateStore;
|
|
29
|
+
bootstrapQueue?: BootstrapQueue;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
type IdParams = { Params: { id: string } };
|
|
@@ -30,7 +39,54 @@ function getSessionOrFail(sessionManager: SessionManager, id: string): { session
|
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDeps) {
|
|
33
|
-
const { sessionManager, piGateway, browserGateway, pendingForkRegistry, pendingDashboardSpawns } = deps;
|
|
42
|
+
const { sessionManager, piGateway, browserGateway, pendingForkRegistry, pendingDashboardSpawns, bootstrapState, bootstrapQueue } = deps;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gate pi-dependent operations on bootstrap status. Returns:
|
|
46
|
+
* - null when ready (proceed).
|
|
47
|
+
* - `{ code: 202, body: { status: "queued", ticketId } }` when installing;
|
|
48
|
+
* the operation is enqueued and will run once status flips to "ready".
|
|
49
|
+
* - `{ code: 503, body: { error } }` when failed.
|
|
50
|
+
* See change: unified-bootstrap-install ยง5.
|
|
51
|
+
*/
|
|
52
|
+
function gateOrEnqueue<T>(handler: () => Promise<T>):
|
|
53
|
+
| null
|
|
54
|
+
| { code: 202; body: { status: "queued"; ticketId: string } }
|
|
55
|
+
| { code: 503; body: { error: string; bootstrap: "failed" | "version-too-old" } } {
|
|
56
|
+
if (!bootstrapState) return null;
|
|
57
|
+
const snap = bootstrapState.get();
|
|
58
|
+
// Block when pi version is below the configured minimum โ
|
|
59
|
+
// even when status is "ready", a too-old pi must not run sessions.
|
|
60
|
+
// See change: unified-bootstrap-install ยง9.3.
|
|
61
|
+
if (
|
|
62
|
+
snap.status === "ready"
|
|
63
|
+
&& snap.error?.message?.startsWith("pi version ")
|
|
64
|
+
) {
|
|
65
|
+
return {
|
|
66
|
+
code: 503,
|
|
67
|
+
body: { error: snap.error.message, bootstrap: "version-too-old" },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (snap.status === "ready") return null;
|
|
71
|
+
if (snap.status === "installing") {
|
|
72
|
+
if (!bootstrapQueue) {
|
|
73
|
+
return {
|
|
74
|
+
code: 202,
|
|
75
|
+
body: { status: "queued", ticketId: "" },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const ticket = bootstrapQueue.enqueue(handler);
|
|
79
|
+
return {
|
|
80
|
+
code: 202,
|
|
81
|
+
body: { status: "queued", ticketId: ticket.ticketId },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// status === "failed"
|
|
85
|
+
return {
|
|
86
|
+
code: 503,
|
|
87
|
+
body: { error: "pi not installed (bootstrap failed)", bootstrap: "failed" },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
34
90
|
|
|
35
91
|
// POST /api/session/:id/prompt
|
|
36
92
|
fastify.post<IdParams & { Body: { text?: string; images?: any[] } }>(
|
|
@@ -160,14 +216,27 @@ export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDep
|
|
|
160
216
|
reply.code(400);
|
|
161
217
|
return { success: false, error: "cwd is required" } satisfies ApiResponse;
|
|
162
218
|
}
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
219
|
+
|
|
220
|
+
const doSpawn = async () => {
|
|
221
|
+
const config = loadConfig();
|
|
222
|
+
const spawnResult = await spawnPiSession(cwd, { strategy: config.spawnStrategy });
|
|
223
|
+
if (spawnResult.process && spawnResult.pid) {
|
|
224
|
+
browserGateway.headlessPidRegistry.register(spawnResult.pid, cwd, spawnResult.process);
|
|
225
|
+
}
|
|
226
|
+
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
227
|
+
pendingDashboardSpawns?.set(cwd, (pendingDashboardSpawns?.get(cwd) ?? 0) + 1);
|
|
228
|
+
}
|
|
229
|
+
return spawnResult;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Bootstrap gate: if pi isn't ready, queue the spawn and return 202.
|
|
233
|
+
const gate = gateOrEnqueue(doSpawn);
|
|
234
|
+
if (gate) {
|
|
235
|
+
reply.code(gate.code);
|
|
236
|
+
return gate.body;
|
|
170
237
|
}
|
|
238
|
+
|
|
239
|
+
const spawnResult = await doSpawn();
|
|
171
240
|
if (!spawnResult.success) {
|
|
172
241
|
reply.code(500);
|
|
173
242
|
return { success: false, error: spawnResult.message } satisfies ApiResponse;
|