@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/AGENTS.md +67 -116
  2. package/README.md +93 -7
  3. package/docs/architecture.md +408 -9
  4. package/package.json +6 -4
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  7. package/packages/extension/src/__tests__/git-info.test.ts +67 -55
  8. package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
  9. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  10. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  11. package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
  12. package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
  13. package/packages/extension/src/bridge.ts +69 -2
  14. package/packages/extension/src/dev-build.ts +1 -1
  15. package/packages/extension/src/git-info.ts +9 -19
  16. package/packages/extension/src/pi-env.d.ts +1 -0
  17. package/packages/extension/src/process-scanner.ts +72 -38
  18. package/packages/extension/src/provider-register.ts +304 -16
  19. package/packages/extension/src/server-auto-start.ts +27 -1
  20. package/packages/extension/src/server-launcher.ts +71 -27
  21. package/packages/server/package.json +16 -2
  22. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  23. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  24. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  25. package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
  26. package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
  27. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  28. package/packages/server/src/__tests__/config-api.test.ts +68 -0
  29. package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
  30. package/packages/server/src/__tests__/directory-service.test.ts +234 -8
  31. package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
  32. package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
  33. package/packages/server/src/__tests__/extension-register.test.ts +3 -1
  34. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  35. package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
  36. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  37. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  38. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  39. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  40. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  41. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
  42. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
  43. package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
  44. package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
  45. package/packages/server/src/__tests__/process-manager.test.ts +45 -18
  46. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  47. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  48. package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
  49. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  50. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  51. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  52. package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
  53. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
  55. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  56. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  57. package/packages/server/src/__tests__/tunnel.test.ts +13 -7
  58. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  59. package/packages/server/src/bootstrap-queue.ts +130 -0
  60. package/packages/server/src/bootstrap-state.ts +131 -0
  61. package/packages/server/src/browse.ts +8 -3
  62. package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
  63. package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
  64. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  65. package/packages/server/src/cli.ts +256 -32
  66. package/packages/server/src/config-api.ts +16 -0
  67. package/packages/server/src/directory-service.ts +270 -39
  68. package/packages/server/src/editor-detection.ts +12 -9
  69. package/packages/server/src/editor-manager.ts +19 -4
  70. package/packages/server/src/editor-pid-registry.ts +9 -8
  71. package/packages/server/src/editor-registry.ts +22 -25
  72. package/packages/server/src/git-operations.ts +1 -1
  73. package/packages/server/src/headless-pid-registry.ts +7 -20
  74. package/packages/server/src/home-lock-release.ts +72 -0
  75. package/packages/server/src/home-lock.ts +389 -0
  76. package/packages/server/src/node-guard.ts +52 -0
  77. package/packages/server/src/package-manager-wrapper.ts +207 -47
  78. package/packages/server/src/pi-core-checker.ts +1 -1
  79. package/packages/server/src/pi-core-updater.ts +7 -1
  80. package/packages/server/src/pi-resource-scanner.ts +5 -8
  81. package/packages/server/src/pi-version-skew.ts +196 -0
  82. package/packages/server/src/preferences-store.ts +17 -3
  83. package/packages/server/src/process-manager.ts +403 -222
  84. package/packages/server/src/provider-probe.ts +234 -0
  85. package/packages/server/src/restart-helper.ts +130 -0
  86. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  87. package/packages/server/src/routes/openspec-routes.ts +25 -1
  88. package/packages/server/src/routes/pi-core-routes.ts +24 -1
  89. package/packages/server/src/routes/provider-auth-routes.ts +8 -8
  90. package/packages/server/src/routes/provider-routes.ts +43 -0
  91. package/packages/server/src/routes/recommended-routes.ts +10 -12
  92. package/packages/server/src/routes/system-routes.ts +20 -33
  93. package/packages/server/src/routes/tool-routes.ts +153 -0
  94. package/packages/server/src/server-pid.ts +5 -9
  95. package/packages/server/src/server.ts +211 -10
  96. package/packages/server/src/session-api.ts +77 -8
  97. package/packages/server/src/session-bootstrap.ts +17 -3
  98. package/packages/server/src/session-diff.ts +21 -21
  99. package/packages/server/src/terminal-manager.ts +61 -20
  100. package/packages/server/src/tunnel.ts +42 -28
  101. package/packages/shared/package.json +10 -3
  102. package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
  103. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  104. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  105. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  106. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  107. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  108. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  109. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  110. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  111. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  112. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  113. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  114. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  115. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  116. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  117. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  118. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  119. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  120. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  121. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  122. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  123. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  124. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  125. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  126. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  127. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  128. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  129. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  130. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  131. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  132. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  133. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  134. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  135. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  136. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  137. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  138. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  139. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  140. package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
  141. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  142. package/packages/shared/src/__tests__/config.test.ts +56 -0
  143. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  144. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  145. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  146. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  147. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  148. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  149. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  150. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  151. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  152. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  153. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  154. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  155. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  156. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  157. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  158. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  159. package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
  160. package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
  161. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  162. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  163. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
  164. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  165. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  166. package/packages/shared/src/bootstrap-install.ts +212 -0
  167. package/packages/shared/src/bridge-register.ts +87 -20
  168. package/packages/shared/src/browser-protocol.ts +71 -1
  169. package/packages/shared/src/config.ts +87 -15
  170. package/packages/shared/src/managed-paths.ts +31 -4
  171. package/packages/shared/src/openspec-poller.ts +63 -46
  172. package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
  173. package/packages/shared/src/platform/commands.ts +100 -0
  174. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  175. package/packages/shared/src/platform/exec.ts +220 -0
  176. package/packages/shared/src/platform/git.ts +155 -0
  177. package/packages/shared/src/platform/index.ts +15 -0
  178. package/packages/shared/src/platform/npm.ts +162 -0
  179. package/packages/shared/src/platform/openspec.ts +91 -0
  180. package/packages/shared/src/platform/paths.ts +276 -0
  181. package/packages/shared/src/platform/process-identify.ts +126 -0
  182. package/packages/shared/src/platform/process-scan.ts +94 -0
  183. package/packages/shared/src/platform/process.ts +168 -0
  184. package/packages/shared/src/platform/runner.ts +369 -0
  185. package/packages/shared/src/platform/shell.ts +44 -0
  186. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  187. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  188. package/packages/shared/src/recommended-extensions.ts +18 -2
  189. package/packages/shared/src/resolve-jiti.ts +62 -3
  190. package/packages/shared/src/rest-api.ts +26 -0
  191. package/packages/shared/src/semaphore.ts +83 -0
  192. package/packages/shared/src/tool-registry/definitions.ts +342 -0
  193. package/packages/shared/src/tool-registry/index.ts +56 -0
  194. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  195. package/packages/shared/src/tool-registry/registry.ts +262 -0
  196. package/packages/shared/src/tool-registry/strategies.ts +198 -0
  197. package/packages/shared/src/tool-registry/types.ts +180 -0
@@ -0,0 +1,130 @@
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
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * In-memory bootstrap state store for the dashboard server.
3
+ *
4
+ * Tracks the degraded-mode status during first-run pi install, upgrade
5
+ * operations, and version-skew detection. Subscribers (browser gateway,
6
+ * CLI progress printer) receive a snapshot on every `set()` call.
7
+ *
8
+ * See change: unified-bootstrap-install.
9
+ */
10
+
11
+ export type BootstrapStatus = "ready" | "installing" | "failed";
12
+
13
+ export interface BootstrapProgress {
14
+ /** Package / phase being processed (e.g. "pi-coding-agent", "bridge-register"). */
15
+ step: string;
16
+ /** Optional completion percentage (0..100). */
17
+ pct?: number;
18
+ /** Last line of npm output or other streaming context. */
19
+ output?: string;
20
+ }
21
+
22
+ export interface BootstrapError {
23
+ message: string;
24
+ stack?: string;
25
+ }
26
+
27
+ export interface BootstrapVersions {
28
+ pi?: string;
29
+ openspec?: string;
30
+ tsx?: string;
31
+ }
32
+
33
+ export interface BootstrapCompatibility {
34
+ minimum: string;
35
+ recommended: string;
36
+ /** null = no upper bound enforced yet. */
37
+ maximum: string | null;
38
+ /** Current resolved pi version, or undefined when pi is unresolved. */
39
+ current?: string;
40
+ /** Hint that the user should upgrade pi (below recommended). */
41
+ upgradeRecommended?: boolean;
42
+ /** Hint that the user should upgrade the dashboard itself (above maximum). */
43
+ upgradeDashboard?: boolean;
44
+ }
45
+
46
+ export interface BootstrapState {
47
+ status: BootstrapStatus;
48
+ progress?: BootstrapProgress;
49
+ error?: BootstrapError;
50
+ version?: BootstrapVersions;
51
+ compatibility?: BootstrapCompatibility;
52
+ /** Set when `registerBridgeExtension` fails after a successful install. */
53
+ bridgeRegistrationError?: string;
54
+ }
55
+
56
+ export type BootstrapListener = (state: BootstrapState) => void;
57
+
58
+ export interface BootstrapStateStore {
59
+ get(): BootstrapState;
60
+ /**
61
+ * Merge `partial` into the current state. Passing `undefined` for a
62
+ * key explicitly clears it (e.g. `set({ progress: undefined })` removes
63
+ * the progress line after completion). Broadcasts to all subscribers.
64
+ */
65
+ set(partial: Partial<BootstrapState>): void;
66
+ subscribe(listener: BootstrapListener): () => void;
67
+ /** Clear all listeners (used in tests + server shutdown). */
68
+ dispose(): void;
69
+ /**
70
+ * Record the package list used by the most recent `bootstrapInstall`
71
+ * call. Used by `POST /api/bootstrap/retry` to re-run the exact failed
72
+ * set rather than a hard-coded default. Not part of the WS-broadcast
73
+ * snapshot — it's purely side-channel metadata for the server.
74
+ * See change: unified-bootstrap-install (verification follow-up).
75
+ */
76
+ setLastInstallPackages(packages: readonly string[]): void;
77
+ /** Read the last install set. Returns a fresh copy. */
78
+ getLastInstallPackages(): string[];
79
+ }
80
+
81
+ /**
82
+ * Create a fresh bootstrap state store. `initial` is merged over the
83
+ * default `{ status: "ready" }`.
84
+ */
85
+ export function createBootstrapState(
86
+ initial?: Partial<BootstrapState>,
87
+ ): BootstrapStateStore {
88
+ let state: BootstrapState = { status: "ready", ...initial };
89
+ let lastInstallPackages: string[] = [];
90
+ const listeners = new Set<BootstrapListener>();
91
+
92
+ function notify(): void {
93
+ const snapshot = { ...state };
94
+ for (const l of listeners) {
95
+ try {
96
+ l(snapshot);
97
+ } catch (err) {
98
+ // Listener errors are non-fatal — log but continue.
99
+ console.error("[bootstrap-state] listener threw:", err);
100
+ }
101
+ }
102
+ }
103
+
104
+ return {
105
+ get() {
106
+ return { ...state };
107
+ },
108
+ set(partial) {
109
+ // Merge: explicit `undefined` in partial clears the field.
110
+ state = { ...state, ...partial } as BootstrapState;
111
+ // Strip keys whose value is undefined to keep the snapshot tidy.
112
+ for (const key of Object.keys(partial) as (keyof BootstrapState)[]) {
113
+ if (partial[key] === undefined) delete state[key];
114
+ }
115
+ notify();
116
+ },
117
+ subscribe(listener) {
118
+ listeners.add(listener);
119
+ return () => listeners.delete(listener);
120
+ },
121
+ dispose() {
122
+ listeners.clear();
123
+ },
124
+ setLastInstallPackages(packages) {
125
+ lastInstallPackages = [...packages];
126
+ },
127
+ getLastInstallPackages() {
128
+ return [...lastInstallPackages];
129
+ },
130
+ };
131
+ }
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import os from "node:os";
7
7
  import type { BrowseEntry, BrowseResult } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
8
+ import { isFilesystemRoot } from "@blackbelt-technology/pi-dashboard-shared/platform/paths.js";
8
9
 
9
10
  const MAX_ENTRIES = 200;
10
11
  const WORD_BOUNDARY_CHARS = new Set(["-", "_", ".", " ", "/"]);
@@ -85,10 +86,14 @@ export async function listDirectories(dirPath?: string, q?: string): Promise<Bro
85
86
  })
86
87
  );
87
88
 
88
- // Parent: null for root
89
- const parent = resolved === "/" ? null : path.dirname(resolved);
89
+ // Parent: null for any filesystem root (`/`, `C:\`, `\\server\share\`).
90
+ // Previously this was `resolved === "/"`, which only recognized the Unix
91
+ // root — on Windows `path.dirname("B:\\")` returns `"B:\\"`, so the
92
+ // picker showed a useless `..` entry at drive roots.
93
+ // See change: platform-path-normalization.
94
+ const parent = isFilesystemRoot(resolved) ? null : path.dirname(resolved);
90
95
 
91
- return { entries, parent, current: resolved };
96
+ return { entries, parent, current: resolved, platform: process.platform };
92
97
  }
93
98
 
94
99
  /**
@@ -4,10 +4,19 @@
4
4
  import type { BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
5
5
  import type { BrowserHandlerContext } from "./handler-context.js";
6
6
  import { safeRealpathSync } from "../resolve-path.js";
7
- import { execFile } from "node:child_process";
8
- import { promisify } from "node:util";
7
+ import { archiveCompleted as openspecArchiveCompleted } from "@blackbelt-technology/pi-dashboard-shared/platform/openspec.js";
8
+ import { normalizePath } from "@blackbelt-technology/pi-dashboard-shared/platform/paths.js";
9
9
 
10
- const execFileAsync = promisify(execFile);
10
+ /**
11
+ * Canonicalize a user-supplied path before storage: normalize separator /
12
+ * trailing-sep / case variants first, then resolve symlinks. Order matters
13
+ * — `realpath` can fail for not-yet-existing paths, so we keep its
14
+ * best-effort fallback but ensure we first have a sane string.
15
+ * See change: platform-path-normalization.
16
+ */
17
+ function canonicalizePath(input: string): string {
18
+ return safeRealpathSync(normalizePath(input));
19
+ }
11
20
 
12
21
  export function handlePinDirectory(
13
22
  msg: Extract<BrowserToServerMessage, { type: "pin_directory" }>,
@@ -15,7 +24,7 @@ export function handlePinDirectory(
15
24
  ): void {
16
25
  const { preferencesStore, directoryService, sessionManager, broadcast } = ctx;
17
26
  if (!preferencesStore) return;
18
- const resolved = safeRealpathSync(msg.path);
27
+ const resolved = canonicalizePath(msg.path);
19
28
  preferencesStore.pinDirectory(resolved);
20
29
  broadcast({ type: "pinned_dirs_updated", paths: preferencesStore.getPinnedDirectories() });
21
30
  if (directoryService) {
@@ -48,7 +57,7 @@ export function handleUnpinDirectory(
48
57
  ctx: BrowserHandlerContext,
49
58
  ): void {
50
59
  if (ctx.preferencesStore) {
51
- ctx.preferencesStore.unpinDirectory(safeRealpathSync(msg.path));
60
+ ctx.preferencesStore.unpinDirectory(canonicalizePath(msg.path));
52
61
  ctx.broadcast({ type: "pinned_dirs_updated", paths: ctx.preferencesStore.getPinnedDirectories() });
53
62
  }
54
63
  }
@@ -58,7 +67,10 @@ export function handleReorderPinnedDirs(
58
67
  ctx: BrowserHandlerContext,
59
68
  ): void {
60
69
  if (ctx.preferencesStore) {
61
- ctx.preferencesStore.reorderPinnedDirs(msg.paths.map(safeRealpathSync));
70
+ // Wrap in arrow fn: map's (elem, index, array) callback would pass
71
+ // the array index as canonicalizePath's 2nd arg, silently breaking
72
+ // platform detection. See platform-path-normalization.
73
+ ctx.preferencesStore.reorderPinnedDirs(msg.paths.map((p) => canonicalizePath(p)));
62
74
  ctx.broadcast({ type: "pinned_dirs_updated", paths: ctx.preferencesStore.getPinnedDirectories() });
63
75
  }
64
76
  }
@@ -89,8 +101,11 @@ export function handleOpenSpecBulkArchive(
89
101
  ctx: BrowserHandlerContext,
90
102
  ): void {
91
103
  if (ctx.directoryService) {
92
- execFileAsync("openspec", ["archive", "--completed"], { cwd: msg.cwd, timeout: 30000 })
93
- .catch(() => {})
104
+ // Delegate to the shared openspec tool module. The runner handles
105
+ // windowsHide, timeout, and argv-array escaping.
106
+ // See change: platform-command-executor.
107
+ openspecArchiveCompleted({ cwd: msg.cwd });
108
+ Promise.resolve()
94
109
  .then(() => ctx.directoryService!.refreshOpenSpec(msg.cwd))
95
110
  .then((data) => {
96
111
  if (data) ctx.broadcast({ type: "openspec_update", cwd: msg.cwd, data });