@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
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type {
3
+ IntentNode,
4
+ ActionDescriptor,
5
+ PluginIntentsMessage,
6
+ PluginActionMessage,
7
+ } from "../dashboard-plugin/intent-types.js";
8
+
9
+ describe("intent-types — wire format", () => {
10
+ it("IntentNode round-trips through JSON.stringify + JSON.parse losslessly", () => {
11
+ const intent: IntentNode = {
12
+ primitive: "ui:action-list",
13
+ props: {
14
+ actions: [
15
+ { label: "Run Flow X" },
16
+ { label: "Run Flow Y" },
17
+ ],
18
+ },
19
+ key: "actions-1",
20
+ actions: {
21
+ onClick: {
22
+ pluginId: "flows",
23
+ action: "flow.run",
24
+ payload: { flow: "X" },
25
+ } satisfies ActionDescriptor,
26
+ },
27
+ };
28
+
29
+ const roundTrip = JSON.parse(JSON.stringify(intent)) as IntentNode;
30
+ expect(roundTrip).toEqual(intent);
31
+ expect(roundTrip.primitive).toBe("ui:action-list");
32
+ expect((roundTrip.actions?.onClick as ActionDescriptor).pluginId).toBe("flows");
33
+ });
34
+
35
+ it("nested IntentNode inside props survives round-trip", () => {
36
+ const inner: IntentNode = {
37
+ primitive: "ui:markdown",
38
+ props: { content: "## Hello" },
39
+ };
40
+
41
+ const outer: IntentNode = {
42
+ primitive: "ui:agent-card",
43
+ props: {
44
+ name: "Explore",
45
+ status: "running",
46
+ body: inner,
47
+ },
48
+ };
49
+
50
+ const roundTrip = JSON.parse(JSON.stringify(outer)) as IntentNode;
51
+ expect(roundTrip).toEqual(outer);
52
+
53
+ // Nested IntentNode is still structurally an IntentNode after parse
54
+ const nestedBody = (roundTrip.props as Record<string, unknown>).body as IntentNode;
55
+ expect(nestedBody.primitive).toBe("ui:markdown");
56
+ expect((nestedBody.props as Record<string, unknown>).content).toBe("## Hello");
57
+ });
58
+
59
+ it("PluginIntentsMessage envelope is well-typed", () => {
60
+ const msg: PluginIntentsMessage = {
61
+ type: "plugin_intents",
62
+ pluginId: "flows",
63
+ sessionId: "abc-123",
64
+ slot: "session-card-action-bar",
65
+ intent: {
66
+ primitive: "ui:action-list",
67
+ props: { actions: [] },
68
+ },
69
+ };
70
+
71
+ const roundTrip = JSON.parse(JSON.stringify(msg)) as PluginIntentsMessage;
72
+ expect(roundTrip.type).toBe("plugin_intents");
73
+ expect(roundTrip.slot).toBe("session-card-action-bar");
74
+ expect(roundTrip.sessionId).toBe("abc-123");
75
+ });
76
+
77
+ it("PluginIntentsMessage with null intent (clear semantics) round-trips", () => {
78
+ const msg: PluginIntentsMessage = {
79
+ type: "plugin_intents",
80
+ pluginId: "flows",
81
+ sessionId: "abc-123",
82
+ slot: "content-view",
83
+ intent: null,
84
+ };
85
+
86
+ const roundTrip = JSON.parse(JSON.stringify(msg)) as PluginIntentsMessage;
87
+ expect(roundTrip.intent).toBeNull();
88
+ });
89
+
90
+ it("PluginActionMessage envelope is well-typed", () => {
91
+ const msg: PluginActionMessage = {
92
+ type: "plugin_action",
93
+ pluginId: "flows",
94
+ sessionId: "abc-123",
95
+ action: "flow.run",
96
+ payload: { flow: "X" },
97
+ };
98
+
99
+ const roundTrip = JSON.parse(JSON.stringify(msg)) as PluginActionMessage;
100
+ expect(roundTrip.type).toBe("plugin_action");
101
+ expect(roundTrip.action).toBe("flow.run");
102
+ expect((roundTrip.payload as { flow: string }).flow).toBe("X");
103
+ });
104
+
105
+ it("sessionId may be null for global slots", () => {
106
+ const msg: PluginIntentsMessage = {
107
+ type: "plugin_intents",
108
+ pluginId: "honcho",
109
+ sessionId: null,
110
+ slot: "settings-section",
111
+ intent: {
112
+ primitive: "ui:status-pill",
113
+ props: { state: "connected", text: "ready" },
114
+ },
115
+ };
116
+
117
+ const roundTrip = JSON.parse(JSON.stringify(msg)) as PluginIntentsMessage;
118
+ expect(roundTrip.sessionId).toBeNull();
119
+ });
120
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Repo-lint: assert `JITI_PACKAGES` is identical between the two source
3
+ * sites that resolve jiti at process startup.
4
+ *
5
+ * 1. `packages/shared/src/platform/binary-lookup.ts` (canonical, used
6
+ * by the server-launcher / electron / doctor / cli runtime path).
7
+ * 2. `packages/server/bin/pi-dashboard.mjs` (bin wrapper, runs before
8
+ * any TS loader is registered so it cannot import the canonical
9
+ * module and must inline the constant).
10
+ *
11
+ * If these drift, a clean-machine `npm i -g pi-dashboard && pi-dashboard`
12
+ * boots inconsistently: the bin wrapper might accept a jiti spec the
13
+ * cli.ts's daemon respawn rejects (or vice-versa). Caught us once with
14
+ * `@oh-my-pi/jiti` in v0.5.3.
15
+ *
16
+ * The lint is a string-parse rather than an import because the bin
17
+ * wrapper is ESM-not-TS and we want to fail fast even if a future move
18
+ * to a CJS-only environment breaks dynamic import.
19
+ *
20
+ * See change: enable-standalone-npm-install task 7.3.
21
+ */
22
+ import { describe, it, expect } from "vitest";
23
+ import { readFileSync } from "node:fs";
24
+ import path from "node:path";
25
+ import url from "node:url";
26
+
27
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
28
+ const REPO_ROOT = path.resolve(here, "..", "..", "..", "..");
29
+
30
+ const BIN_PATH = path.join(REPO_ROOT, "packages/server/bin/pi-dashboard.mjs");
31
+ const SHARED_PATH = path.join(REPO_ROOT, "packages/shared/src/platform/binary-lookup.ts");
32
+
33
+ /**
34
+ * Parse a `const JITI_PACKAGES = [ ... ]` (with optional `export` /
35
+ * `as const`) and return the array of string literals in declared order.
36
+ * Returns `null` if no such declaration is found.
37
+ */
38
+ function parseJitiPackagesArray(source: string): string[] | null {
39
+ // Match either:
40
+ // const JITI_PACKAGES = ["a", "b"];
41
+ // export const JITI_PACKAGES = ["a", "b"] as const;
42
+ const decl = source.match(
43
+ /(?:export\s+)?const\s+JITI_PACKAGES\s*(?::[^=]+)?=\s*\[([^\]]+)\]/,
44
+ );
45
+ if (!decl) return null;
46
+ const inner = decl[1]!;
47
+ const items: string[] = [];
48
+ // Pull every quoted string in order. Accepts double or single quotes.
49
+ for (const m of inner.matchAll(/['"]([^'"]+)['"]/g)) {
50
+ items.push(m[1]!);
51
+ }
52
+ return items;
53
+ }
54
+
55
+ describe("JITI_PACKAGES parity (binary-lookup.ts ⇔ bin/pi-dashboard.mjs)", () => {
56
+ it("both sites declare the same array in the same order", () => {
57
+ const binSrc = readFileSync(BIN_PATH, "utf-8");
58
+ const sharedSrc = readFileSync(SHARED_PATH, "utf-8");
59
+
60
+ const binList = parseJitiPackagesArray(binSrc);
61
+ const sharedList = parseJitiPackagesArray(sharedSrc);
62
+
63
+ expect(binList, `JITI_PACKAGES not found in ${BIN_PATH}`).not.toBeNull();
64
+ expect(sharedList, `JITI_PACKAGES not found in ${SHARED_PATH}`).not.toBeNull();
65
+
66
+ expect(binList).toEqual(sharedList);
67
+ });
68
+
69
+ it("primary lookup is bare \"jiti\" (regression for v0.5.3 fork drift)", () => {
70
+ const binList = parseJitiPackagesArray(readFileSync(BIN_PATH, "utf-8"));
71
+ const sharedList = parseJitiPackagesArray(readFileSync(SHARED_PATH, "utf-8"));
72
+ // Plain "jiti" is what `dependencies.jiti` in packages/server/package.json
73
+ // installs. It MUST be the first candidate or the bin wrapper will look up
74
+ // the wrong package on a clean install.
75
+ expect(binList?.[0]).toBe("jiti");
76
+ expect(sharedList?.[0]).toBe("jiti");
77
+ });
78
+
79
+ it("does NOT contain @oh-my-pi/jiti (removed by 2026-05-08-migrate-pi-fork-to-earendil)", () => {
80
+ const binList = parseJitiPackagesArray(readFileSync(BIN_PATH, "utf-8"));
81
+ const sharedList = parseJitiPackagesArray(readFileSync(SHARED_PATH, "utf-8"));
82
+ expect(binList).not.toContain("@oh-my-pi/jiti");
83
+ expect(sharedList).not.toContain("@oh-my-pi/jiti");
84
+ });
85
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { detectLegacyManagedDir } from "../legacy-managed-dir.js";
6
+
7
+ describe("legacy-managed-dir", () => {
8
+ let tmpHome: string;
9
+
10
+ beforeEach(() => {
11
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "legacy-managed-dir-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(tmpHome, { recursive: true, force: true });
16
+ });
17
+
18
+ it("returns present:false when directory does not exist", () => {
19
+ const res = detectLegacyManagedDir({ homedir: tmpHome });
20
+ expect(res).toEqual({ present: false });
21
+ });
22
+
23
+ it("returns present:false when path is a file, not a directory", () => {
24
+ fs.writeFileSync(path.join(tmpHome, ".pi-dashboard"), "garbage");
25
+ const res = detectLegacyManagedDir({ homedir: tmpHome });
26
+ expect(res).toEqual({ present: false });
27
+ });
28
+
29
+ it("returns present:true with zero pkgCount when dir exists but has no node_modules", () => {
30
+ fs.mkdirSync(path.join(tmpHome, ".pi-dashboard"), { recursive: true });
31
+ const res = detectLegacyManagedDir({ homedir: tmpHome });
32
+ expect(res.present).toBe(true);
33
+ if (res.present) {
34
+ expect(res.pkgCount).toBe(0);
35
+ expect(res.path).toContain(".pi-dashboard");
36
+ expect(res.sizeMb).toBeGreaterThanOrEqual(0);
37
+ }
38
+ });
39
+
40
+ it("counts direct children under node_modules/ as pkgCount", () => {
41
+ const nm = path.join(tmpHome, ".pi-dashboard", "node_modules");
42
+ fs.mkdirSync(path.join(nm, "foo"), { recursive: true });
43
+ fs.mkdirSync(path.join(nm, "bar"), { recursive: true });
44
+ fs.mkdirSync(path.join(nm, "@scope"), { recursive: true });
45
+ const res = detectLegacyManagedDir({ homedir: tmpHome });
46
+ expect(res.present).toBe(true);
47
+ if (res.present) expect(res.pkgCount).toBe(3);
48
+ });
49
+
50
+ it("computes a non-zero sizeMb when content exists", () => {
51
+ const dir = path.join(tmpHome, ".pi-dashboard");
52
+ fs.mkdirSync(dir, { recursive: true });
53
+ // Write 2 MB of bytes
54
+ fs.writeFileSync(path.join(dir, "blob.bin"), Buffer.alloc(2 * 1024 * 1024));
55
+ const res = detectLegacyManagedDir({ homedir: tmpHome });
56
+ expect(res.present).toBe(true);
57
+ if (res.present) expect(res.sizeMb).toBeGreaterThanOrEqual(2);
58
+ });
59
+ });
@@ -24,6 +24,18 @@ const ALLOWLIST: readonly string[] = [
24
24
  // See change: consolidate-windows-spawn-and-platform-handlers.
25
25
  "packages/shared/src/platform/detached-spawn.ts",
26
26
  "packages/shared/src/platform/subprocess-adapter.ts",
27
+ // Legacy-pi cleanup needs a synchronous npm-root probe at server
28
+ // startup; predates the platform/exec wrapper. See origin commit
29
+ // ab711621 (feat(bootstrap): detect + one-click cleanup of legacy
30
+ // @mariozechner/pi-coding-agent).
31
+ "packages/server/src/legacy-pi-cleanup.ts",
32
+ // The startup recovery HTTP server runs precisely when top-level
33
+ // dependencies are missing (corrupted node_modules) — importing the
34
+ // platform/exec wrapper there would defeat the recovery flow because
35
+ // its transitive deps may be the very things that are missing. The
36
+ // file's own header explicitly mandates: "Keep it dependency-free."
37
+ // See change: add-startup-recovery-server (commit e606e8b0).
38
+ "packages/server/src/recovery-server.ts",
27
39
  ];
28
40
 
29
41
  /**
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Repo-lint: Electron callers of launchDashboardServer must pass an
3
+ * explicit `nodeBin:` OR set `ELECTRON_RUN_AS_NODE` in the env, so the
4
+ * spawned process uses a real Node binary instead of the Electron GUI
5
+ * binary (which silently re-launches the app and exits on the
6
+ * single-instance lock).
7
+ *
8
+ * Also enforces that only `pick-node.ts` may reference `process.execPath`
9
+ * inside `packages/electron/src/lib/**`.
10
+ *
11
+ * See design D4 in openspec/changes/fix-electron-server-launch-node-bin/design.md.
12
+ */
13
+ import { describe, it, expect } from "vitest";
14
+ import fs from "node:fs/promises";
15
+ import { Dirent } from "node:fs";
16
+ import path from "node:path";
17
+ import url from "node:url";
18
+
19
+ /**
20
+ * The only file in packages/electron/src/lib/ that may reference
21
+ * process.execPath directly (it wraps the value inside PickNodeInput
22
+ * and passes it through rather than using it as a Node binary itself).
23
+ */
24
+ const EXECPATH_ALLOWLIST = new Set(["pick-node.ts"]);
25
+
26
+ /** Walk .ts/.tsx files, excluding node_modules, dist, and __tests__. */
27
+ async function* walk(dir: string): AsyncGenerator<string> {
28
+ let entries: Dirent[];
29
+ try {
30
+ entries = await fs.readdir(dir, { withFileTypes: true });
31
+ } catch {
32
+ return;
33
+ }
34
+ for (const entry of entries) {
35
+ const full = path.join(dir, entry.name);
36
+ if (entry.isDirectory()) {
37
+ if (["node_modules", "dist", "__tests__"].includes(entry.name)) continue;
38
+ yield* walk(full);
39
+ } else if (entry.isFile() && /\.(ts|tsx|mts|cts)$/.test(entry.name)) {
40
+ yield full;
41
+ }
42
+ }
43
+ }
44
+
45
+ describe("no-electron-execpath-spawn", () => {
46
+ it("launchDashboardServer calls in electron/lib include nodeBin or ELECTRON_RUN_AS_NODE", async () => {
47
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
48
+ const repoRoot = path.resolve(here, "..", "..", "..", "..");
49
+ const electronLibDir = path.join(repoRoot, "packages", "electron", "src", "lib");
50
+
51
+ const violations: Array<{ file: string; line: number; text: string }> = [];
52
+
53
+ for await (const file of walk(electronLibDir)) {
54
+ const content = await fs.readFile(file, "utf-8");
55
+ // Find all launchDashboardServer call sites. We look for the function
56
+ // name on a line, then scan forward for the matching closing paren to
57
+ // extract the options object text.
58
+ const lines = content.split(/\r?\n/);
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i]!;
61
+ if (!line.includes("launchDashboardServer(")) continue;
62
+
63
+ // Collect the call body: from this line to the matching closing paren.
64
+ let depth = 0;
65
+ let body = "";
66
+ for (let j = i; j < lines.length; j++) {
67
+ const l = lines[j]!;
68
+ body += l + "\n";
69
+ for (const ch of l) {
70
+ if (ch === "(") depth++;
71
+ else if (ch === ")") depth--;
72
+ }
73
+ if (depth === 0 && body.includes("launchDashboardServer(")) break;
74
+ }
75
+
76
+ const hasNodeBin = /\bnodeBin\s*:/.test(body);
77
+ const hasElectronRunAsNode = /ELECTRON_RUN_AS_NODE/.test(body);
78
+
79
+ if (!hasNodeBin && !hasElectronRunAsNode) {
80
+ violations.push({
81
+ file: path.relative(repoRoot, file),
82
+ line: i + 1,
83
+ text: line.trim(),
84
+ });
85
+ }
86
+ }
87
+ }
88
+
89
+ if (violations.length > 0) {
90
+ const msg =
91
+ `launchDashboardServer() called from Electron lib without nodeBin: or ELECTRON_RUN_AS_NODE.\n` +
92
+ `In Electron, process.execPath is the GUI binary — spawning it without ELECTRON_RUN_AS_NODE=1\n` +
93
+ `re-launches the app, hits the single-instance lock, and exits silently.\n\n` +
94
+ `Fix: call pickNodeForServer() and pass nodeBin: pick.nodeBin.\n` +
95
+ `See design D4: openspec/changes/fix-electron-server-launch-node-bin/design.md\n\n` +
96
+ `Offenders (${violations.length}):\n` +
97
+ violations.map((v) => ` ${v.file}:${v.line} ${v.text}`).join("\n");
98
+ expect(violations, msg).toHaveLength(0);
99
+ }
100
+ });
101
+
102
+ it("process.execPath not used directly as a node binary in electron/lib (outside pick-node.ts)", async () => {
103
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
104
+ const repoRoot = path.resolve(here, "..", "..", "..", "..");
105
+ const electronLibDir = path.join(repoRoot, "packages", "electron", "src", "lib");
106
+
107
+ // Pattern: process.execPath used as a binary path assignment (not as a
108
+ // processExecPath: injection key which is the permitted picker-call pattern).
109
+ // Flagged: nodeBin = ... ?? process.execPath
110
+ // Flagged: nodeBin: process.execPath
111
+ // Allowed: processExecPath: process.execPath (picker input)
112
+ // Allowed: string interpolation / log messages
113
+ const BINARY_EXECPATH_RE = /(?:nodeBin|cmd|bin)\s*(?:=|:)[^,;\n]*\bprocess\.execPath\b|\?\?\s*process\.execPath/;
114
+ // Safe patterns that should not be flagged even if they match above
115
+ const SAFE_RE = /processExecPath\s*:\s*process\.execPath/;
116
+
117
+ const violations: Array<{ file: string; line: number; text: string }> = [];
118
+
119
+ for await (const file of walk(electronLibDir)) {
120
+ const basename = path.basename(file);
121
+ if (EXECPATH_ALLOWLIST.has(basename)) continue;
122
+
123
+ const content = await fs.readFile(file, "utf-8");
124
+ const lines = content.split(/\r?\n/);
125
+ for (let i = 0; i < lines.length; i++) {
126
+ const line = lines[i]!;
127
+ if (BINARY_EXECPATH_RE.test(line) && !SAFE_RE.test(line)) {
128
+ violations.push({
129
+ file: path.relative(repoRoot, file),
130
+ line: i + 1,
131
+ text: line.trim(),
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ if (violations.length > 0) {
138
+ const msg =
139
+ `process.execPath used directly as a Node binary in packages/electron/src/lib.\n` +
140
+ `In Electron main, process.execPath is the GUI binary, not a Node interpreter.\n` +
141
+ `Use pickNodeForServer() from pick-node.ts to select the correct binary.\n\n` +
142
+ `Allowed file: ${[...EXECPATH_ALLOWLIST].join(", ")}\n` +
143
+ `Allowed pattern: processExecPath: process.execPath (picker injection)\n\n` +
144
+ `Offenders (${violations.length}):\n` +
145
+ violations.map((v) => ` ${v.file}:${v.line} ${v.text}`).join("\n");
146
+ expect(violations, msg).toHaveLength(0);
147
+ }
148
+ });
149
+ });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Repo-lint: dashboard plugins MUST NOT claim `command-route` for any
3
+ * `/flows*` command. Flow operations in the dashboard are button-driven
4
+ * (`SessionFlowActions`, `FlowDashboard` Abort, `FlowLaunchDialog`); the
5
+ * pi-flows extension itself still registers the slash commands for TUI.
6
+ *
7
+ * See change: fix-pi-flows-end-to-end (Group 8, task 8.5).
8
+ */
9
+ import { describe, it, expect } from "vitest";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const PACKAGES_DIR = path.resolve(__dirname, "..", "..", "..", "..");
16
+
17
+ interface PluginClaim {
18
+ slot?: string;
19
+ command?: string;
20
+ component?: string;
21
+ }
22
+
23
+ interface ManifestSlice {
24
+ id?: string;
25
+ claims?: PluginClaim[];
26
+ }
27
+
28
+ function readPluginManifests(): Array<{ pkg: string; manifest: ManifestSlice }> {
29
+ const out: Array<{ pkg: string; manifest: ManifestSlice }> = [];
30
+ for (const entry of fs.readdirSync(PACKAGES_DIR, { withFileTypes: true })) {
31
+ if (!entry.isDirectory()) continue;
32
+ const pkgJsonPath = path.join(PACKAGES_DIR, entry.name, "package.json");
33
+ if (!fs.existsSync(pkgJsonPath)) continue;
34
+ let raw: string;
35
+ try {
36
+ raw = fs.readFileSync(pkgJsonPath, "utf-8");
37
+ } catch {
38
+ continue;
39
+ }
40
+ let parsed: { "pi-dashboard-plugin"?: ManifestSlice };
41
+ try {
42
+ parsed = JSON.parse(raw);
43
+ } catch {
44
+ continue;
45
+ }
46
+ const m = parsed["pi-dashboard-plugin"];
47
+ if (m) out.push({ pkg: entry.name, manifest: m });
48
+ }
49
+ return out;
50
+ }
51
+
52
+ describe("repo-lint: no dashboard plugin claims command-route for /flows*", () => {
53
+ it("every monorepo plugin manifest is free of /flows* command-route claims", () => {
54
+ const offenders: string[] = [];
55
+ for (const { pkg, manifest } of readPluginManifests()) {
56
+ for (const claim of manifest.claims ?? []) {
57
+ if (claim.slot !== "command-route") continue;
58
+ if (!claim.command || !claim.command.startsWith("/flows")) continue;
59
+ offenders.push(`${pkg}: command "${claim.command}" → component "${claim.component ?? "?"}"`);
60
+ }
61
+ }
62
+ if (offenders.length > 0) {
63
+ throw new Error(
64
+ `Found dashboard plugin command-route claims for /flows*:\n${offenders.map((o) => " " + o).join("\n")}\n\n` +
65
+ "Dashboard flow operations are button-driven (SessionFlowActions / FlowDashboard / FlowLaunchDialog). " +
66
+ "pi-flows itself still registers /flows* commands for TUI use; the dashboard plugin manifest must not.",
67
+ );
68
+ }
69
+ expect(offenders).toEqual([]);
70
+ });
71
+ });