@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,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
+ }