@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.
Files changed (238) hide show
  1. package/AGENTS.md +64 -8
  2. package/README.md +308 -101
  3. package/docs/architecture.md +515 -16
  4. package/package.json +14 -7
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
  7. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  8. package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
  9. package/packages/extension/src/__tests__/git-info.test.ts +67 -55
  10. package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
  11. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  12. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  13. package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
  14. package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
  15. package/packages/extension/src/ask-user-tool.ts +289 -20
  16. package/packages/extension/src/bridge.ts +107 -6
  17. package/packages/extension/src/command-handler.ts +34 -39
  18. package/packages/extension/src/dev-build.ts +1 -1
  19. package/packages/extension/src/git-info.ts +9 -19
  20. package/packages/extension/src/pi-env.d.ts +1 -0
  21. package/packages/extension/src/process-scanner.ts +72 -38
  22. package/packages/extension/src/prompt-expander.ts +25 -4
  23. package/packages/extension/src/provider-register.ts +304 -16
  24. package/packages/extension/src/server-auto-start.ts +27 -1
  25. package/packages/extension/src/server-launcher.ts +71 -27
  26. package/packages/server/package.json +17 -2
  27. package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
  28. package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
  29. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  30. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  31. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  32. package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
  33. package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
  34. package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
  35. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  36. package/packages/server/src/__tests__/config-api.test.ts +68 -0
  37. package/packages/server/src/__tests__/cors.test.ts +34 -2
  38. package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
  39. package/packages/server/src/__tests__/directory-service.test.ts +234 -8
  40. package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
  41. package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
  42. package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
  43. package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
  44. package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
  45. package/packages/server/src/__tests__/extension-register.test.ts +3 -1
  46. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  47. package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
  48. package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
  49. package/packages/server/src/__tests__/git-operations.test.ts +9 -7
  50. package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
  51. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  52. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  53. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  54. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  55. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  56. package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
  57. package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
  58. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
  59. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
  60. package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
  61. package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
  62. package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
  63. package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
  64. package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
  65. package/packages/server/src/__tests__/process-manager.test.ts +45 -18
  66. package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
  67. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  68. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  69. package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
  70. package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
  71. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  72. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  73. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  74. package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
  75. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
  76. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
  77. package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
  78. package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
  79. package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
  80. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  81. package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
  82. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  83. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  84. package/packages/server/src/__tests__/tunnel.test.ts +103 -6
  85. package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
  86. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  87. package/packages/server/src/bootstrap-queue.ts +130 -0
  88. package/packages/server/src/bootstrap-state.ts +131 -0
  89. package/packages/server/src/browse.ts +108 -9
  90. package/packages/server/src/browser-gateway.ts +16 -3
  91. package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
  92. package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
  93. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  94. package/packages/server/src/cli.ts +256 -32
  95. package/packages/server/src/config-api.ts +16 -0
  96. package/packages/server/src/directory-service.ts +270 -39
  97. package/packages/server/src/editor-detection.ts +12 -9
  98. package/packages/server/src/editor-manager.ts +39 -5
  99. package/packages/server/src/editor-pid-registry.ts +199 -0
  100. package/packages/server/src/editor-registry.ts +22 -25
  101. package/packages/server/src/fix-pty-permissions.ts +44 -0
  102. package/packages/server/src/git-operations.ts +1 -1
  103. package/packages/server/src/headless-pid-registry.ts +16 -20
  104. package/packages/server/src/home-lock-release.ts +72 -0
  105. package/packages/server/src/home-lock.ts +389 -0
  106. package/packages/server/src/node-guard.ts +52 -0
  107. package/packages/server/src/npm-search-proxy.ts +71 -0
  108. package/packages/server/src/openspec-tasks.ts +158 -0
  109. package/packages/server/src/package-manager-wrapper.ts +225 -34
  110. package/packages/server/src/pi-core-checker.ts +290 -0
  111. package/packages/server/src/pi-core-updater.ts +172 -0
  112. package/packages/server/src/pi-gateway.ts +7 -0
  113. package/packages/server/src/pi-resource-scanner.ts +5 -8
  114. package/packages/server/src/pi-version-skew.ts +196 -0
  115. package/packages/server/src/preferences-store.ts +17 -3
  116. package/packages/server/src/process-manager.ts +403 -222
  117. package/packages/server/src/provider-probe.ts +234 -0
  118. package/packages/server/src/restart-helper.ts +130 -0
  119. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  120. package/packages/server/src/routes/file-routes.ts +30 -3
  121. package/packages/server/src/routes/openspec-routes.ts +107 -1
  122. package/packages/server/src/routes/pi-core-routes.ts +140 -0
  123. package/packages/server/src/routes/provider-auth-routes.ts +12 -10
  124. package/packages/server/src/routes/provider-routes.ts +55 -2
  125. package/packages/server/src/routes/recommended-routes.ts +225 -0
  126. package/packages/server/src/routes/system-routes.ts +30 -34
  127. package/packages/server/src/routes/tool-routes.ts +153 -0
  128. package/packages/server/src/server-pid.ts +5 -9
  129. package/packages/server/src/server.ts +363 -26
  130. package/packages/server/src/session-api.ts +77 -8
  131. package/packages/server/src/session-bootstrap.ts +17 -3
  132. package/packages/server/src/session-diff.ts +21 -21
  133. package/packages/server/src/terminal-manager.ts +65 -20
  134. package/packages/server/src/test-env-guard.ts +26 -0
  135. package/packages/server/src/test-support/test-server.ts +63 -0
  136. package/packages/server/src/tunnel.ts +172 -34
  137. package/packages/shared/package.json +10 -3
  138. package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
  139. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  140. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  141. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  142. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  143. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  144. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  145. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  146. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  147. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  148. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  149. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  150. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  151. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  152. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  153. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  154. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  155. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  156. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  157. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  158. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  159. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  160. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  161. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  162. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  163. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  164. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  165. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  166. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  167. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  168. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  169. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  170. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  172. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  173. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  174. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  175. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  176. package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
  177. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  178. package/packages/shared/src/__tests__/config.test.ts +59 -3
  179. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  180. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  181. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  182. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  183. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  184. package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
  185. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  186. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  187. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  188. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  189. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  190. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  191. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  192. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  193. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  194. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  195. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  196. package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
  197. package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
  198. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  199. package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
  200. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  201. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
  202. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  203. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  204. package/packages/shared/src/bootstrap-install.ts +212 -0
  205. package/packages/shared/src/bridge-register.ts +87 -20
  206. package/packages/shared/src/browser-protocol.ts +93 -1
  207. package/packages/shared/src/config.ts +87 -15
  208. package/packages/shared/src/managed-paths.ts +31 -4
  209. package/packages/shared/src/openspec-poller.ts +71 -49
  210. package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
  211. package/packages/shared/src/platform/commands.ts +100 -0
  212. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  213. package/packages/shared/src/platform/exec.ts +220 -0
  214. package/packages/shared/src/platform/git.ts +155 -0
  215. package/packages/shared/src/platform/index.ts +15 -0
  216. package/packages/shared/src/platform/npm.ts +162 -0
  217. package/packages/shared/src/platform/openspec.ts +91 -0
  218. package/packages/shared/src/platform/paths.ts +276 -0
  219. package/packages/shared/src/platform/process-identify.ts +126 -0
  220. package/packages/shared/src/platform/process-scan.ts +94 -0
  221. package/packages/shared/src/platform/process.ts +168 -0
  222. package/packages/shared/src/platform/runner.ts +369 -0
  223. package/packages/shared/src/platform/shell.ts +44 -0
  224. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  225. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  226. package/packages/shared/src/recommended-extensions.ts +196 -0
  227. package/packages/shared/src/resolve-jiti.ts +62 -3
  228. package/packages/shared/src/rest-api.ts +97 -0
  229. package/packages/shared/src/semaphore.ts +83 -0
  230. package/packages/shared/src/source-matching.ts +126 -0
  231. package/packages/shared/src/test-support/setup-home.ts +74 -0
  232. package/packages/shared/src/tool-registry/definitions.ts +342 -0
  233. package/packages/shared/src/tool-registry/index.ts +56 -0
  234. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  235. package/packages/shared/src/tool-registry/registry.ts +262 -0
  236. package/packages/shared/src/tool-registry/strategies.ts +198 -0
  237. package/packages/shared/src/tool-registry/types.ts +180 -0
  238. package/packages/shared/src/types.ts +7 -0
@@ -17,17 +17,42 @@
17
17
  */
18
18
  import { createServer, type ServerConfig } from "./server.js";
19
19
  import { loadConfig, ensureConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
20
- import { spawn } from "node:child_process";
20
+ import { spawn } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
21
21
  import { createRequire } from "node:module";
22
- import { fileURLToPath } from "node:url";
22
+ import { fileURLToPath, pathToFileURL } from "node:url";
23
23
  import fs from "node:fs";
24
+ import os from "node:os";
24
25
  import path from "node:path";
25
- import { readPid, isProcessAlive, removePid, isServerRunning } from "./server-pid.js";
26
+ import { readPid, removePid, isServerRunning } from "./server-pid.js";
27
+ import {
28
+ findPortHolders as platformFindPortHolders,
29
+ isProcessAlive as platformIsProcessAlive,
30
+ killProcess as platformKillProcess,
31
+ parseNetstatListeners as platformParseNetstatListeners,
32
+ } from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
33
+
34
+ // Re-exports for back-compat — other modules / tests may import these from cli.
35
+ export const parseNetstatListeners = platformParseNetstatListeners;
36
+ export function findPortHolders(
37
+ port: number,
38
+ execImpl?: (cmd: string, opts: { encoding: "utf-8" }) => string,
39
+ ): number[] {
40
+ return platformFindPortHolders(port, execImpl ? { exec: execImpl } : undefined);
41
+ }
26
42
  import { isDashboardRunning } from "@blackbelt-technology/pi-dashboard-shared/server-identity.js";
27
43
  import { discoverDashboard } from "@blackbelt-technology/pi-dashboard-shared/mdns-discovery.js";
28
44
  import { resolveJitiImport } from "@blackbelt-technology/pi-dashboard-shared/resolve-jiti.js";
29
-
30
- const SUBCOMMANDS = ["start", "stop", "restart", "status"] as const;
45
+ import { assertNodeVersionSupported } from "./node-guard.js";
46
+ import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
47
+ import { bootstrapInstall } from "@blackbelt-technology/pi-dashboard-shared/bootstrap-install.js";
48
+ import {
49
+ findBundledExtension,
50
+ registerBridgeExtension,
51
+ } from "@blackbelt-technology/pi-dashboard-shared/bridge-register.js";
52
+ import type { DashboardServer } from "./server.js";
53
+ import { updateBootstrapCompatibility } from "./pi-version-skew.js";
54
+
55
+ const SUBCOMMANDS = ["start", "stop", "restart", "status", "upgrade-pi"] as const;
31
56
  type Subcommand = (typeof SUBCOMMANDS)[number];
32
57
 
33
58
  export interface ParsedArgs {
@@ -87,6 +112,7 @@ export function buildConfig(flags: Partial<ServerConfig>): ServerConfig {
87
112
  maxStringFieldSize: fileConfig.memoryLimits.maxStringFieldSize,
88
113
  maxWsBufferBytes: fileConfig.memoryLimits.maxWsBufferBytes,
89
114
  editor: fileConfig.editor,
115
+ openspec: fileConfig.openspec,
90
116
  resolvedTrustedNetworks: fileConfig.resolvedTrustedNetworks,
91
117
  corsAllowedOrigins: fileConfig.cors.allowedOrigins,
92
118
  };
@@ -94,8 +120,17 @@ export function buildConfig(flags: Partial<ServerConfig>): ServerConfig {
94
120
 
95
121
  /**
96
122
  * Run the server in the foreground (original behavior).
123
+ *
124
+ * After the server starts listening, the degraded-mode bootstrap kicks
125
+ * off: if `pi` is not resolvable via the ToolRegistry, the server flips
126
+ * `bootstrapState` to "installing" and begins a background
127
+ * `bootstrapInstall`. Session-spawn and other pi-dependent endpoints
128
+ * queue or 503 during this window (see change tasks §5).
129
+ *
130
+ * See change: unified-bootstrap-install.
97
131
  */
98
132
  async function runForeground(config: ServerConfig): Promise<void> {
133
+ assertNodeVersionSupported();
99
134
  const server = await createServer(config);
100
135
 
101
136
  let shuttingDown = false;
@@ -114,12 +149,139 @@ async function runForeground(config: ServerConfig): Promise<void> {
114
149
  process.on("SIGTERM", shutdown);
115
150
 
116
151
  await server.start();
152
+
153
+ // Kick off the degraded-mode first-run bootstrap if pi is unresolvable.
154
+ // Runs async — server is already listening, so UI + non-pi endpoints
155
+ // remain fully operational during the ~30s install window.
156
+ // TODO(single-dashboard-per-home): when home-lock wiring lands, wrap
157
+ // this inside the acquired lock to serialize concurrent first-run
158
+ // installs from multiple dashboard invocations on the same HOME.
159
+ runDegradedModeBootstrap(server).catch((err) => {
160
+ console.error("[bootstrap] unexpected failure in bootstrap orchestrator:", err);
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Orchestrate the first-run bootstrap flow.
166
+ *
167
+ * - If pi is already resolvable → leave `bootstrapState` at the default
168
+ * "ready" and return immediately.
169
+ * - Otherwise flip to "installing", run `bootstrapInstall`, then:
170
+ * • on success, rescan the registry, attempt bridge registration
171
+ * (failures are non-fatal and land in `bridgeRegistrationError`),
172
+ * flip to "ready".
173
+ * • on failure, flip to "failed" with the error.
174
+ *
175
+ * Structured log lines at each transition aid diagnosis in daemon-mode
176
+ * (stdout goes to ~/.pi/dashboard/server.log).
177
+ */
178
+ async function runDegradedModeBootstrap(server: DashboardServer): Promise<void> {
179
+ const registry = getDefaultRegistry();
180
+ const initial = registry.resolve("pi");
181
+
182
+ if (initial.ok) {
183
+ // Default state is "ready" — no change needed. Log once for clarity.
184
+ console.log(`[bootstrap] ready (pi resolved via ${initial.source})`);
185
+ // Populate version-skew compatibility info even when no install was
186
+ // needed — the UI banner renders upgradeRecommended hints.
187
+ try {
188
+ const serverPkg = path.resolve(
189
+ path.dirname(fileURLToPath(import.meta.url)),
190
+ "..",
191
+ "package.json",
192
+ );
193
+ updateBootstrapCompatibility(server.bootstrapState, serverPkg);
194
+ } catch (err) {
195
+ console.warn("[bootstrap] version-skew check failed (non-fatal):", err);
196
+ }
197
+ return;
198
+ }
199
+
200
+ const installPackages = ["@mariozechner/pi-coding-agent", "@fission-ai/openspec", "tsx"];
201
+ server.bootstrapState.setLastInstallPackages(installPackages);
202
+ console.log("[bootstrap] installing (pi unresolved, running background install)");
203
+ server.bootstrapState.set({
204
+ status: "installing",
205
+ progress: { step: "pi", output: "starting install…" },
206
+ error: undefined,
207
+ });
208
+
209
+ try {
210
+ const res = await bootstrapInstall({
211
+ packages: installPackages,
212
+ progress: (p) => {
213
+ server.bootstrapState.set({
214
+ progress: { step: p.step, output: p.output },
215
+ });
216
+ },
217
+ });
218
+
219
+ if (!res.ok) {
220
+ console.error(`[bootstrap] failed: ${res.error}`);
221
+ server.bootstrapState.set({
222
+ status: "failed",
223
+ error: { message: res.error },
224
+ progress: undefined,
225
+ });
226
+ return;
227
+ }
228
+
229
+ // Rescan registry so pi is re-resolved after the fresh install.
230
+ // If `rescan` is not exposed, the resolver's strategy chain re-runs
231
+ // on the next `resolve()` call anyway; we just want fresh timing.
232
+ type Rescannable = { rescan?: (name: string) => void };
233
+ const maybeRescan = (registry as unknown as Rescannable).rescan;
234
+ if (typeof maybeRescan === "function") maybeRescan.call(registry, "pi");
235
+
236
+ // Attempt bridge registration. Failures are non-fatal per spec §10.3.
237
+ let bridgeErr: string | undefined;
238
+ try {
239
+ const extPath = findBundledExtension(process.cwd());
240
+ if (extPath) {
241
+ registerBridgeExtension(extPath);
242
+ } else {
243
+ bridgeErr = "bundled extension not found after install";
244
+ }
245
+ } catch (err) {
246
+ bridgeErr = err instanceof Error ? err.message : String(err);
247
+ }
248
+
249
+ server.bootstrapState.set({
250
+ status: "ready",
251
+ progress: undefined,
252
+ error: undefined,
253
+ bridgeRegistrationError: bridgeErr,
254
+ });
255
+ // Populate compatibility info after a successful install.
256
+ try {
257
+ const serverPkg = path.resolve(
258
+ path.dirname(fileURLToPath(import.meta.url)),
259
+ "..",
260
+ "package.json",
261
+ );
262
+ updateBootstrapCompatibility(server.bootstrapState, serverPkg);
263
+ } catch (err) {
264
+ console.warn("[bootstrap] version-skew check failed (non-fatal):", err);
265
+ }
266
+ console.log(
267
+ `[bootstrap] ready (installed ${res.installed.join(", ")}${bridgeErr ? `; bridge warning: ${bridgeErr}` : ""})`,
268
+ );
269
+ } catch (err) {
270
+ const message = err instanceof Error ? err.message : String(err);
271
+ console.error(`[bootstrap] failed: ${message}`);
272
+ server.bootstrapState.set({
273
+ status: "failed",
274
+ error: { message },
275
+ progress: undefined,
276
+ });
277
+ }
117
278
  }
118
279
 
119
280
  /**
120
281
  * Start the server as a detached background daemon.
121
282
  */
122
283
  async function cmdStart(config: ServerConfig): Promise<void> {
284
+ assertNodeVersionSupported();
123
285
  const running = await isServerRunning(config.port);
124
286
  if (running) {
125
287
  console.log(`Dashboard server is already running (pid ${running})`);
@@ -146,10 +308,14 @@ async function cmdStart(config: ServerConfig): Promise<void> {
146
308
  try {
147
309
  tsLoader = resolveJitiImport();
148
310
  } catch {
149
- // Fallback to tsx when jiti is not available (e.g. running outside pi)
311
+ // Fallback to tsx when jiti is not available (e.g. running outside pi).
312
+ // The loader is passed to `node --import`; on Windows, Node >= 20 rejects
313
+ // raw absolute paths with a drive letter (parsed as URL scheme), so we
314
+ // return a file:// URL. See change: fix-windows-server-parity.
150
315
  try {
151
316
  const tsxMain = createRequire(cliPath).resolve("tsx");
152
- tsLoader = path.join(path.dirname(tsxMain), "esm", "index.mjs");
317
+ const tsxLoaderPath = path.join(path.dirname(tsxMain), "esm", "index.mjs");
318
+ tsLoader = pathToFileURL(tsxLoaderPath).href;
153
319
  } catch {
154
320
  console.error(
155
321
  "[pi-dashboard] Cannot find TypeScript loader. " +
@@ -159,17 +325,30 @@ async function cmdStart(config: ServerConfig): Promise<void> {
159
325
  }
160
326
  }
161
327
 
162
- // Redirect daemon stdout/stderr to a log file for crash diagnosis
163
- const logDir = path.join(process.env.HOME ?? "~", ".pi", "dashboard");
328
+ // Redirect daemon stdout/stderr to a log file for crash diagnosis.
329
+ // Log is opened in append mode ("a") so output from prior start attempts
330
+ // is preserved across retries — critical for diagnosing intermittent or
331
+ // silent launch failures. A timestamped header line distinguishes runs.
332
+ // See change: fix-windows-server-parity.
333
+ const logDir = path.join(os.homedir(), ".pi", "dashboard");
164
334
  fs.mkdirSync(logDir, { recursive: true });
165
- const logFd = fs.openSync(path.join(logDir, "server.log"), "w");
166
-
335
+ const logPath = path.join(logDir, "server.log");
336
+ const logFd = fs.openSync(logPath, "a");
337
+ fs.writeSync(
338
+ logFd,
339
+ `\n[${new Date().toISOString()}] pi-dashboard start (parent pid ${process.pid}, port ${config.port})\n`,
340
+ );
341
+
342
+ // tsLoader is a file:// URL (required on Windows for node --import).
343
+ // See change: fix-windows-server-parity.
167
344
  const child = spawn(process.execPath, ["--import", tsLoader, cliPath, ...args], {
168
345
  detached: true,
169
346
  stdio: ["ignore", logFd, logFd],
170
347
  env: { ...process.env },
171
348
  });
172
349
  child.unref();
350
+ // Close the parent's copy of the fd — child has its own via stdio inheritance.
351
+ try { fs.closeSync(logFd); } catch { /* ignore */ }
173
352
 
174
353
  // Wait for dashboard to become available (up to 5 seconds)
175
354
  const deadline = Date.now() + 5000;
@@ -196,31 +375,21 @@ async function cmdStart(config: ServerConfig): Promise<void> {
196
375
  /**
197
376
  * Stop the running server daemon.
198
377
  */
199
- /** Kill a process by PID, wait for exit, force-kill if needed. */
378
+ /**
379
+ * Kill a process by PID with logging. Delegates to the shared platform
380
+ * primitive (`packages/shared/src/platform/process.ts`) which handles the
381
+ * Windows (taskkill) vs Unix (SIGTERM→SIGKILL) split.
382
+ * See change: consolidate-platform-handlers.
383
+ */
200
384
  async function killProcess(pid: number, label: string): Promise<boolean> {
201
- if (!isProcessAlive(pid)) return false;
202
- try { process.kill(pid, "SIGTERM"); } catch { return false; }
203
- const deadline = Date.now() + 5000;
204
- while (Date.now() < deadline) {
205
- await new Promise((r) => setTimeout(r, 200));
206
- if (!isProcessAlive(pid)) {
207
- console.log(`${label} stopped (pid ${pid})`);
208
- return true;
209
- }
210
- }
211
- try { process.kill(pid, "SIGKILL"); } catch { /* already dead */ }
212
- console.log(`${label} stopped (forced, pid ${pid})`);
385
+ const result = await platformKillProcess(pid);
386
+ if (!result.ok) return false;
387
+ console.log(`${label} stopped${result.forced ? " (forced)" : ""} (pid ${pid})`);
213
388
  return true;
214
389
  }
215
390
 
216
- /** Find PIDs holding a port via lsof (macOS/Linux). */
217
- function findPortHolders(port: number): number[] {
218
- try {
219
- const { execSync } = require("node:child_process");
220
- const output = execSync(`lsof -t -i :${port} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf-8" });
221
- return output.trim().split("\n").map(Number).filter((n: number) => n > 0 && n !== process.pid);
222
- } catch { return []; }
223
- }
391
+ // Local alias to preserve prior internal references.
392
+ const isProcessAlive = (pid: number) => platformIsProcessAlive(pid);
224
393
 
225
394
  async function cmdStop(): Promise<void> {
226
395
  const config = loadConfig();
@@ -255,6 +424,58 @@ async function cmdStop(): Promise<void> {
255
424
  /**
256
425
  * Show server status.
257
426
  */
427
+ /**
428
+ * `pi-dashboard upgrade-pi` — upgrade pi-coding-agent via bootstrap.
429
+ *
430
+ * If a dashboard is currently running, POST to /api/bootstrap/upgrade-pi
431
+ * (so the running server owns the install, broadcasts state, and reloads
432
+ * connected sessions). Otherwise run `bootstrapInstall` directly with a
433
+ * streaming progress formatter and exit when done.
434
+ *
435
+ * See change: unified-bootstrap-install §8.
436
+ */
437
+ async function cmdUpgradePi(config: ServerConfig): Promise<void> {
438
+ const status = await isDashboardRunning(config.port);
439
+ if (status.running) {
440
+ console.log(
441
+ `[upgrade-pi] dashboard running at http://localhost:${config.port}, delegating to server`,
442
+ );
443
+ try {
444
+ const res = await fetch(`http://localhost:${config.port}/api/bootstrap/upgrade-pi`, {
445
+ method: "POST",
446
+ });
447
+ if (!res.ok) {
448
+ const body = await res.text();
449
+ console.error(`[upgrade-pi] server rejected upgrade: HTTP ${res.status} ${body}`);
450
+ process.exit(1);
451
+ }
452
+ const body = (await res.json()) as { ticketId?: string };
453
+ console.log(`[upgrade-pi] queued (ticketId=${body.ticketId ?? "?"})`);
454
+ console.log("[upgrade-pi] progress is streamed to open dashboard tabs; CLI exits now.");
455
+ return;
456
+ } catch (err) {
457
+ console.error("[upgrade-pi] failed to reach server:", err);
458
+ process.exit(1);
459
+ }
460
+ }
461
+
462
+ console.log("[upgrade-pi] no dashboard running — installing directly");
463
+ const res = await bootstrapInstall({
464
+ packages: ["@mariozechner/pi-coding-agent"],
465
+ progress: (p) => {
466
+ const line = p.output
467
+ ? `[upgrade-pi] ${p.step} ${p.status}: ${p.output}`
468
+ : `[upgrade-pi] ${p.step} ${p.status}`;
469
+ console.log(line);
470
+ },
471
+ });
472
+ if (!res.ok) {
473
+ console.error(`[upgrade-pi] failed: ${res.error}`);
474
+ process.exit(1);
475
+ }
476
+ console.log(`[upgrade-pi] ✓ installed ${res.installed.join(", ")}`);
477
+ }
478
+
258
479
  async function cmdStatus(port: number): Promise<void> {
259
480
  // 1. Try mDNS discovery first
260
481
  try {
@@ -328,6 +549,9 @@ async function main() {
328
549
  case "status":
329
550
  await cmdStatus(config.port);
330
551
  break;
552
+ case "upgrade-pi":
553
+ await cmdUpgradePi(config);
554
+ break;
331
555
  default:
332
556
  // No subcommand — run in foreground (backward compatible)
333
557
  await runForeground(config);
@@ -100,6 +100,17 @@ export function writeConfigPartial(partial: Record<string, any>): WriteConfigRes
100
100
  mergedAuth.allowedUsers = partial.auth.allowedUsers;
101
101
  }
102
102
 
103
+ // fix-trusted-networks-no-oauth: propagate bypassHosts / bypassUrls
104
+ // from the incoming partial. Without these, the UI's Trusted Networks
105
+ // save path silently dropped every entry on disk. `!== undefined`
106
+ // (not truthiness) lets an empty array clear all entries.
107
+ if (partial.auth.bypassHosts !== undefined) {
108
+ mergedAuth.bypassHosts = partial.auth.bypassHosts;
109
+ }
110
+ if (partial.auth.bypassUrls !== undefined) {
111
+ mergedAuth.bypassUrls = partial.auth.bypassUrls;
112
+ }
113
+
103
114
  partial.auth = mergedAuth;
104
115
  }
105
116
 
@@ -114,6 +125,11 @@ export function writeConfigPartial(partial: Record<string, any>): WriteConfigRes
114
125
  restartRequired = true;
115
126
  }
116
127
 
128
+ // Merge openspec sub-object (no restart required — live-reconfigured)
129
+ if (partial.openspec) {
130
+ partial.openspec = { ...existing.openspec, ...partial.openspec };
131
+ }
132
+
117
133
  const merged = { ...existing, ...partial };
118
134
 
119
135
  // Remove computed fields that shouldn't be persisted