@blackbelt-technology/pi-agent-dashboard 0.5.2 → 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 +11 -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,130 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mergeInstallableList, readInstallableList, writeInstallableList } from "../installable-list.js";
3
- import type { InstallableList } from "../installable-list.js";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import os from "node:os";
7
-
8
- // ── mergeInstallableList tests ──────────────────────────────────────────────
9
-
10
- describe("mergeInstallableList", () => {
11
- it("keep-user-pin: user version wins when it differs from bundled", () => {
12
- const existing: InstallableList = {
13
- version: "1",
14
- packages: [{ name: "tsx", version: "^4.0.0", required: true, kind: "npm" }],
15
- };
16
- const bundled: InstallableList = {
17
- version: "2",
18
- packages: [{ name: "tsx", version: "^5.0.0", required: true, kind: "npm" }],
19
- };
20
- const { list, warnings } = mergeInstallableList(existing, bundled);
21
- const found = list.packages.find((p) => p.name === "tsx");
22
- expect(found?.version).toBe("^4.0.0");
23
- expect(warnings.length).toBeGreaterThan(0);
24
- expect(warnings[0]).toContain("tsx");
25
- expect(warnings[0]).toContain("^4.0.0");
26
- expect(warnings[0]).toContain("^5.0.0");
27
- });
28
-
29
- it("drop-pin-warn: package in existing but not in bundled gets deprecated=true + warning", () => {
30
- const existing: InstallableList = {
31
- version: "1",
32
- packages: [{ name: "old-tool", version: "^1.0.0", required: false, kind: "npm" }],
33
- };
34
- const bundled: InstallableList = { version: "2", packages: [] };
35
- const { list, warnings } = mergeInstallableList(existing, bundled);
36
- const found = list.packages.find((p) => p.name === "old-tool");
37
- expect(found?.deprecated).toBe(true);
38
- expect(warnings.length).toBeGreaterThan(0);
39
- expect(warnings[0]).toContain("old-tool");
40
- });
41
-
42
- it("add-new-required: new required package from bundled is added as-is", () => {
43
- const existing: InstallableList = { version: "1", packages: [] };
44
- const bundled: InstallableList = {
45
- version: "2",
46
- packages: [{ name: "pi", version: "*", required: true, kind: "npm" }],
47
- };
48
- const { list, warnings } = mergeInstallableList(existing, bundled);
49
- const found = list.packages.find((p) => p.name === "pi");
50
- expect(found).toBeDefined();
51
- expect(found?.required).toBe(true);
52
- expect(found?.defaultOff).toBeFalsy();
53
- expect(warnings).toHaveLength(0);
54
- });
55
-
56
- it("add-new-optional: new optional package from bundled is added with defaultOff=true", () => {
57
- const existing: InstallableList = { version: "1", packages: [] };
58
- const bundled: InstallableList = {
59
- version: "2",
60
- packages: [{ name: "openspec", version: "*", required: false, kind: "npm" }],
61
- };
62
- const { list, warnings } = mergeInstallableList(existing, bundled);
63
- const found = list.packages.find((p) => p.name === "openspec");
64
- expect(found).toBeDefined();
65
- expect(found?.defaultOff).toBe(true);
66
- expect(warnings).toHaveLength(0);
67
- });
68
-
69
- it("version marker in result comes from bundled", () => {
70
- const existing: InstallableList = { version: "1", packages: [] };
71
- const bundled: InstallableList = { version: "42", packages: [] };
72
- const { list } = mergeInstallableList(existing, bundled);
73
- expect(list.version).toBe("42");
74
- });
75
- });
76
-
77
- // ── readInstallableList tests ───────────────────────────────────────────────
78
- // Use a real temp directory (HOME is already ephemeral in the test runner).
79
-
80
- describe("readInstallableList", () => {
81
- let tmpDir: string;
82
-
83
- beforeEach(() => {
84
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "installable-test-"));
85
- });
86
-
87
- afterEach(() => {
88
- fs.rmSync(tmpDir, { recursive: true, force: true });
89
- vi.restoreAllMocks();
90
- });
91
-
92
- it("returns null when file is absent", async () => {
93
- const result = await readInstallableList(tmpDir);
94
- expect(result).toBeNull();
95
- });
96
-
97
- it("drops entries with invalid kind and warns", async () => {
98
- const list: InstallableList = {
99
- version: "1",
100
- packages: [
101
- { name: "good-pkg", version: "*", required: true, kind: "npm" },
102
- { name: "bad-pkg", version: "*", required: true, kind: "unknown-kind" as any },
103
- ],
104
- };
105
- // Write via writeInstallableList (bypasses the drop-invalid-kind guard).
106
- const filePath = path.join(tmpDir, "installable.json");
107
- fs.writeFileSync(filePath, JSON.stringify(list), "utf-8");
108
-
109
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
110
- const result = await readInstallableList(tmpDir);
111
-
112
- expect(result).not.toBeNull();
113
- expect(result!.packages.map((p) => p.name)).toEqual(["good-pkg"]);
114
- expect(warnSpy).toHaveBeenCalledOnce();
115
- expect(warnSpy.mock.calls[0]![0]).toContain("bad-pkg");
116
- });
117
-
118
- it("reads a valid file and returns the list", async () => {
119
- const list: InstallableList = {
120
- version: "3",
121
- packages: [{ name: "tsx", version: "^5.0.0", required: true, kind: "npm" }],
122
- };
123
- await writeInstallableList(list, tmpDir);
124
- const result = await readInstallableList(tmpDir);
125
- expect(result).not.toBeNull();
126
- expect(result!.version).toBe("3");
127
- expect(result!.packages).toHaveLength(1);
128
- expect(result!.packages[0]!.name).toBe("tsx");
129
- });
130
- });
@@ -1,52 +0,0 @@
1
- /**
2
- * Contract test: the bridge auto-spawn code path (server-launcher.ts) must
3
- * NOT import from `installable-list`. Only Electron seeds `installable.json`
4
- * on first run; Bridge and Standalone starters must not produce or consume
5
- * that file.
6
- *
7
- * This is a static source scan — no runtime execution. If this test fails,
8
- * a dependency on installable-list was accidentally added to the bridge
9
- * launcher which would break the "file-absent is a no-op" contract on
10
- * Bridge/Standalone bootstraps.
11
- *
12
- * See change: simplify-electron-bootstrap-derived-state (task 5.7).
13
- */
14
- import { describe, expect, it } from "vitest";
15
- import fs from "node:fs";
16
- import path from "node:path";
17
- import url from "node:url";
18
-
19
- const here = path.dirname(url.fileURLToPath(import.meta.url));
20
- const repoRoot = path.resolve(here, "..", "..", "..", "..");
21
-
22
- /** Files that form the bridge auto-spawn contract. */
23
- const BRIDGE_SPAWN_FILES: readonly string[] = [
24
- "packages/extension/src/server-launcher.ts",
25
- "packages/extension/src/server-auto-start.ts",
26
- "packages/extension/src/connection.ts",
27
- ];
28
-
29
- describe("bridge auto-spawn does not reference installable-list", () => {
30
- for (const rel of BRIDGE_SPAWN_FILES) {
31
- it(`${rel} does not import from installable-list`, () => {
32
- const file = path.resolve(repoRoot, rel);
33
- if (!fs.existsSync(file)) {
34
- // File absent (optional extension file) — contract trivially satisfied.
35
- return;
36
- }
37
- const source = fs.readFileSync(file, "utf-8");
38
-
39
- // Strip line comments before checking so a commented-out import
40
- // does not trigger a false positive.
41
- const stripped = source
42
- .replace(/\/\/[^\n]*/g, "")
43
- .replace(/\/\*[\s\S]*?\*\//g, "");
44
-
45
- expect(
46
- stripped,
47
- `${rel} must not import from "installable-list" — only Electron seeds installable.json. ` +
48
- `Bridge/Standalone starters must not produce or consume that file.`,
49
- ).not.toMatch(/installable-list/);
50
- });
51
- }
52
- });
@@ -1,406 +0,0 @@
1
- /**
2
- * Shared bootstrap installer — single entry point for installing pi,
3
- * openspec, tsx, and recommended packages into the managed directory
4
- * (~/.pi-dashboard/). Callable from any entry point: Electron wizard,
5
- * `pi-dashboard` CLI first-run, `pi-dashboard upgrade-pi` subcommand,
6
- * and the `POST /api/bootstrap/upgrade-pi` REST handler.
7
- *
8
- * This module is deliberately free of Electron-specific concerns
9
- * (bundled-node, offline-bundle cacache, resourcesPath). Those remain
10
- * in `packages/electron/src/lib/dependency-installer.ts` which now
11
- * delegates its "install from npm registry" step to this function.
12
- *
13
- * See change: unified-bootstrap-install.
14
- */
15
- import { spawn as cpSpawn, spawnSync as cpSpawnSync } from "./platform/exec.js";
16
- import {
17
- cpSync,
18
- existsSync,
19
- mkdirSync,
20
- readFileSync,
21
- rmSync,
22
- writeFileSync,
23
- } from "node:fs";
24
- import path from "node:path";
25
- import { getManagedDir } from "./managed-paths.js";
26
- import { getDefaultRegistry, type ToolRegistry } from "./tool-registry/index.js";
27
-
28
- /**
29
- * Per-package progress tick. Mirrors the Electron `InstallProgress`
30
- * shape so existing wizard UI code needs no changes.
31
- */
32
- export interface InstallProgress {
33
- step: string;
34
- status: "pending" | "running" | "done" | "error";
35
- error?: string;
36
- /** Last line of npm output (for streaming progress). */
37
- output?: string;
38
- }
39
-
40
- export type ProgressCallback = (progress: InstallProgress) => void;
41
-
42
- export interface BootstrapInstallOptions {
43
- /** Packages to install via `npm install <pkg>` (registry fetch). */
44
- packages: string[];
45
- /** Root of the managed install. Defaults to `getManagedDir()`. */
46
- managedDir?: string;
47
- /** Called on every progress tick (pending/running/done/error). */
48
- progress?: ProgressCallback;
49
- /**
50
- * Optional override of the npm invocation. By default the function
51
- * resolves the `npm` tool via `ToolRegistry.resolve("npm")` and
52
- * falls back to the plain `npm` / `npm.cmd` binary on PATH. When
53
- * Electron wants to steer the install to bundled Node + npm-cli.js,
54
- * it passes the full argv prefix (e.g. `["<path>/node", "<path>/npm-cli.js"]`).
55
- */
56
- npmArgv?: string[];
57
- /**
58
- * Optional environment overrides merged into the child process env.
59
- * Electron uses this to put bundled Node on PATH for postinstall
60
- * scripts.
61
- */
62
- env?: NodeJS.ProcessEnv;
63
- /**
64
- * Inject a tool registry (tests). Defaults to `getDefaultRegistry()`.
65
- */
66
- registry?: ToolRegistry;
67
- }
68
-
69
- export interface BootstrapInstallSuccess {
70
- ok: true;
71
- installed: string[];
72
- managedDir: string;
73
- }
74
-
75
- export interface BootstrapInstallFailure {
76
- ok: false;
77
- error: string;
78
- installed: string[];
79
- managedDir: string;
80
- }
81
-
82
- export type BootstrapInstallResult = BootstrapInstallSuccess | BootstrapInstallFailure;
83
-
84
- /** Ensure the managed directory exists with a package.json. */
85
- export function ensureManagedDir(managedDir: string): void {
86
- mkdirSync(managedDir, { recursive: true });
87
- const pkgPath = path.join(managedDir, "package.json");
88
- if (!existsSync(pkgPath)) {
89
- writeFileSync(
90
- pkgPath,
91
- JSON.stringify({ name: "pi-dashboard-managed", private: true, type: "module" }, null, 2),
92
- );
93
- }
94
- }
95
-
96
- /**
97
- * Resolve the npm invocation used for bootstrap installs.
98
- *
99
- * Order:
100
- * 1. Explicit `npmArgv` override (Electron bundled-node case).
101
- * 2. `ToolRegistry.resolve("npm")`.
102
- * 3. Plain `npm` (Unix) or `npm.cmd` (Windows) on PATH.
103
- *
104
- * Returns the argv list that will have `install <packages...>` appended.
105
- */
106
- export function resolveNpmArgv(
107
- opts: Pick<BootstrapInstallOptions, "npmArgv" | "registry">,
108
- ): string[] {
109
- if (opts.npmArgv && opts.npmArgv.length > 0) return [...opts.npmArgv];
110
-
111
- const registry = opts.registry ?? getDefaultRegistry();
112
- if (registry.has("npm")) {
113
- const res = registry.resolve("npm");
114
- if (res.ok && res.path) return [res.path];
115
- }
116
-
117
- // Last resort: rely on PATH. On Windows the .cmd shim is required
118
- // because spawn doesn't auto-append extensions.
119
- const npmBin = process.platform === "win32" ? "npm.cmd" : "npm"; // platform-branch-ok
120
- return [npmBin];
121
- }
122
-
123
- /** Internal: spawn npm with a given argv + packages; stream progress. */
124
- function runNpmOnce(
125
- argvBase: string[],
126
- packages: string[],
127
- cwd: string,
128
- env: NodeJS.ProcessEnv,
129
- onOutput?: (line: string) => void,
130
- ): Promise<void> {
131
- return new Promise((resolve, reject) => {
132
- const [cmd, ...baseArgs] = argvBase;
133
- if (!cmd) {
134
- reject(new Error("resolveNpmArgv returned an empty argv"));
135
- return;
136
- }
137
- const args = [...baseArgs, "install", ...packages];
138
-
139
- const child = cpSpawn(cmd, args, {
140
- cwd,
141
- env,
142
- stdio: ["ignore", "pipe", "pipe"],
143
- timeout: 300_000,
144
- });
145
-
146
- let tail = "";
147
-
148
- const handleData = (data: Buffer): void => {
149
- const text = data.toString();
150
- tail += text;
151
- if (tail.length > 4096) tail = tail.slice(-4096);
152
- const lines = text.split("\n").filter((l) => l.trim());
153
- const last = lines[lines.length - 1];
154
- if (last && onOutput) onOutput(last.trim().substring(0, 120));
155
- };
156
-
157
- child.stdout?.on("data", handleData);
158
- child.stderr?.on("data", handleData);
159
-
160
- child.on("error", (err) => reject(new Error(err.message)));
161
- child.on("close", (code) => {
162
- if (code !== 0) {
163
- reject(new Error(tail.slice(-500) || `npm install exited with code ${code}`));
164
- } else {
165
- resolve();
166
- }
167
- });
168
- });
169
- }
170
-
171
- /**
172
- * Install the given packages into the managed directory.
173
- *
174
- * Per-package progress is reported via `progress`. Installation is
175
- * sequential (not concurrent) so a failure stops the chain — matching
176
- * the behavior of the Electron wizard today. The return value reports
177
- * which packages completed successfully before any failure.
178
- */
179
- export async function bootstrapInstall(
180
- opts: BootstrapInstallOptions,
181
- ): Promise<BootstrapInstallResult> {
182
- const managedDir = opts.managedDir ?? getManagedDir();
183
- ensureManagedDir(managedDir);
184
-
185
- const argvBase = resolveNpmArgv(opts);
186
- const env = { ...process.env, ...(opts.env ?? {}) };
187
-
188
- const installed: string[] = [];
189
- for (const pkg of opts.packages) {
190
- const step = pkg.split("/").pop() || pkg;
191
- opts.progress?.({ step, status: "running" });
192
- try {
193
- await runNpmOnce(argvBase, [pkg], managedDir, env, (output) => {
194
- opts.progress?.({ step, status: "running", output });
195
- });
196
- opts.progress?.({ step, status: "done" });
197
- installed.push(pkg);
198
- } catch (err) {
199
- const message = err instanceof Error ? err.message : String(err);
200
- opts.progress?.({ step, status: "error", error: message });
201
- return { ok: false, error: message, installed, managedDir };
202
- }
203
- }
204
-
205
- return { ok: true, installed, managedDir };
206
- }
207
-
208
- /**
209
- * Convenience wrapper: install pi, openspec, tsx into the default
210
- * managed directory. Used by the CLI degraded-mode first-run path.
211
- */
212
- export async function bootstrapInstallDefaults(
213
- progress?: ProgressCallback,
214
- ): Promise<BootstrapInstallResult> {
215
- return bootstrapInstall({
216
- packages: ["@earendil-works/pi-coding-agent", "@fission-ai/openspec"],
217
- progress,
218
- });
219
- }
220
-
221
- // ── Managed Node runtime install ───────────────────────────────────────
222
- //
223
- // See change: embed-managed-node-runtime (spec: managed-node-runtime).
224
- //
225
- // `installManagedNode` copies a bundled Node distribution into
226
- // `<managedDir>/node/` and writes a `<managedDir>/node/.version` marker.
227
- // Idempotent: skip when marker matches the bundled version, replace on
228
- // mismatch, no-op when the bundled source isn't available (standalone
229
- // CLI install with no Electron resources).
230
-
231
- export interface InstallManagedNodeOptions {
232
- /**
233
- * Source directory containing the bundled Node distribution.
234
- * Layout matches the upstream Node zip/tar:
235
- * Windows: `<dir>/node.exe`, `<dir>/npm.cmd`, `<dir>/npx.cmd`
236
- * Unix: `<dir>/bin/node`, `<dir>/bin/npm`, `<dir>/bin/npx`
237
- *
238
- * Caller resolves this (Electron uses `path.dirname(getBundledNodePath())`
239
- * after stripping the platform-specific suffix). Pass `null` /
240
- * `undefined` for the standalone CLI install case — the function
241
- * no-ops without error.
242
- */
243
- bundledNodeDir?: string | null;
244
- /** Root of the managed install. Defaults to `getManagedDir()`. */
245
- managedDir?: string;
246
- /** Called on every progress tick. Mirrors `bootstrapInstall` shape. */
247
- progress?: ProgressCallback;
248
- /**
249
- * Test seam: override how the bundled Node version is read. Default
250
- * spawns `<bundledNodeDir>/bin/node --version` (or `node.exe` on
251
- * Windows) and trims stdout. Tests inject a fake to avoid real spawns.
252
- */
253
- _readVersion?: (sourceBinary: string) => string | null;
254
- }
255
-
256
- export interface InstallManagedNodeResult {
257
- /** True iff the operation succeeded (including the no-op cases). */
258
- ok: boolean;
259
- /** Did we actually copy files? false when no-op or skipped. */
260
- copied: boolean;
261
- /** Resolved managed Node directory (`<managedDir>/node`). */
262
- managedNodeDir: string;
263
- /** Bundled Node version (e.g. `v22.12.0`) when known. */
264
- version?: string;
265
- /** Reason for skip / failure. Always set when `copied === false`. */
266
- reason?: string;
267
- /** Error message when `ok === false`. */
268
- error?: string;
269
- }
270
-
271
- /** Path to the source `node` / `node.exe` binary inside `bundledNodeDir`. */
272
- function sourceNodeBinary(bundledNodeDir: string): string {
273
- return process.platform === "win32" // platform-branch-ok: Node distribution layout differs Windows vs Unix
274
- ? path.join(bundledNodeDir, "node.exe")
275
- : path.join(bundledNodeDir, "bin", "node");
276
- }
277
-
278
- /**
279
- * Spawn `<nodeBinary> --version` and return the trimmed stdout (e.g.
280
- * `v22.12.0`). Returns null on failure or when the binary is missing.
281
- * Synchronous because bootstrap is naturally serial.
282
- */
283
- function readNodeVersion(nodeBinary: string): string | null {
284
- if (!existsSync(nodeBinary)) return null;
285
- try {
286
- const r = cpSpawnSync(nodeBinary, ["--version"], {
287
- stdio: ["ignore", "pipe", "pipe"],
288
- timeout: 5_000,
289
- encoding: "utf-8",
290
- });
291
- if (r.status !== 0) return null;
292
- const out = (r.stdout ?? "").toString().trim();
293
- return out || null;
294
- } catch {
295
- return null;
296
- }
297
- }
298
-
299
- /** Read the `<managedNodeDir>/.version` marker if present, else null. */
300
- function readManagedMarker(managedNodeDir: string): string | null {
301
- const markerPath = path.join(managedNodeDir, ".version");
302
- if (!existsSync(markerPath)) return null;
303
- try {
304
- return readFileSync(markerPath, "utf-8").trim() || null;
305
- } catch {
306
- return null;
307
- }
308
- }
309
-
310
- /**
311
- * Idempotently copy the bundled Node distribution into `<managedDir>/node/`.
312
- *
313
- * - First-run: full recursive copy + write `.version` marker.
314
- * - Re-run with matching marker: no-op.
315
- * - Mismatched marker (or missing marker with dir present): replace +
316
- * rewrite marker.
317
- * - Bundled source absent or `bundledNodeDir == null`: no-op.
318
- * - Failed copy mid-flight: marker NOT written, so next call retries.
319
- */
320
- export async function installManagedNode(
321
- opts: InstallManagedNodeOptions = {},
322
- ): Promise<InstallManagedNodeResult> {
323
- const managedDir = opts.managedDir ?? getManagedDir();
324
- const managedNodeDir = path.join(managedDir, "node");
325
- const step = "node-runtime";
326
-
327
- const bundledDir = opts.bundledNodeDir ?? null;
328
- if (!bundledDir) {
329
- return {
330
- ok: true,
331
- copied: false,
332
- managedNodeDir,
333
- reason: "no bundled source",
334
- };
335
- }
336
-
337
- const sourceBinary = sourceNodeBinary(bundledDir);
338
- const sourceVersion = (opts._readVersion ?? readNodeVersion)(sourceBinary);
339
- if (!sourceVersion) {
340
- return {
341
- ok: true,
342
- copied: false,
343
- managedNodeDir,
344
- reason: `bundled node binary missing or unreadable: ${sourceBinary}`,
345
- };
346
- }
347
-
348
- const existingMarker = readManagedMarker(managedNodeDir);
349
- if (existingMarker === sourceVersion) {
350
- return {
351
- ok: true,
352
- copied: false,
353
- managedNodeDir,
354
- version: sourceVersion,
355
- reason: "version matches bundled — no copy needed",
356
- };
357
- }
358
-
359
- opts.progress?.({
360
- step,
361
- status: "running",
362
- output: `Installing Node ${sourceVersion} runtime`,
363
- });
364
-
365
- try {
366
- // Replace any existing dir (handles the mismatch + missing-marker
367
- // cases) so the copy is from a clean slate.
368
- if (existsSync(managedNodeDir)) {
369
- rmSync(managedNodeDir, { recursive: true, force: true });
370
- }
371
- mkdirSync(path.dirname(managedNodeDir), { recursive: true });
372
-
373
- cpSync(bundledDir, managedNodeDir, {
374
- recursive: true,
375
- force: true,
376
- // dereference: false keeps symlinks-as-symlinks (Unix npm shim).
377
- // verbatimSymlinks would also work in newer Node; default is fine.
378
- });
379
-
380
- // Marker last — partial copy on failure leaves no marker, so the
381
- // next invocation treats the dir as missing and retries.
382
- writeFileSync(
383
- path.join(managedNodeDir, ".version"),
384
- sourceVersion + "\n",
385
- "utf-8",
386
- );
387
-
388
- opts.progress?.({ step, status: "done", output: `Node ${sourceVersion}` });
389
- return {
390
- ok: true,
391
- copied: true,
392
- managedNodeDir,
393
- version: sourceVersion,
394
- };
395
- } catch (err) {
396
- const message = err instanceof Error ? err.message : String(err);
397
- opts.progress?.({ step, status: "error", error: message });
398
- return {
399
- ok: false,
400
- copied: false,
401
- managedNodeDir,
402
- version: sourceVersion,
403
- error: message,
404
- };
405
- }
406
- }