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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/AGENTS.md +64 -8
  2. package/README.md +308 -101
  3. package/docs/architecture.md +515 -16
  4. package/package.json +14 -7
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
  7. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  8. package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
  9. package/packages/extension/src/__tests__/git-info.test.ts +67 -55
  10. package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
  11. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  12. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  13. package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
  14. package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
  15. package/packages/extension/src/ask-user-tool.ts +289 -20
  16. package/packages/extension/src/bridge.ts +107 -6
  17. package/packages/extension/src/command-handler.ts +34 -39
  18. package/packages/extension/src/dev-build.ts +1 -1
  19. package/packages/extension/src/git-info.ts +9 -19
  20. package/packages/extension/src/pi-env.d.ts +1 -0
  21. package/packages/extension/src/process-scanner.ts +72 -38
  22. package/packages/extension/src/prompt-expander.ts +25 -4
  23. package/packages/extension/src/provider-register.ts +304 -16
  24. package/packages/extension/src/server-auto-start.ts +27 -1
  25. package/packages/extension/src/server-launcher.ts +71 -27
  26. package/packages/server/package.json +17 -2
  27. package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
  28. package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
  29. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  30. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  31. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  32. package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
  33. package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
  34. package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
  35. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  36. package/packages/server/src/__tests__/config-api.test.ts +68 -0
  37. package/packages/server/src/__tests__/cors.test.ts +34 -2
  38. package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
  39. package/packages/server/src/__tests__/directory-service.test.ts +234 -8
  40. package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
  41. package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
  42. package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
  43. package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
  44. package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
  45. package/packages/server/src/__tests__/extension-register.test.ts +3 -1
  46. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  47. package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
  48. package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
  49. package/packages/server/src/__tests__/git-operations.test.ts +9 -7
  50. package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
  51. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  52. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  53. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  54. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  55. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  56. package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
  57. package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
  58. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
  59. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
  60. package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
  61. package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
  62. package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
  63. package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
  64. package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
  65. package/packages/server/src/__tests__/process-manager.test.ts +45 -18
  66. package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
  67. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  68. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  69. package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
  70. package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
  71. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  72. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  73. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  74. package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
  75. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
  76. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
  77. package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
  78. package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
  79. package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
  80. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  81. package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
  82. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  83. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  84. package/packages/server/src/__tests__/tunnel.test.ts +103 -6
  85. package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
  86. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  87. package/packages/server/src/bootstrap-queue.ts +130 -0
  88. package/packages/server/src/bootstrap-state.ts +131 -0
  89. package/packages/server/src/browse.ts +108 -9
  90. package/packages/server/src/browser-gateway.ts +16 -3
  91. package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
  92. package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
  93. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  94. package/packages/server/src/cli.ts +256 -32
  95. package/packages/server/src/config-api.ts +16 -0
  96. package/packages/server/src/directory-service.ts +270 -39
  97. package/packages/server/src/editor-detection.ts +12 -9
  98. package/packages/server/src/editor-manager.ts +39 -5
  99. package/packages/server/src/editor-pid-registry.ts +199 -0
  100. package/packages/server/src/editor-registry.ts +22 -25
  101. package/packages/server/src/fix-pty-permissions.ts +44 -0
  102. package/packages/server/src/git-operations.ts +1 -1
  103. package/packages/server/src/headless-pid-registry.ts +16 -20
  104. package/packages/server/src/home-lock-release.ts +72 -0
  105. package/packages/server/src/home-lock.ts +389 -0
  106. package/packages/server/src/node-guard.ts +52 -0
  107. package/packages/server/src/npm-search-proxy.ts +71 -0
  108. package/packages/server/src/openspec-tasks.ts +158 -0
  109. package/packages/server/src/package-manager-wrapper.ts +225 -34
  110. package/packages/server/src/pi-core-checker.ts +290 -0
  111. package/packages/server/src/pi-core-updater.ts +172 -0
  112. package/packages/server/src/pi-gateway.ts +7 -0
  113. package/packages/server/src/pi-resource-scanner.ts +5 -8
  114. package/packages/server/src/pi-version-skew.ts +196 -0
  115. package/packages/server/src/preferences-store.ts +17 -3
  116. package/packages/server/src/process-manager.ts +403 -222
  117. package/packages/server/src/provider-probe.ts +234 -0
  118. package/packages/server/src/restart-helper.ts +130 -0
  119. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  120. package/packages/server/src/routes/file-routes.ts +30 -3
  121. package/packages/server/src/routes/openspec-routes.ts +107 -1
  122. package/packages/server/src/routes/pi-core-routes.ts +140 -0
  123. package/packages/server/src/routes/provider-auth-routes.ts +12 -10
  124. package/packages/server/src/routes/provider-routes.ts +55 -2
  125. package/packages/server/src/routes/recommended-routes.ts +225 -0
  126. package/packages/server/src/routes/system-routes.ts +30 -34
  127. package/packages/server/src/routes/tool-routes.ts +153 -0
  128. package/packages/server/src/server-pid.ts +5 -9
  129. package/packages/server/src/server.ts +363 -26
  130. package/packages/server/src/session-api.ts +77 -8
  131. package/packages/server/src/session-bootstrap.ts +17 -3
  132. package/packages/server/src/session-diff.ts +21 -21
  133. package/packages/server/src/terminal-manager.ts +65 -20
  134. package/packages/server/src/test-env-guard.ts +26 -0
  135. package/packages/server/src/test-support/test-server.ts +63 -0
  136. package/packages/server/src/tunnel.ts +172 -34
  137. package/packages/shared/package.json +10 -3
  138. package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
  139. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  140. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  141. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  142. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  143. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  144. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  145. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  146. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  147. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  148. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  149. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  150. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  151. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  152. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  153. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  154. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  155. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  156. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  157. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  158. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  159. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  160. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  161. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  162. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  163. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  164. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  165. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  166. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  167. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  168. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  169. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  170. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  172. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  173. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  174. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  175. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  176. package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
  177. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  178. package/packages/shared/src/__tests__/config.test.ts +59 -3
  179. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  180. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  181. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  182. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  183. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  184. package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
  185. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  186. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  187. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  188. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  189. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  190. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  191. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  192. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  193. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  194. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  195. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  196. package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
  197. package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
  198. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  199. package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
  200. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  201. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
  202. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  203. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  204. package/packages/shared/src/bootstrap-install.ts +212 -0
  205. package/packages/shared/src/bridge-register.ts +87 -20
  206. package/packages/shared/src/browser-protocol.ts +93 -1
  207. package/packages/shared/src/config.ts +87 -15
  208. package/packages/shared/src/managed-paths.ts +31 -4
  209. package/packages/shared/src/openspec-poller.ts +71 -49
  210. package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
  211. package/packages/shared/src/platform/commands.ts +100 -0
  212. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  213. package/packages/shared/src/platform/exec.ts +220 -0
  214. package/packages/shared/src/platform/git.ts +155 -0
  215. package/packages/shared/src/platform/index.ts +15 -0
  216. package/packages/shared/src/platform/npm.ts +162 -0
  217. package/packages/shared/src/platform/openspec.ts +91 -0
  218. package/packages/shared/src/platform/paths.ts +276 -0
  219. package/packages/shared/src/platform/process-identify.ts +126 -0
  220. package/packages/shared/src/platform/process-scan.ts +94 -0
  221. package/packages/shared/src/platform/process.ts +168 -0
  222. package/packages/shared/src/platform/runner.ts +369 -0
  223. package/packages/shared/src/platform/shell.ts +44 -0
  224. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  225. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  226. package/packages/shared/src/recommended-extensions.ts +196 -0
  227. package/packages/shared/src/resolve-jiti.ts +62 -3
  228. package/packages/shared/src/rest-api.ts +97 -0
  229. package/packages/shared/src/semaphore.ts +83 -0
  230. package/packages/shared/src/source-matching.ts +126 -0
  231. package/packages/shared/src/test-support/setup-home.ts +74 -0
  232. package/packages/shared/src/tool-registry/definitions.ts +342 -0
  233. package/packages/shared/src/tool-registry/index.ts +56 -0
  234. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  235. package/packages/shared/src/tool-registry/registry.ts +262 -0
  236. package/packages/shared/src/tool-registry/strategies.ts +198 -0
  237. package/packages/shared/src/tool-registry/types.ts +180 -0
  238. package/packages/shared/src/types.ts +7 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Safe child-process wrappers that always set `windowsHide: true`.
3
+ *
4
+ * Rationale
5
+ * ─────────
6
+ * On Windows, spawning a `.cmd` shim (or anything node.exe wraps via cmd.exe)
7
+ * flashes a cmd-prompt window unless `windowsHide: true` is passed. This is
8
+ * a universal source of visible-UI bugs in this project: bridge process
9
+ * scanners, git polls, openspec polls, terminal subprocess checks, etc.
10
+ * Every spawn site needed to remember to set the flag, and we kept missing
11
+ * some (session-diff, git-operations, update-checker, doctor, tunnel, ...).
12
+ *
13
+ * Rather than fixing this per call site forever, this module wraps the
14
+ * Node `child_process` API with `windowsHide: true` as the default. Callers
15
+ * can still override by explicitly passing `windowsHide: false` if they
16
+ * genuinely want a visible console (none of our callers do).
17
+ *
18
+ * **Every spawn in packages/*\/src SHOULD import from here** instead of
19
+ * directly from `node:child_process`. A repo-level check can fail if
20
+ * direct imports sneak back in. See change: consolidate-platform-handlers.
21
+ */
22
+ import {
23
+ execSync as nodeExecSync,
24
+ exec as nodeExec,
25
+ execFile as nodeExecFile,
26
+ spawnSync as nodeSpawnSync,
27
+ spawn as nodeSpawn,
28
+ type ExecSyncOptions,
29
+ type ExecOptions,
30
+ type ExecFileOptions,
31
+ type SpawnSyncOptions,
32
+ type SpawnOptions,
33
+ type ChildProcess,
34
+ type SpawnSyncReturns,
35
+ } from "node:child_process";
36
+ import { promisify } from "node:util";
37
+
38
+ // ── Argv safety (Windows .cmd / .bat handling) ─────────────────────────────
39
+
40
+ /**
41
+ * Build a spawn-safe argv for ANY command on ANY platform.
42
+ *
43
+ * The canonical way to invoke subprocesses without flashing cmd.exe
44
+ * console windows on Windows. Handles three cases:
45
+ *
46
+ * 1. Windows + `.cmd` / `.bat` shim → explicit `cmd.exe /c <cmd> <args>`.
47
+ * This is the ONLY reliable way to invoke `.cmd` files without the
48
+ * flashing-console bug (Node issue #21825, which happens when
49
+ * `shell: true` is combined with `.cmd` + `detached` + `windowsHide`).
50
+ * cmd.exe respects `windowsHide: true` on its own console directly.
51
+ *
52
+ * 2. Windows + native binary (`.exe`) → direct argv.
53
+ *
54
+ * 3. Unix (any binary or shell script) → direct argv.
55
+ *
56
+ * Always returns `{ shell: false, windowsHide: true }` — NEVER uses
57
+ * `shell: true`. Callers pass these spawn options along with the argv.
58
+ *
59
+ * Example:
60
+ * const { argv, spawnOptions } = buildSafeArgv("npm.cmd", ["root", "-g"]);
61
+ * spawnSync(argv[0], argv.slice(1), { cwd, env, ...spawnOptions });
62
+ *
63
+ * See change: consolidate-windows-spawn-and-platform-handlers.
64
+ */
65
+ export interface SafeArgv {
66
+ argv: string[];
67
+ spawnOptions: { shell: false; windowsHide: true };
68
+ }
69
+
70
+ export function buildSafeArgv(
71
+ cmd: string,
72
+ args: readonly string[] = [],
73
+ platform: NodeJS.Platform = process.platform,
74
+ ): SafeArgv {
75
+ if (platform === "win32") {
76
+ // Route through cmd.exe for TWO cases:
77
+ // 1. Explicit .cmd/.bat shim — Node can't spawn these directly
78
+ // with shell:false (CVE-2024-27980 fix in Node >= 20.12).
79
+ // 2. Extensionless name (e.g. "npm", "pi", "git") — Windows
80
+ // resolves these via PATHEXT, but only shells do. Without
81
+ // cmd.exe, spawn("npm") returns ENOENT because there's no
82
+ // literal "npm" binary — just "npm.cmd".
83
+ // Native .exe / absolute paths bypass cmd.exe (no PATHEXT needed).
84
+ //
85
+ // /d = skip AutoRun, /s = treat quoted first token as command
86
+ // (preserves spaces), /c = run and exit. cmd.exe honors
87
+ // windowsHide on its console, so inner .cmd's node.exe inherits an
88
+ // invisible console — no flash.
89
+ const isShim = /\.(cmd|bat)$/i.test(cmd);
90
+ const hasExtension = /\.[A-Za-z0-9]+$/.test(cmd);
91
+ if (isShim || !hasExtension) {
92
+ return {
93
+ argv: ["cmd.exe", "/d", "/s", "/c", cmd, ...args],
94
+ spawnOptions: { shell: false, windowsHide: true },
95
+ };
96
+ }
97
+ }
98
+ return {
99
+ argv: [cmd, ...args],
100
+ spawnOptions: { shell: false, windowsHide: true },
101
+ };
102
+ }
103
+
104
+ // ── Option helpers ──────────────────────────────────────────────────────────
105
+
106
+ type AnyOptions = { windowsHide?: boolean } | undefined;
107
+
108
+ /**
109
+ * Merge caller options with `windowsHide: true` as the default.
110
+ * Explicit `windowsHide: false` from the caller is honored (for the rare
111
+ * case where a visible console is actually desired).
112
+ */
113
+ function withHide<T extends AnyOptions>(opts: T): T & { windowsHide: boolean } {
114
+ const hide = opts?.windowsHide ?? true;
115
+ return { ...(opts ?? {}), windowsHide: hide } as T & { windowsHide: boolean };
116
+ }
117
+
118
+ // ── Synchronous wrappers ────────────────────────────────────────────────────
119
+
120
+ /** Wrapped `execSync`. Always `windowsHide: true` unless overridden. */
121
+ export function execSync(command: string, options: ExecSyncOptions & { encoding: BufferEncoding }): string;
122
+ export function execSync(command: string, options?: ExecSyncOptions): Buffer | string;
123
+ export function execSync(
124
+ command: string,
125
+ options?: ExecSyncOptions,
126
+ ): Buffer | string {
127
+ return nodeExecSync(command, withHide(options));
128
+ }
129
+
130
+ /** Wrapped `spawnSync`. Always `windowsHide: true` unless overridden. */
131
+ export function spawnSync<T extends string | Buffer = Buffer>(
132
+ command: string,
133
+ args?: readonly string[],
134
+ options?: SpawnSyncOptions,
135
+ ): SpawnSyncReturns<T> {
136
+ return nodeSpawnSync(command, args ?? [], withHide(options)) as SpawnSyncReturns<T>;
137
+ }
138
+
139
+ // ── Asynchronous (callback) wrappers ────────────────────────────────────────
140
+
141
+ /** Wrapped `exec` (callback form). */
142
+ export function exec(
143
+ command: string,
144
+ callback?: (err: Error | null, stdout: string, stderr: string) => void,
145
+ ): ChildProcess;
146
+ export function exec(
147
+ command: string,
148
+ options: ExecOptions,
149
+ callback?: (err: Error | null, stdout: string | Buffer, stderr: string | Buffer) => void,
150
+ ): ChildProcess;
151
+ export function exec(
152
+ command: string,
153
+ optionsOrCallback?: ExecOptions | ((err: Error | null, stdout: any, stderr: any) => void),
154
+ maybeCallback?: (err: Error | null, stdout: any, stderr: any) => void,
155
+ ): ChildProcess {
156
+ if (typeof optionsOrCallback === "function") {
157
+ return nodeExec(command, withHide(undefined) as ExecOptions, optionsOrCallback);
158
+ }
159
+ return nodeExec(command, withHide(optionsOrCallback) as ExecOptions, maybeCallback);
160
+ }
161
+
162
+ /** Wrapped `execFile` (callback form). */
163
+ export function execFile(
164
+ file: string,
165
+ args: readonly string[] | undefined,
166
+ options: ExecFileOptions,
167
+ callback?: (err: Error | null, stdout: string | Buffer, stderr: string | Buffer) => void,
168
+ ): ChildProcess;
169
+ export function execFile(
170
+ file: string,
171
+ args?: readonly string[],
172
+ callback?: (err: Error | null, stdout: string, stderr: string) => void,
173
+ ): ChildProcess;
174
+ export function execFile(
175
+ file: string,
176
+ args?: readonly string[],
177
+ optionsOrCallback?: ExecFileOptions | ((err: Error | null, stdout: any, stderr: any) => void),
178
+ maybeCallback?: (err: Error | null, stdout: any, stderr: any) => void,
179
+ ): ChildProcess {
180
+ if (typeof optionsOrCallback === "function") {
181
+ return nodeExecFile(file, args ?? [], withHide(undefined) as ExecFileOptions, optionsOrCallback);
182
+ }
183
+ return nodeExecFile(file, args ?? [], withHide(optionsOrCallback) as ExecFileOptions, maybeCallback);
184
+ }
185
+
186
+ /** Wrapped `spawn`. Always `windowsHide: true` unless overridden. */
187
+ export function spawn(
188
+ command: string,
189
+ args?: readonly string[],
190
+ options?: SpawnOptions,
191
+ ): ChildProcess {
192
+ return nodeSpawn(command, args ?? [], withHide(options));
193
+ }
194
+
195
+ // ── Promise-returning variants ──────────────────────────────────────────────
196
+
197
+ /** Promise-returning exec. */
198
+ export const execAsync = promisify(exec) as unknown as (
199
+ command: string,
200
+ options?: ExecOptions,
201
+ ) => Promise<{ stdout: string | Buffer; stderr: string | Buffer }>;
202
+
203
+ /** Promise-returning execFile. */
204
+ export const execFileAsync = promisify(execFile) as unknown as (
205
+ file: string,
206
+ args?: readonly string[],
207
+ options?: ExecFileOptions,
208
+ ) => Promise<{ stdout: string | Buffer; stderr: string | Buffer }>;
209
+
210
+ // ── Types pass-through for convenience ──────────────────────────────────────
211
+
212
+ export type {
213
+ ExecSyncOptions,
214
+ ExecOptions,
215
+ ExecFileOptions,
216
+ SpawnSyncOptions,
217
+ SpawnOptions,
218
+ ChildProcess,
219
+ SpawnSyncReturns,
220
+ };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Git tool module — Recipe-based API for git operations the dashboard runs
3
+ * from multiple call sites (session-diff, git-info extension, doctor).
4
+ *
5
+ * Every function in this file is a thin wrapper over `run(recipe, input)`:
6
+ * no `child_process` imports, no `process.platform` branches, no inline
7
+ * shell-escape logic. The Recipe objects describe *what* git invocation
8
+ * to run; the runner handles *how* to spawn it safely.
9
+ *
10
+ * Exit codes:
11
+ * `git diff` exits 0 when there's a diff, 1 when there's nothing to
12
+ * show (we tolerate 1).
13
+ * Other commands exit 0 on success and non-zero on real errors.
14
+ *
15
+ * See change: platform-command-executor.
16
+ */
17
+ import { run, unwrap, type Recipe, type Result } from "./runner.js";
18
+
19
+ // ── Recipes (pure data) ─────────────────────────────────────────────────────
20
+
21
+ const GIT_TIMEOUT = 15_000;
22
+
23
+ interface WithCwd {
24
+ cwd: string;
25
+ }
26
+
27
+ export const GIT_IS_REPO: Recipe<WithCwd, boolean> = {
28
+ argv: () => ["git", "rev-parse", "--is-inside-work-tree"],
29
+ parse: (out) => out.trim() === "true",
30
+ timeout: GIT_TIMEOUT,
31
+ };
32
+
33
+ export const GIT_CURRENT_BRANCH: Recipe<WithCwd, string | undefined> = {
34
+ argv: () => ["git", "rev-parse", "--abbrev-ref", "HEAD"],
35
+ parse: (out) => out.trim() || undefined,
36
+ timeout: GIT_TIMEOUT,
37
+ };
38
+
39
+ export const GIT_HEAD_SHA: Recipe<WithCwd & { short?: boolean }, string | undefined> = {
40
+ argv: ({ short }) => short ? ["git", "rev-parse", "--short", "HEAD"] : ["git", "rev-parse", "HEAD"],
41
+ parse: (out) => out.trim() || undefined,
42
+ timeout: GIT_TIMEOUT,
43
+ };
44
+
45
+ export const GIT_REMOTE_URL: Recipe<WithCwd & { remote?: string }, string | undefined> = {
46
+ argv: ({ remote }) => ["git", "remote", "get-url", remote ?? "origin"],
47
+ parse: (out) => out.trim() || undefined,
48
+ timeout: GIT_TIMEOUT,
49
+ };
50
+
51
+ export const GIT_DIFF: Recipe<WithCwd & { path: string; ref?: string }, string> = {
52
+ argv: ({ path, ref }) => ["git", "diff", ref ?? "HEAD", "--", path],
53
+ parse: (out) => out,
54
+ timeout: GIT_TIMEOUT,
55
+ // git diff exits 1 when --exit-code is set or in some configurations;
56
+ // no diff is not an error for our callers.
57
+ tolerate: [1],
58
+ };
59
+
60
+ export const GIT_STATUS_PORCELAIN: Recipe<WithCwd & { path?: string }, string> = {
61
+ argv: ({ path }) =>
62
+ path === undefined
63
+ ? ["git", "status", "--porcelain"]
64
+ : ["git", "status", "--porcelain", "--", path],
65
+ parse: (out) => out,
66
+ timeout: GIT_TIMEOUT,
67
+ };
68
+
69
+ /**
70
+ * `gh pr view --json number -q .number` — requires the `gh` CLI.
71
+ * Returns undefined when there is no PR for the current branch (gh exits 1).
72
+ */
73
+ export const GH_PR_NUMBER: Recipe<WithCwd, number | undefined> = {
74
+ argv: () => ["gh", "pr", "view", "--json", "number", "-q", ".number"],
75
+ parse: (out) => {
76
+ const n = parseInt(out.trim(), 10);
77
+ return Number.isFinite(n) ? n : undefined;
78
+ },
79
+ timeout: GIT_TIMEOUT,
80
+ tolerate: [1], // gh exits 1 when no PR exists — not an error
81
+ };
82
+
83
+ // ── Registry (for lint / docs / enumeration) ────────────────────────────────
84
+
85
+ export const GIT_RECIPES = {
86
+ GIT_IS_REPO,
87
+ GIT_CURRENT_BRANCH,
88
+ GIT_HEAD_SHA,
89
+ GIT_REMOTE_URL,
90
+ GIT_DIFF,
91
+ GIT_STATUS_PORCELAIN,
92
+ GH_PR_NUMBER,
93
+ } as const;
94
+
95
+ // ── Public API — typed functions (use Result for explicit control) ──────────
96
+
97
+ export function isGitRepo(input: WithCwd): Result<boolean> {
98
+ return run(GIT_IS_REPO, input, { cwd: input.cwd });
99
+ }
100
+
101
+ export function currentBranch(input: WithCwd): Result<string | undefined> {
102
+ return run(GIT_CURRENT_BRANCH, input, { cwd: input.cwd });
103
+ }
104
+
105
+ export function headSha(input: WithCwd & { short?: boolean }): Result<string | undefined> {
106
+ return run(GIT_HEAD_SHA, input, { cwd: input.cwd });
107
+ }
108
+
109
+ export function remoteUrl(input: WithCwd & { remote?: string }): Result<string | undefined> {
110
+ return run(GIT_REMOTE_URL, input, { cwd: input.cwd });
111
+ }
112
+
113
+ export function diff(input: WithCwd & { path: string; ref?: string }): Result<string> {
114
+ return run(GIT_DIFF, input, { cwd: input.cwd });
115
+ }
116
+
117
+ export function statusPorcelain(input: WithCwd & { path?: string }): Result<string> {
118
+ return run(GIT_STATUS_PORCELAIN, input, { cwd: input.cwd });
119
+ }
120
+
121
+ export function prNumber(input: WithCwd): Result<number | undefined> {
122
+ return run(GH_PR_NUMBER, input, { cwd: input.cwd });
123
+ }
124
+
125
+ // ── Best-effort convenience wrappers (swallow errors → default) ─────────────
126
+ // Callers that only want "the value or a default" without dealing with Result
127
+ // discriminants can use these instead.
128
+
129
+ export function isGitRepoOr(input: WithCwd, fallback = false): boolean {
130
+ return unwrap(isGitRepo(input), fallback);
131
+ }
132
+
133
+ export function currentBranchOr(input: WithCwd, fallback?: string): string | undefined {
134
+ return unwrap(currentBranch(input), fallback);
135
+ }
136
+
137
+ export function headShaOr(input: WithCwd & { short?: boolean }, fallback?: string): string | undefined {
138
+ return unwrap(headSha(input), fallback);
139
+ }
140
+
141
+ export function remoteUrlOr(input: WithCwd & { remote?: string }, fallback?: string): string | undefined {
142
+ return unwrap(remoteUrl(input), fallback);
143
+ }
144
+
145
+ export function diffOr(input: WithCwd & { path: string; ref?: string }, fallback = ""): string {
146
+ return unwrap(diff(input), fallback);
147
+ }
148
+
149
+ export function statusPorcelainOr(input: WithCwd & { path?: string }, fallback = ""): string {
150
+ return unwrap(statusPorcelain(input), fallback);
151
+ }
152
+
153
+ export function prNumberOr(input: WithCwd, fallback?: number): number | undefined {
154
+ return unwrap(prNumber(input), fallback);
155
+ }
@@ -0,0 +1,15 @@
1
+ export * from "./binary-lookup.js";
2
+ export * from "./process.js";
3
+ export * from "./process-scan.js";
4
+ export * from "./shell.js";
5
+ export * from "./commands.js";
6
+ export * from "./exec.js";
7
+ export * from "./runner.js";
8
+ export * from "./detached-spawn.js";
9
+ export * from "./spawn-mechanism.js";
10
+ export * from "./process-identify.js";
11
+ export * from "./subprocess-adapter.js";
12
+ export * as git from "./git.js";
13
+ export * as openspec from "./openspec.js";
14
+ export * as npm from "./npm.js";
15
+ export * as paths from "./paths.js";
@@ -0,0 +1,162 @@
1
+ /**
2
+ * npm tool module — Recipe-based API for the npm CLI.
3
+ *
4
+ * Covers the subset of npm operations the dashboard actually invokes:
5
+ * - `npm root -g` (resolve the global node_modules path)
6
+ * - `npm outdated` (check for updates, local or global)
7
+ * - `npm install` (install a package, local or global)
8
+ * - `npm view <pkg> version` (read upstream version)
9
+ *
10
+ * See change: platform-command-executor.
11
+ */
12
+ import { run, unwrap, type Recipe, type Result } from "./runner.js";
13
+
14
+ const NPM_TIMEOUT_FAST = 10_000;
15
+ const NPM_TIMEOUT_INSTALL = 120_000;
16
+
17
+ interface WithCwd {
18
+ cwd?: string;
19
+ }
20
+
21
+ // ── Recipes ─────────────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * `npm root -g` — returns the absolute path to the global node_modules.
25
+ * Cached by callers (it's stable per Node install).
26
+ */
27
+ export const NPM_ROOT_GLOBAL: Recipe<Record<string, never>, string> = {
28
+ argv: () => ["npm", "root", "-g"],
29
+ parse: (out) => out.trim(),
30
+ timeout: NPM_TIMEOUT_FAST,
31
+ };
32
+
33
+ /**
34
+ * `npm outdated <pkg> --json` (or without `<pkg>` for project-wide).
35
+ * npm exits 1 when updates are available and 0 when up-to-date — we tolerate
36
+ * exit 1 so callers see the JSON body either way.
37
+ */
38
+ export const NPM_OUTDATED: Recipe<WithCwd & { pkg?: string }, unknown | null> = {
39
+ argv: ({ pkg }) => pkg === undefined
40
+ ? ["npm", "outdated", "--json"]
41
+ : ["npm", "outdated", pkg, "--json"],
42
+ parse: (out) => {
43
+ const trimmed = out.trim();
44
+ if (!trimmed) return null;
45
+ try { return JSON.parse(trimmed); } catch { return null; }
46
+ },
47
+ timeout: NPM_TIMEOUT_FAST,
48
+ tolerate: [1],
49
+ };
50
+
51
+ /**
52
+ * `npm outdated -g <pkg> --json`. Same exit-1 tolerance.
53
+ */
54
+ export const NPM_OUTDATED_GLOBAL: Recipe<{ pkg?: string }, unknown | null> = {
55
+ argv: ({ pkg }) => pkg === undefined
56
+ ? ["npm", "outdated", "-g", "--json"]
57
+ : ["npm", "outdated", "-g", pkg, "--json"],
58
+ parse: (out) => {
59
+ const trimmed = out.trim();
60
+ if (!trimmed) return null;
61
+ try { return JSON.parse(trimmed); } catch { return null; }
62
+ },
63
+ timeout: NPM_TIMEOUT_FAST,
64
+ tolerate: [1],
65
+ };
66
+
67
+ /**
68
+ * `npm install <pkg>@<version>` — local install. Long timeout.
69
+ */
70
+ export const NPM_INSTALL: Recipe<WithCwd & { pkg: string; version?: string }, string> = {
71
+ argv: ({ pkg, version }) => ["npm", "install", version ? `${pkg}@${version}` : pkg],
72
+ parse: (out) => out,
73
+ timeout: NPM_TIMEOUT_INSTALL,
74
+ };
75
+
76
+ /**
77
+ * `npm install -g <pkg>@<version>` — global install.
78
+ */
79
+ export const NPM_INSTALL_GLOBAL: Recipe<{ pkg: string; version?: string }, string> = {
80
+ argv: ({ pkg, version }) => ["npm", "install", "-g", version ? `${pkg}@${version}` : pkg],
81
+ parse: (out) => out,
82
+ timeout: NPM_TIMEOUT_INSTALL,
83
+ };
84
+
85
+ /**
86
+ * `npm view <pkg> version` — the single-value shorthand for "latest version".
87
+ */
88
+ export const NPM_VIEW_VERSION: Recipe<{ pkg: string }, string> = {
89
+ argv: ({ pkg }) => ["npm", "view", pkg, "version"],
90
+ parse: (out) => out.trim(),
91
+ timeout: NPM_TIMEOUT_FAST,
92
+ };
93
+
94
+ export const NPM_RECIPES = {
95
+ NPM_ROOT_GLOBAL,
96
+ NPM_OUTDATED,
97
+ NPM_OUTDATED_GLOBAL,
98
+ NPM_INSTALL,
99
+ NPM_INSTALL_GLOBAL,
100
+ NPM_VIEW_VERSION,
101
+ } as const;
102
+
103
+ // ── Public API ──────────────────────────────────────────────────────────────
104
+
105
+ /**
106
+ * `npm root -g`. Returns a `Result` for explicit error handling; use
107
+ * `rootGlobalOr` for best-effort semantics.
108
+ *
109
+ * Previous versions cached the result in a module-level variable. That
110
+ * cache is now owned by `ToolRegistry` (the runner consults the
111
+ * registry for every resolved binary including `npm` itself). Cache
112
+ * invalidation flows through `registry.rescan()`.
113
+ *
114
+ * See change: consolidate-tool-resolution.
115
+ */
116
+ export function rootGlobal(): Result<string> {
117
+ return run(NPM_ROOT_GLOBAL, {}, {});
118
+ }
119
+
120
+ /**
121
+ * Test-only no-op kept for backward compatibility with existing test
122
+ * suites. The `cachedGlobalRoot` variable no longer exists.
123
+ */
124
+ export function _resetNpmRootCache(): void { /* no-op */ }
125
+
126
+ export function outdated(input: WithCwd & { pkg?: string }): Result<unknown | null> {
127
+ return run(NPM_OUTDATED, input, { cwd: input.cwd });
128
+ }
129
+
130
+ export function outdatedGlobal(input: { pkg?: string } = {}): Result<unknown | null> {
131
+ return run(NPM_OUTDATED_GLOBAL, input, {});
132
+ }
133
+
134
+ export function install(input: WithCwd & { pkg: string; version?: string }): Result<string> {
135
+ return run(NPM_INSTALL, input, { cwd: input.cwd });
136
+ }
137
+
138
+ export function installGlobal(input: { pkg: string; version?: string }): Result<string> {
139
+ return run(NPM_INSTALL_GLOBAL, input, {});
140
+ }
141
+
142
+ export function viewVersion(input: { pkg: string }): Result<string> {
143
+ return run(NPM_VIEW_VERSION, input, {});
144
+ }
145
+
146
+ // ── Best-effort variants ────────────────────────────────────────────────────
147
+
148
+ export function rootGlobalOr(fallback = ""): string {
149
+ return unwrap(rootGlobal(), fallback);
150
+ }
151
+
152
+ export function outdatedOr(input: WithCwd & { pkg?: string }, fallback: unknown | null = null): unknown | null {
153
+ return unwrap(outdated(input), fallback);
154
+ }
155
+
156
+ export function outdatedGlobalOr(input: { pkg?: string } = {}, fallback: unknown | null = null): unknown | null {
157
+ return unwrap(outdatedGlobal(input), fallback);
158
+ }
159
+
160
+ export function viewVersionOr(input: { pkg: string }, fallback = ""): string {
161
+ return unwrap(viewVersion(input), fallback);
162
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * OpenSpec tool module — Recipe-based API for the openspec CLI.
3
+ *
4
+ * Replaces the ad-hoc spawnSync/execFile calls in `openspec-poller.ts`
5
+ * with typed Recipes executed through the runner. The higher-level
6
+ * `pollOpenSpec` / `pollOpenSpecAsync` functions remain in
7
+ * `openspec-poller.ts` (they aggregate list + per-change status into
8
+ * the dashboard's OpenSpecData shape) and now use these primitives.
9
+ *
10
+ * See change: platform-command-executor.
11
+ */
12
+ import { run, unwrap, type Recipe, type Result } from "./runner.js";
13
+
14
+ const OPENSPEC_TIMEOUT = 10_000;
15
+
16
+ interface WithCwd {
17
+ cwd: string;
18
+ }
19
+
20
+ /** Parse JSON from stdout; returns null on parse failure. */
21
+ function parseJsonOrNull(out: string): unknown | null {
22
+ const trimmed = out.trim();
23
+ if (!trimmed) return null;
24
+ try {
25
+ return JSON.parse(trimmed);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ // ── Recipes ─────────────────────────────────────────────────────────────────
32
+
33
+ export const OPENSPEC_LIST: Recipe<WithCwd, unknown | null> = {
34
+ argv: () => ["openspec", "list", "--json"],
35
+ parse: parseJsonOrNull,
36
+ timeout: OPENSPEC_TIMEOUT,
37
+ };
38
+
39
+ export const OPENSPEC_STATUS: Recipe<WithCwd & { change: string }, unknown | null> = {
40
+ argv: ({ change }) => ["openspec", "status", "--change", change, "--json"],
41
+ parse: parseJsonOrNull,
42
+ timeout: OPENSPEC_TIMEOUT,
43
+ };
44
+
45
+ /**
46
+ * `openspec archive --completed` — bulk-archives all completed changes.
47
+ * Stdout is human-readable (not JSON); callers typically don't parse it,
48
+ * they just await success/failure.
49
+ */
50
+ export const OPENSPEC_ARCHIVE_COMPLETED: Recipe<WithCwd, string> = {
51
+ argv: () => ["openspec", "archive", "--completed"],
52
+ parse: (out) => out,
53
+ // Archive operations can be slow when many changes are processed.
54
+ timeout: 30_000,
55
+ };
56
+
57
+ export const OPENSPEC_RECIPES = {
58
+ OPENSPEC_LIST,
59
+ OPENSPEC_STATUS,
60
+ OPENSPEC_ARCHIVE_COMPLETED,
61
+ } as const;
62
+
63
+ // ── Public API ──────────────────────────────────────────────────────────────
64
+
65
+ /** Run `openspec list --json` and return the parsed JSON, or null on failure. */
66
+ export function list(input: WithCwd): Result<unknown | null> {
67
+ return run(OPENSPEC_LIST, input, { cwd: input.cwd });
68
+ }
69
+
70
+ /** Run `openspec status --change <name> --json` and return parsed JSON or null. */
71
+ export function status(input: WithCwd & { change: string }): Result<unknown | null> {
72
+ return run(OPENSPEC_STATUS, input, { cwd: input.cwd });
73
+ }
74
+
75
+ /** Run `openspec archive --completed`. Returns raw stdout on success. */
76
+ export function archiveCompleted(input: WithCwd): Result<string> {
77
+ return run(OPENSPEC_ARCHIVE_COMPLETED, input, { cwd: input.cwd });
78
+ }
79
+
80
+ // ── Best-effort variants (mirror the pattern established in git.ts) ─────────
81
+
82
+ export function listOr(input: WithCwd, fallback: unknown | null = null): unknown | null {
83
+ return unwrap(list(input), fallback);
84
+ }
85
+
86
+ export function statusOr(
87
+ input: WithCwd & { change: string },
88
+ fallback: unknown | null = null,
89
+ ): unknown | null {
90
+ return unwrap(status(input), fallback);
91
+ }