@blackbelt-technology/pi-agent-dashboard 0.5.3 → 0.5.4

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 (212) hide show
  1. package/AGENTS.md +19 -30
  2. package/README.md +69 -1
  3. package/docs/architecture.md +89 -165
  4. package/package.json +10 -7
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
  7. package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
  8. package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
  9. package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
  10. package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
  11. package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
  12. package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
  13. package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
  14. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
  15. package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
  16. package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
  17. package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
  19. package/packages/extension/src/bridge-default-model-gate.ts +32 -0
  20. package/packages/extension/src/bridge.ts +299 -20
  21. package/packages/extension/src/command-handler.ts +53 -7
  22. package/packages/extension/src/dashboard-default-adapter.ts +5 -0
  23. package/packages/extension/src/prompt-bus.ts +15 -0
  24. package/packages/extension/src/slash-dispatch.ts +30 -15
  25. package/packages/extension/src/source-detector.ts +13 -5
  26. package/packages/extension/src/usage-limit-orderer.ts +18 -1
  27. package/packages/server/bin/pi-dashboard.mjs +62 -14
  28. package/packages/server/package.json +9 -5
  29. package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
  30. package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
  31. package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
  32. package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
  33. package/packages/server/src/__tests__/cli-version.test.ts +151 -0
  34. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
  35. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
  36. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
  37. package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
  38. package/packages/server/src/__tests__/directory-service.test.ts +9 -0
  39. package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
  40. package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
  41. package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
  42. package/packages/server/src/__tests__/health-shape.test.ts +35 -12
  43. package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
  44. package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
  45. package/packages/server/src/__tests__/package-routes.test.ts +6 -2
  46. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
  47. package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
  48. package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
  49. package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
  50. package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
  51. package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
  52. package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
  53. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  54. package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
  55. package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
  56. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
  57. package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
  58. package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
  59. package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
  60. package/packages/server/src/browser-gateway.ts +83 -5
  61. package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
  63. package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
  64. package/packages/server/src/changelog-parser.ts +1 -1
  65. package/packages/server/src/cli.ts +68 -250
  66. package/packages/server/src/event-status-extraction.ts +14 -62
  67. package/packages/server/src/event-wiring.ts +23 -10
  68. package/packages/server/src/memory-session-manager.ts +4 -0
  69. package/packages/server/src/pi-core-checker.ts +1 -1
  70. package/packages/server/src/pi-dev-version-check.ts +1 -1
  71. package/packages/server/src/pi-version-skew.ts +24 -46
  72. package/packages/server/src/plugin-intent-cache.ts +67 -0
  73. package/packages/server/src/preferences-store.ts +199 -13
  74. package/packages/server/src/recovery-server.ts +366 -0
  75. package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
  76. package/packages/server/src/routes/doctor-routes.ts +26 -21
  77. package/packages/server/src/routes/manifest-route.ts +162 -0
  78. package/packages/server/src/routes/openspec-routes.ts +4 -25
  79. package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
  80. package/packages/server/src/routes/pi-core-routes.ts +3 -23
  81. package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
  82. package/packages/server/src/routes/recommended-routes.ts +21 -0
  83. package/packages/server/src/routes/system-routes.ts +73 -11
  84. package/packages/server/src/server.ts +105 -307
  85. package/packages/server/src/session-api.ts +5 -63
  86. package/packages/shared/package.json +1 -1
  87. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
  88. package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
  89. package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
  90. package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
  91. package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
  92. package/packages/shared/src/__tests__/config.test.ts +40 -0
  93. package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
  94. package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
  95. package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
  96. package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
  97. package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
  98. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
  99. package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
  100. package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
  101. package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
  102. package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
  103. package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
  104. package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
  105. package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
  106. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
  107. package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
  108. package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
  109. package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
  110. package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
  111. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
  112. package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
  113. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
  114. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
  115. package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
  116. package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
  117. package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
  118. package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
  119. package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
  120. package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
  121. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
  122. package/packages/shared/src/bridge-register.ts +35 -2
  123. package/packages/shared/src/browser-protocol.ts +176 -2
  124. package/packages/shared/src/config.ts +12 -0
  125. package/packages/shared/src/dashboard-paths.ts +69 -0
  126. package/packages/shared/src/dashboard-plugin/index.ts +2 -0
  127. package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
  128. package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
  129. package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
  130. package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
  131. package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
  132. package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
  133. package/packages/shared/src/dashboard-starter.ts +22 -0
  134. package/packages/shared/src/doctor-core.ts +49 -27
  135. package/packages/shared/src/launch-source-types.ts +9 -9
  136. package/packages/shared/src/legacy-managed-dir.ts +97 -0
  137. package/packages/shared/src/mdns-discovery.ts +4 -1
  138. package/packages/shared/src/pi-package-resolver.ts +388 -0
  139. package/packages/shared/src/platform/binary-lookup.ts +27 -3
  140. package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
  141. package/packages/shared/src/platform/exec.ts +22 -0
  142. package/packages/shared/src/platform/node-spawn.ts +42 -41
  143. package/packages/shared/src/plugin-bridge-register.ts +275 -18
  144. package/packages/shared/src/protocol.ts +94 -2
  145. package/packages/shared/src/recommended-extensions.ts +34 -10
  146. package/packages/shared/src/server-identity.ts +74 -5
  147. package/packages/shared/src/server-launcher.ts +20 -0
  148. package/packages/shared/src/source-matching.ts +1 -1
  149. package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
  150. package/packages/shared/src/tool-registry/definitions.ts +91 -7
  151. package/packages/shared/src/types.ts +12 -8
  152. package/scripts/maybe-patch-package.cjs +44 -0
  153. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
  154. package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
  155. package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
  156. package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
  157. package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
  158. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
  159. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
  160. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
  161. package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
  162. package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
  163. package/packages/server/src/bootstrap-install-from-list.ts +0 -232
  164. package/packages/server/src/bootstrap-queue.ts +0 -130
  165. package/packages/server/src/bootstrap-state.ts +0 -159
  166. package/packages/server/src/legacy-pi-cleanup.ts +0 -151
  167. package/packages/server/src/routes/bootstrap-routes.ts +0 -125
  168. package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
  169. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
  170. package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
  171. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
  172. package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
  173. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
  174. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
  175. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
  176. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
  177. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
  178. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
  179. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
  180. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
  181. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
  182. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
  183. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
  184. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
  185. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
  186. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
  187. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
  188. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
  189. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
  190. package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
  191. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
  192. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
  193. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
  194. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
  195. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
  196. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
  197. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
  198. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
  199. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
  200. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
  201. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
  202. package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
  203. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
  204. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
  205. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
  206. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
  207. package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
  208. package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
  209. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
  210. package/packages/shared/src/bootstrap-install.ts +0 -406
  211. package/packages/shared/src/installable-list.ts +0 -152
  212. package/packages/shared/src/launch-source-flag.ts +0 -14
@@ -1,232 +0,0 @@
1
- /**
2
- * Bootstrap install reconciler driven by ~/.pi/dashboard/installable.json.
3
- * Invoked by cli.ts before app.listen.
4
- *
5
- * File-absent path is a deliberate no-op: Bridge and Standalone starters
6
- * never write installable.json; only Electron seeds it on first run.
7
- * When the file is absent, this function logs and returns immediately so
8
- * bootstrap.status transitions to "ready" without delay.
9
- *
10
- * See change: simplify-electron-bootstrap-derived-state.
11
- */
12
- import os from "node:os";
13
- import path from "node:path";
14
- import { createRequire } from "node:module";
15
- import { getManagedDir } from "@blackbelt-technology/pi-dashboard-shared/managed-paths.js";
16
- import {
17
- readInstallableList,
18
- type InstallablePackage,
19
- } from "@blackbelt-technology/pi-dashboard-shared/installable-list.js";
20
- import { bootstrapInstall } from "@blackbelt-technology/pi-dashboard-shared/bootstrap-install.js";
21
- import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
22
- import type { BootstrapStateStore } from "./bootstrap-state.js";
23
-
24
- // ── Injectable helpers (overridable in tests) ──────────────────────────────
25
-
26
- export type InstallProgressCallback = (line: string) => void;
27
- export type PackageInstaller = (
28
- pkg: InstallablePackage,
29
- onOutput: InstallProgressCallback,
30
- ) => Promise<void>;
31
-
32
- // ── Installed-check ────────────────────────────────────────────────────────
33
-
34
- /**
35
- * Return true if `pkgName` is resolvable from `managedDir/node_modules`.
36
- * Version satisfies check is intentionally omitted (no semver dep) —
37
- * we treat "resolves at all" as satisfied. This is sufficient for the
38
- * Phase B bootstrap use case.
39
- */
40
- export function isNpmPackageInstalled(pkgName: string, managedDir: string): boolean {
41
- try {
42
- // createRequire resolves from the given path; look in managedDir/node_modules.
43
- const req = createRequire(path.join(managedDir, "package.json"));
44
- req.resolve(pkgName + "/package.json");
45
- return true;
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
- // ── Default installers ─────────────────────────────────────────────────────
52
-
53
- async function defaultNpmInstall(
54
- pkg: InstallablePackage,
55
- managedDir: string,
56
- onOutput: InstallProgressCallback,
57
- ): Promise<void> {
58
- const spec =
59
- pkg.version && pkg.version !== "*"
60
- ? `${pkg.name}@${pkg.version}`
61
- : pkg.name;
62
- const res = await bootstrapInstall({
63
- packages: [spec],
64
- managedDir,
65
- progress: (p) => {
66
- if (p.output) onOutput(p.output);
67
- },
68
- });
69
- if (!res.ok) {
70
- throw new Error(res.error);
71
- }
72
- }
73
-
74
- async function defaultPiExtensionInstall(
75
- pkg: InstallablePackage,
76
- onOutput: InstallProgressCallback,
77
- ): Promise<void> {
78
- const registry = getDefaultRegistry();
79
- const { module: piModule } = await registry.resolveModule<{
80
- DefaultPackageManager: any;
81
- SettingsManager: any;
82
- }>("pi-coding-agent");
83
- const agentDir = path.join(os.homedir(), ".pi", "agent");
84
- const settingsManager = piModule.SettingsManager.create(process.cwd(), agentDir);
85
- const pm = new piModule.DefaultPackageManager({
86
- cwd: process.cwd(),
87
- agentDir,
88
- settingsManager,
89
- });
90
- pm.setProgressCallback((event: { message?: string }) => {
91
- if (event.message) onOutput(event.message);
92
- });
93
- await pm.installAndPersist(pkg.name, { local: false });
94
- }
95
-
96
- // ── Options ────────────────────────────────────────────────────────────────
97
-
98
- export interface BootstrapInstallFromListOptions {
99
- /** Override config dir for installable.json (default: ~/.pi/dashboard/). */
100
- configDir?: string;
101
- /** Override managed dir for npm installs (default: ~/.pi-dashboard/). */
102
- managedDir?: string;
103
- /**
104
- * Injectable npm installer. Defaults to bootstrapInstall.
105
- * Receives the InstallablePackage and a streaming output callback.
106
- */
107
- npmInstall?: PackageInstaller;
108
- /**
109
- * Injectable pi-extension installer. Defaults to pi DefaultPackageManager.
110
- * Receives the InstallablePackage and a streaming output callback.
111
- */
112
- piInstall?: PackageInstaller;
113
- /**
114
- * Injectable installed-check for npm packages.
115
- * Defaults to isNpmPackageInstalled.
116
- */
117
- isInstalled?: (pkg: InstallablePackage, managedDir: string) => boolean;
118
- }
119
-
120
- // ── Main reconciler ────────────────────────────────────────────────────────
121
-
122
- /**
123
- * Reconcile packages from installable.json against the managed directory.
124
- *
125
- * - File absent: log and return immediately (not a failure).
126
- * - Per package: check installed → skip or install.
127
- * - Required failure: set bootstrap status=failed, throw (abort server start).
128
- * - Optional failure: log, record in failed[], continue.
129
- */
130
- export async function bootstrapInstallFromList(
131
- bootstrapState: BootstrapStateStore,
132
- opts?: BootstrapInstallFromListOptions,
133
- ): Promise<void> {
134
- const configDir =
135
- opts?.configDir ?? path.join(os.homedir(), ".pi", "dashboard");
136
- const managedDir = opts?.managedDir ?? getManagedDir();
137
-
138
- // Read installable.json; absent file is a deliberate no-op.
139
- const list = await readInstallableList(configDir);
140
- if (list === null) {
141
- console.log(
142
- "[bootstrap] bootstrap.installable.skipped reason=file-not-found",
143
- );
144
- return;
145
- }
146
-
147
- // Only process packages that are active (not deprecated, not defaultOff).
148
- const packages = list.packages.filter((p) => !p.deprecated && !p.defaultOff);
149
- const total = packages.length;
150
- let installedCount = 0;
151
- const failed: string[] = [];
152
-
153
- // Stamp initial installable progress into bootstrap state.
154
- bootstrapState.set({ installable: { total, installed: 0, failed: [] } });
155
-
156
- const checkInstalled =
157
- opts?.isInstalled ?? ((p, dir) => isNpmPackageInstalled(p.name, dir));
158
- const doNpmInstall: PackageInstaller =
159
- opts?.npmInstall ??
160
- ((p, cb) => defaultNpmInstall(p, managedDir, cb));
161
- const doPiInstall: PackageInstaller =
162
- opts?.piInstall ?? defaultPiExtensionInstall;
163
-
164
- for (const pkg of packages) {
165
- // Fast path: already installed (npm packages only; pi-extension always attempts).
166
- if (pkg.kind === "npm" && checkInstalled(pkg, managedDir)) {
167
- console.log(
168
- `[bootstrap] bootstrap.installable.package name=${pkg.name} status=satisfied`,
169
- );
170
- installedCount++;
171
- bootstrapState.set({
172
- installable: { total, installed: installedCount, failed },
173
- });
174
- continue;
175
- }
176
-
177
- // Emit installing progress.
178
- bootstrapState.set({
179
- progress: { step: pkg.name, output: "installing..." },
180
- installable: { total, installed: installedCount, failed },
181
- });
182
-
183
- try {
184
- const onOutput = (line: string): void => {
185
- bootstrapState.set({ progress: { step: pkg.name, output: line } });
186
- };
187
-
188
- if (pkg.kind === "npm") {
189
- await doNpmInstall(pkg, onOutput);
190
- } else {
191
- await doPiInstall(pkg, onOutput);
192
- }
193
-
194
- installedCount++;
195
- bootstrapState.set({
196
- progress: undefined,
197
- installable: { total, installed: installedCount, failed },
198
- });
199
- console.log(
200
- `[bootstrap] bootstrap.installable.package name=${pkg.name} status=done`,
201
- );
202
- } catch (err) {
203
- const message = err instanceof Error ? err.message : String(err);
204
- console.error(
205
- `[bootstrap] bootstrap.installable.package name=${pkg.name} status=error error=${message}`,
206
- );
207
- failed.push(pkg.name);
208
- bootstrapState.set({
209
- progress: undefined,
210
- installable: { total, installed: installedCount, failed: [...failed] },
211
- });
212
-
213
- if (pkg.required) {
214
- const errorMessage = `Required package "${pkg.name}" failed to install: ${message}`;
215
- bootstrapState.set({
216
- status: "failed",
217
- error: { message: errorMessage },
218
- });
219
- throw new Error(errorMessage);
220
- }
221
- // Optional package failure: log, continue to next package.
222
- }
223
- }
224
-
225
- // Final state snapshot.
226
- bootstrapState.set({
227
- installable: { total, installed: installedCount, failed },
228
- });
229
- console.log(
230
- `[bootstrap] bootstrap.installable.done total=${total} installed=${installedCount} failed=${failed.length}`,
231
- );
232
- }
@@ -1,130 +0,0 @@
1
- /**
2
- * In-memory queue for pi-dependent operations deferred during bootstrap
3
- * install. When `bootstrapState.status === "installing"`, callers should
4
- * enqueue their handler and return a 202 Accepted with the ticketId.
5
- * When status transitions to "ready", `flushAll()` runs every queued
6
- * handler sequentially in enqueue order.
7
- *
8
- * Queue is process-local and NOT persisted. If the dashboard crashes
9
- * mid-install, queued requests are lost — documented as a known
10
- * limitation in design.md §16.2.
11
- *
12
- * See change: unified-bootstrap-install.
13
- */
14
- import { randomUUID } from "node:crypto";
15
-
16
- export interface QueuedTicket<T> {
17
- ticketId: string;
18
- /**
19
- * Resolves when the queued handler runs (or rejects if it throws).
20
- * Call sites can await this when they want to synchronously return
21
- * the eventual result — but 202-Accepted flows MUST NOT await, they
22
- * return the ticketId to the client immediately.
23
- */
24
- result: Promise<T>;
25
- }
26
-
27
- export interface BootstrapQueue {
28
- enqueue<T>(handler: () => Promise<T>): QueuedTicket<T>;
29
- flushAll(): Promise<void>;
30
- /** Number of currently pending tickets. */
31
- size(): number;
32
- /** Drop all pending tickets without running them (used at shutdown). */
33
- clear(reason?: string): void;
34
- /**
35
- * Register a listener invoked after each ticket runs (success or
36
- * failure). The server wires this to a `bootstrap_ticket_complete`
37
- * WS broadcast so browser clients can correlate the outcome of a
38
- * 202-accepted request via their stored ticketId.
39
- * See change: unified-bootstrap-install.
40
- */
41
- onTicketComplete(
42
- listener: (evt: { ticketId: string; success: boolean; error?: string }) => void,
43
- ): () => void;
44
- }
45
-
46
- interface PendingEntry {
47
- ticketId: string;
48
- run: () => Promise<void>;
49
- /** Reject the caller's `result` promise. Called by `clear()` to
50
- * drain tickets at shutdown. */
51
- reject: (err: unknown) => void;
52
- }
53
-
54
- export function createBootstrapQueue(): BootstrapQueue {
55
- const pending: PendingEntry[] = [];
56
- const listeners = new Set<
57
- (evt: { ticketId: string; success: boolean; error?: string }) => void
58
- >();
59
-
60
- function notify(evt: { ticketId: string; success: boolean; error?: string }): void {
61
- for (const l of listeners) {
62
- try {
63
- l(evt);
64
- } catch (err) {
65
- console.error("[bootstrap-queue] ticket-complete listener threw:", err);
66
- }
67
- }
68
- }
69
-
70
- return {
71
- enqueue<T>(handler: () => Promise<T>): QueuedTicket<T> {
72
- const ticketId = randomUUID();
73
- let resolve!: (value: T) => void;
74
- let reject!: (err: unknown) => void;
75
- const result = new Promise<T>((res, rej) => {
76
- resolve = res;
77
- reject = rej;
78
- });
79
- pending.push({
80
- ticketId,
81
- reject,
82
- run: async () => {
83
- try {
84
- const value = await handler();
85
- resolve(value);
86
- notify({ ticketId, success: true });
87
- } catch (err) {
88
- reject(err);
89
- const message = err instanceof Error ? err.message : String(err);
90
- notify({ ticketId, success: false, error: message });
91
- }
92
- },
93
- });
94
- return { ticketId, result };
95
- },
96
-
97
- async flushAll(): Promise<void> {
98
- while (pending.length > 0) {
99
- const entry = pending.shift();
100
- if (!entry) break;
101
- try {
102
- await entry.run();
103
- } catch (err) {
104
- // Handler errors propagate via the ticket's `result` promise;
105
- // they should never reach here unless there's a bug in `run`.
106
- console.error(`[bootstrap-queue] ticket ${entry.ticketId} run threw:`, err);
107
- }
108
- }
109
- },
110
-
111
- size() {
112
- return pending.length;
113
- },
114
-
115
- clear(reason = "queue cleared") {
116
- const drained = pending.splice(0, pending.length);
117
- for (const entry of drained) {
118
- // Reject the caller's `result` promise directly and broadcast the
119
- // completion so any browser holding the ticketId learns the
120
- // outcome.
121
- entry.reject(new Error(reason));
122
- notify({ ticketId: entry.ticketId, success: false, error: reason });
123
- }
124
- },
125
- onTicketComplete(listener) {
126
- listeners.add(listener);
127
- return () => listeners.delete(listener);
128
- },
129
- };
130
- }
@@ -1,159 +0,0 @@
1
- import type { DashboardStarter } from "@blackbelt-technology/pi-dashboard-shared/dashboard-starter.js";
2
-
3
- /**
4
- * In-memory bootstrap state store for the dashboard server.
5
- *
6
- * Tracks the degraded-mode status during first-run pi install, upgrade
7
- * operations, and version-skew detection. Subscribers (browser gateway,
8
- * CLI progress printer) receive a snapshot on every `set()` call.
9
- *
10
- * See change: unified-bootstrap-install.
11
- */
12
-
13
- export type BootstrapStatus = "ready" | "installing" | "failed";
14
-
15
- export interface BootstrapProgress {
16
- /** Package / phase being processed (e.g. "pi-coding-agent", "bridge-register"). */
17
- step: string;
18
- /** Optional completion percentage (0..100). */
19
- pct?: number;
20
- /** Last line of npm output or other streaming context. */
21
- output?: string;
22
- }
23
-
24
- export interface BootstrapError {
25
- message: string;
26
- stack?: string;
27
- }
28
-
29
- export interface BootstrapVersions {
30
- pi?: string;
31
- openspec?: string;
32
- tsx?: string;
33
- }
34
-
35
- export interface BootstrapCompatibility {
36
- minimum: string;
37
- recommended: string;
38
- /** null = no upper bound enforced yet. */
39
- maximum: string | null;
40
- /** Current resolved pi version, or undefined when pi is unresolved. */
41
- current?: string;
42
- /** Hint that the user should upgrade pi (below recommended). */
43
- upgradeRecommended?: boolean;
44
- /** Hint that the user should upgrade the dashboard itself (above maximum). */
45
- upgradeDashboard?: boolean;
46
- }
47
-
48
- export interface BootstrapState {
49
- status: BootstrapStatus;
50
- progress?: BootstrapProgress;
51
- error?: BootstrapError;
52
- version?: BootstrapVersions;
53
- compatibility?: BootstrapCompatibility;
54
- /** Set when `registerBridgeExtension` fails after a successful install. */
55
- bridgeRegistrationError?: string;
56
- /**
57
- * Who started this server process. Defaults to "Standalone" (direct CLI).
58
- * Set at boot time from `parseDashboardStarter(process.env)`.
59
- */
60
- starter?: DashboardStarter;
61
- /**
62
- * Installable list reconciliation progress.
63
- * Set by bootstrapInstallFromList during Phase B reconcile.
64
- * See change: simplify-electron-bootstrap-derived-state.
65
- */
66
- installable?: {
67
- total: number;
68
- installed: number;
69
- /** Package names that failed to install. */
70
- failed: string[];
71
- };
72
- /**
73
- * Legacy `@mariozechner/pi-coding-agent` installs detected on disk.
74
- * Populated at server start and after every cleanup POST. See
75
- * `legacy-pi-cleanup.ts`.
76
- */
77
- legacyPiInstalls?: Array<{
78
- scope: "npm-global" | "npx-cache" | "managed";
79
- path: string;
80
- version: string | null;
81
- }>;
82
- }
83
-
84
- export type BootstrapListener = (state: BootstrapState) => void;
85
-
86
- export interface BootstrapStateStore {
87
- get(): BootstrapState;
88
- /**
89
- * Merge `partial` into the current state. Passing `undefined` for a
90
- * key explicitly clears it (e.g. `set({ progress: undefined })` removes
91
- * the progress line after completion). Broadcasts to all subscribers.
92
- */
93
- set(partial: Partial<BootstrapState>): void;
94
- subscribe(listener: BootstrapListener): () => void;
95
- /** Clear all listeners (used in tests + server shutdown). */
96
- dispose(): void;
97
- /**
98
- * Record the package list used by the most recent `bootstrapInstall`
99
- * call. Used by `POST /api/bootstrap/retry` to re-run the exact failed
100
- * set rather than a hard-coded default. Not part of the WS-broadcast
101
- * snapshot — it's purely side-channel metadata for the server.
102
- * See change: unified-bootstrap-install (verification follow-up).
103
- */
104
- setLastInstallPackages(packages: readonly string[]): void;
105
- /** Read the last install set. Returns a fresh copy. */
106
- getLastInstallPackages(): string[];
107
- }
108
-
109
- /**
110
- * Create a fresh bootstrap state store. `initial` is merged over the
111
- * default `{ status: "ready" }`.
112
- */
113
- export function createBootstrapState(
114
- initial?: Partial<BootstrapState>,
115
- ): BootstrapStateStore {
116
- let state: BootstrapState = { status: "ready", ...initial };
117
- let lastInstallPackages: string[] = [];
118
- const listeners = new Set<BootstrapListener>();
119
-
120
- function notify(): void {
121
- const snapshot = { ...state };
122
- for (const l of listeners) {
123
- try {
124
- l(snapshot);
125
- } catch (err) {
126
- // Listener errors are non-fatal — log but continue.
127
- console.error("[bootstrap-state] listener threw:", err);
128
- }
129
- }
130
- }
131
-
132
- return {
133
- get() {
134
- return { ...state };
135
- },
136
- set(partial) {
137
- // Merge: explicit `undefined` in partial clears the field.
138
- state = { ...state, ...partial } as BootstrapState;
139
- // Strip keys whose value is undefined to keep the snapshot tidy.
140
- for (const key of Object.keys(partial) as (keyof BootstrapState)[]) {
141
- if (partial[key] === undefined) delete state[key];
142
- }
143
- notify();
144
- },
145
- subscribe(listener) {
146
- listeners.add(listener);
147
- return () => listeners.delete(listener);
148
- },
149
- dispose() {
150
- listeners.clear();
151
- },
152
- setLastInstallPackages(packages) {
153
- lastInstallPackages = [...packages];
154
- },
155
- getLastInstallPackages() {
156
- return [...lastInstallPackages];
157
- },
158
- };
159
- }
@@ -1,151 +0,0 @@
1
- /**
2
- * Detects and removes legacy `@mariozechner/pi-coding-agent` installs.
3
- *
4
- * Pi was renamed to `@earendil-works/pi-coding-agent` at v0.74. The legacy
5
- * scope is published only up to v0.73.x and conflicts with the new scope's
6
- * `bin/pi` symlink in npm-global (EEXIST). This module surfaces legacy
7
- * installs so the UI can offer a one-click cleanup.
8
- *
9
- * Detection is read-only and cheap (3 fs.stat calls + optional `npm root
10
- * -g`). Cleanup is gated behind a POST endpoint — never silent.
11
- *
12
- * Scopes scanned:
13
- * - npm-global: `$(npm root -g)/@mariozechner/pi-coding-agent`
14
- * - npx-cache: `~/.npm/_npx/<hash>/node_modules/@mariozechner/pi-coding-agent`
15
- * - managed: `~/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent`
16
- */
17
- import fs from "node:fs";
18
- import path from "node:path";
19
- import os from "node:os";
20
- import { execSync } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
21
-
22
- export const LEGACY_PI_PACKAGE = "@mariozechner/pi-coding-agent";
23
-
24
- export type LegacyPiScope = "npm-global" | "npx-cache" | "managed";
25
-
26
- export interface LegacyPiInstall {
27
- scope: LegacyPiScope;
28
- path: string;
29
- version: string | null;
30
- }
31
-
32
- export interface LegacyPiCleanupResult {
33
- scope: LegacyPiScope;
34
- path: string;
35
- removed: boolean;
36
- error?: string;
37
- }
38
-
39
- // ── Pure helpers (no I/O) ──────────────────────────────────────────
40
-
41
- /** Build the legacy package path under a given node_modules root. */
42
- export function legacyPathUnder(nodeModulesDir: string): string {
43
- return path.join(nodeModulesDir, ...LEGACY_PI_PACKAGE.split("/"));
44
- }
45
-
46
- /** Read `version` from a package.json blob; returns null on any parse failure. */
47
- export function parseVersion(packageJsonRaw: string): string | null {
48
- try {
49
- const parsed = JSON.parse(packageJsonRaw);
50
- return typeof parsed.version === "string" ? parsed.version : null;
51
- } catch {
52
- return null;
53
- }
54
- }
55
-
56
- // ── Detection ──────────────────────────────────────────────────────
57
-
58
- function safeStatDir(p: string): boolean {
59
- try { return fs.statSync(p).isDirectory(); } catch { return false; }
60
- }
61
-
62
- function readVersionOf(packageDir: string): string | null {
63
- try {
64
- const raw = fs.readFileSync(path.join(packageDir, "package.json"), "utf-8");
65
- return parseVersion(raw);
66
- } catch {
67
- return null;
68
- }
69
- }
70
-
71
- function detectNpmGlobal(): LegacyPiInstall | null {
72
- let globalRoot: string;
73
- try {
74
- globalRoot = execSync("npm root -g", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
75
- } catch {
76
- return null; // npm not available, or call failed — treat as no install
77
- }
78
- if (!globalRoot) return null;
79
- const pkgDir = legacyPathUnder(globalRoot);
80
- if (!safeStatDir(pkgDir)) return null;
81
- return { scope: "npm-global", path: pkgDir, version: readVersionOf(pkgDir) };
82
- }
83
-
84
- function detectNpxCache(): LegacyPiInstall[] {
85
- const root = path.join(os.homedir(), ".npm", "_npx");
86
- let entries: string[];
87
- try { entries = fs.readdirSync(root); } catch { return []; }
88
- const found: LegacyPiInstall[] = [];
89
- for (const hash of entries) {
90
- const pkgDir = legacyPathUnder(path.join(root, hash, "node_modules"));
91
- if (safeStatDir(pkgDir)) {
92
- found.push({ scope: "npx-cache", path: pkgDir, version: readVersionOf(pkgDir) });
93
- }
94
- }
95
- return found;
96
- }
97
-
98
- function detectManaged(): LegacyPiInstall | null {
99
- const pkgDir = legacyPathUnder(path.join(os.homedir(), ".pi-dashboard", "node_modules"));
100
- if (!safeStatDir(pkgDir)) return null;
101
- return { scope: "managed", path: pkgDir, version: readVersionOf(pkgDir) };
102
- }
103
-
104
- /**
105
- * Scan all three locations for legacy pi installs. Synchronous because
106
- * the cost is dominated by one `npm root -g` invocation (~50ms once);
107
- * everything else is fs.statSync. Called at startup and on POST refresh.
108
- */
109
- export function detectLegacyPiInstalls(): LegacyPiInstall[] {
110
- const found: LegacyPiInstall[] = [];
111
- const g = detectNpmGlobal();
112
- if (g) found.push(g);
113
- found.push(...detectNpxCache());
114
- const m = detectManaged();
115
- if (m) found.push(m);
116
- return found;
117
- }
118
-
119
- // ── Cleanup ────────────────────────────────────────────────────────
120
-
121
- function rmrf(target: string): void {
122
- fs.rmSync(target, { recursive: true, force: true });
123
- }
124
-
125
- function removeOne(install: LegacyPiInstall): LegacyPiCleanupResult {
126
- const base: Pick<LegacyPiCleanupResult, "scope" | "path"> = { scope: install.scope, path: install.path };
127
- try {
128
- if (install.scope === "npm-global") {
129
- // npm-global needs the package-manager call so any bin symlinks are
130
- // cleaned up too. Using `--no-fund --no-audit` to keep output quiet.
131
- execSync(`npm uninstall -g --no-fund --no-audit ${LEGACY_PI_PACKAGE}`, {
132
- stdio: ["ignore", "pipe", "pipe"],
133
- encoding: "utf-8",
134
- });
135
- } else {
136
- // npx-cache and managed are plain node_modules subtrees we can rm.
137
- rmrf(install.path);
138
- }
139
- return { ...base, removed: true };
140
- } catch (err: any) {
141
- return { ...base, removed: false, error: err?.message ?? String(err) };
142
- }
143
- }
144
-
145
- /**
146
- * Remove all detected legacy installs. Each scope is attempted
147
- * independently; one failure does not abort the others.
148
- */
149
- export function uninstallLegacyPi(installs: readonly LegacyPiInstall[]): LegacyPiCleanupResult[] {
150
- return installs.map(removeOne);
151
- }