@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.1

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 (181) hide show
  1. package/AGENTS.md +339 -190
  2. package/README.md +50 -7
  3. package/docs/architecture.md +238 -23
  4. package/package.json +23 -13
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
  13. package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
  14. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  15. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  16. package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
  17. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  19. package/packages/extension/src/ask-user-tool.ts +1 -1
  20. package/packages/extension/src/bridge-context.ts +1 -1
  21. package/packages/extension/src/bridge.ts +169 -4
  22. package/packages/extension/src/command-handler.ts +65 -2
  23. package/packages/extension/src/flow-event-wiring.ts +1 -1
  24. package/packages/extension/src/markdown-image-inliner.ts +268 -0
  25. package/packages/extension/src/multiselect-list.ts +1 -1
  26. package/packages/extension/src/pi-env.d.ts +16 -9
  27. package/packages/extension/src/prompt-expander.ts +50 -2
  28. package/packages/extension/src/provider-register.ts +132 -8
  29. package/packages/extension/src/retry-tracker.ts +123 -0
  30. package/packages/extension/src/server-launcher.ts +18 -1
  31. package/packages/extension/src/session-sync.ts +15 -1
  32. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  33. package/packages/server/package.json +6 -6
  34. package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
  35. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
  36. package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
  37. package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
  38. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  39. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  40. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  41. package/packages/server/src/__tests__/cli-parse.test.ts +22 -4
  42. package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
  43. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  44. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  45. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  46. package/packages/server/src/__tests__/directory-service.test.ts +1 -1
  47. package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
  48. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
  49. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  50. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  51. package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
  52. package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
  53. package/packages/server/src/__tests__/health-shape.test.ts +43 -0
  54. package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
  55. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  56. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
  57. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  58. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  59. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  60. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  61. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  62. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  63. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +236 -0
  64. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  65. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  66. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  67. package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
  68. package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
  69. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  70. package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
  71. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
  72. package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
  73. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
  74. package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
  75. package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
  76. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  77. package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
  78. package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
  79. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
  80. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  81. package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
  82. package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
  83. package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
  84. package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
  85. package/packages/server/src/bootstrap-install-from-list.ts +232 -0
  86. package/packages/server/src/bootstrap-state.ts +18 -0
  87. package/packages/server/src/browser-gateway.ts +70 -24
  88. package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
  89. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  90. package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
  91. package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
  92. package/packages/server/src/changelog-fs.ts +167 -0
  93. package/packages/server/src/changelog-parser.ts +321 -0
  94. package/packages/server/src/changelog-remote.ts +134 -0
  95. package/packages/server/src/cli.ts +23 -2
  96. package/packages/server/src/directory-service.ts +31 -0
  97. package/packages/server/src/event-wiring.ts +105 -5
  98. package/packages/server/src/headless-pid-registry.ts +54 -5
  99. package/packages/server/src/home-lock.d.ts +124 -0
  100. package/packages/server/src/home-lock.js +330 -0
  101. package/packages/server/src/home-lock.js.map +1 -0
  102. package/packages/server/src/idle-timer.ts +15 -1
  103. package/packages/server/src/pending-client-correlations.ts +73 -0
  104. package/packages/server/src/pending-fork-registry.ts +24 -12
  105. package/packages/server/src/pi-core-checker.ts +77 -17
  106. package/packages/server/src/pi-core-updater.ts +81 -15
  107. package/packages/server/src/pi-dev-version-check.ts +145 -0
  108. package/packages/server/src/pi-gateway.ts +10 -0
  109. package/packages/server/src/pi-version-skew.ts +12 -4
  110. package/packages/server/src/process-manager.ts +115 -21
  111. package/packages/server/src/provider-auth-handlers.ts +9 -0
  112. package/packages/server/src/provider-auth-storage.ts +83 -51
  113. package/packages/server/src/provider-catalogue-cache.ts +47 -0
  114. package/packages/server/src/routes/doctor-routes.ts +140 -0
  115. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  116. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  117. package/packages/server/src/routes/provider-auth-routes.ts +14 -1
  118. package/packages/server/src/routes/provider-routes.ts +4 -4
  119. package/packages/server/src/routes/system-routes.ts +38 -1
  120. package/packages/server/src/server.ts +85 -66
  121. package/packages/server/src/session-api.ts +54 -3
  122. package/packages/server/src/session-bootstrap.ts +27 -12
  123. package/packages/server/src/session-discovery.ts +11 -4
  124. package/packages/server/src/session-file-reader.ts +1 -1
  125. package/packages/server/src/session-scanner.ts +4 -2
  126. package/packages/server/src/spawn-failure-log.ts +130 -0
  127. package/packages/server/src/spawn-preflight.ts +82 -0
  128. package/packages/server/src/spawn-register-watchdog.ts +291 -0
  129. package/packages/server/src/spawn-token.ts +20 -0
  130. package/packages/server/src/terminal-manager.ts +12 -1
  131. package/packages/shared/package.json +1 -1
  132. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -17
  133. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  134. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  135. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  136. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  137. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  138. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +6 -3
  139. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  140. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
  141. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
  142. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  143. package/packages/shared/src/__tests__/config.test.ts +48 -0
  144. package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
  145. package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
  146. package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
  147. package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
  148. package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
  149. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
  150. package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
  151. package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
  152. package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
  153. package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
  154. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
  155. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
  156. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  157. package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
  158. package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
  159. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  160. package/packages/shared/src/bootstrap-install.ts +197 -3
  161. package/packages/shared/src/browser-protocol.ts +155 -1
  162. package/packages/shared/src/changelog-types.ts +111 -0
  163. package/packages/shared/src/config.ts +15 -0
  164. package/packages/shared/src/dashboard-starter.ts +33 -0
  165. package/packages/shared/src/doctor-core.ts +821 -0
  166. package/packages/shared/src/index.ts +9 -0
  167. package/packages/shared/src/installable-list.ts +152 -0
  168. package/packages/shared/src/launch-source-flag.ts +14 -0
  169. package/packages/shared/src/launch-source-types.ts +18 -0
  170. package/packages/shared/src/openspec-activity-detector.ts +25 -7
  171. package/packages/shared/src/platform/detached-spawn.ts +13 -2
  172. package/packages/shared/src/platform/managed-node-path.ts +77 -0
  173. package/packages/shared/src/platform/node-spawn.ts +29 -21
  174. package/packages/shared/src/protocol.ts +54 -2
  175. package/packages/shared/src/resolve-jiti.ts +62 -9
  176. package/packages/shared/src/rest-api.ts +4 -0
  177. package/packages/shared/src/skill-block-parser.ts +115 -0
  178. package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
  179. package/packages/shared/src/tool-registry/definitions.ts +33 -8
  180. package/packages/shared/src/tool-registry/strategies.ts +42 -0
  181. package/packages/shared/src/types.ts +64 -0
@@ -22,6 +22,8 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
22
22
  import path from "node:path";
23
23
  import os from "node:os";
24
24
  import { fetchPackageMeta } from "./npm-search-proxy.js";
25
+ import { invalidateChangelogCache } from "./changelog-parser.js";
26
+ import { getLatestPiRelease, type PiDevReleaseInfo } from "./pi-dev-version-check.js";
25
27
 
26
28
  const execFileAsync = promisify(execFile);
27
29
 
@@ -34,16 +36,16 @@ const MANAGED_NODE_MODULES = path.join(MANAGED_DIR, "node_modules");
34
36
 
35
37
  /** Known core packages (not extensions). Order matters for display. */
36
38
  export const CORE_PACKAGE_NAMES: readonly string[] = [
39
+ "@earendil-works/pi-coding-agent",
37
40
  "@mariozechner/pi-coding-agent",
38
- "@oh-my-pi/pi-coding-agent",
39
41
  "@blackbelt-technology/pi-agent-dashboard",
40
42
  "@blackbelt-technology/pi-model-proxy",
41
43
  ];
42
44
 
43
45
  /** Display name mapping for known packages. Falls back to package name. */
44
46
  const DISPLAY_NAMES: Readonly<Record<string, string>> = {
45
- "@mariozechner/pi-coding-agent": "pi (core agent)",
46
- "@oh-my-pi/pi-coding-agent": "pi (core agent — fork)",
47
+ "@earendil-works/pi-coding-agent": "pi (core agent)",
48
+ "@mariozechner/pi-coding-agent": "pi (core agent — legacy fork)",
47
49
  "@blackbelt-technology/pi-agent-dashboard": "pi-dashboard",
48
50
  "@blackbelt-technology/pi-model-proxy": "pi-model-proxy",
49
51
  };
@@ -68,20 +70,40 @@ function resolveDisplayName(name: string): string {
68
70
  return DISPLAY_NAMES[name] ?? name;
69
71
  }
70
72
 
73
+ /**
74
+ * Dynamically-discovered package-name aliases for `@mariozechner/pi-coding-agent`.
75
+ * Populated from pi.dev's `latest-version` response, which returns the
76
+ * authoritative package name for fresh installs (used for the upcoming
77
+ * `@mariozechner` → `@earendil-works` scope migration). The dashboard
78
+ * accepts whatever name pi.dev declares as a trusted alias — this avoids
79
+ * having to ship a release every time the canonical scope changes.
80
+ *
81
+ * See change: improve-pi-update-detection.
82
+ */
83
+ let dynamicPiAliases: Set<string> = new Set();
84
+
85
+ /** Test seam: clear runtime aliases between tests. */
86
+ export function _resetDynamicPiAliases(): void {
87
+ dynamicPiAliases = new Set();
88
+ }
89
+
71
90
  /**
72
91
  * Strict whitelist check: a package is part of the pi-ecosystem CORE
73
- * tooling if and only if its name is in `CORE_PACKAGE_NAMES`. The
74
- * previous `pi-*` name-prefix heuristic was removed because it caused
75
- * recommended-extension packages (e.g. `pi-agent-browser`,
76
- * `@tintinweb/pi-subagents`) to appear in BOTH the Core ecosystem panel
77
- * and the Installed Packages panel. Recommended extensions are now
78
- * surfaced exclusively through `/api/packages/installed`. See change:
79
- * consolidate-packages-settings-ui.
92
+ * tooling if and only if its name is in `CORE_PACKAGE_NAMES` OR a
93
+ * pi.dev-declared alias. The previous `pi-*` name-prefix heuristic was
94
+ * removed because it caused recommended-extension packages (e.g.
95
+ * `pi-agent-browser`, `@tintinweb/pi-subagents`) to appear in BOTH the
96
+ * Core ecosystem panel and the Installed Packages panel. Recommended
97
+ * extensions are now surfaced exclusively through
98
+ * `/api/packages/installed`. See change: consolidate-packages-settings-ui.
80
99
  */
81
100
  function looksLikePiEcosystem(name: string): boolean {
82
- return CORE_PACKAGE_NAMES.includes(name);
101
+ return CORE_PACKAGE_NAMES.includes(name) || dynamicPiAliases.has(name);
83
102
  }
84
103
 
104
+ /** Pi packages whose latestVersion comes from pi.dev (not npm registry). */
105
+ const PI_DEV_PACKAGE = "@mariozechner/pi-coding-agent";
106
+
85
107
  export interface NpmListRunner {
86
108
  /** Run `npm list -g --depth=0 --json` and return stdout. */
87
109
  (): Promise<string>;
@@ -92,6 +114,13 @@ export interface PiCoreCheckerOptions {
92
114
  npmList?: NpmListRunner;
93
115
  /** Inject version fetcher (for tests). */
94
116
  fetchLatest?: (packageName: string) => Promise<string | null>;
117
+ /**
118
+ * Inject pi.dev release fetcher (for tests). Production uses
119
+ * `getLatestPiRelease` which honours PI_OFFLINE / PI_SKIP_VERSION_CHECK
120
+ * envs and falls back to `undefined` on any failure. See change:
121
+ * improve-pi-update-detection.
122
+ */
123
+ fetchPiDevRelease?: (currentVersion: string) => Promise<PiDevReleaseInfo | undefined>;
95
124
  /** Override managed directory (for tests). */
96
125
  managedDir?: string;
97
126
  }
@@ -114,19 +143,28 @@ export class PiCoreChecker {
114
143
  private cache: { at: number; data: PiCoreStatus } | null = null;
115
144
  private readonly npmList: NpmListRunner;
116
145
  private readonly fetchLatest: (packageName: string) => Promise<string | null>;
146
+ private readonly fetchPiDevRelease: (currentVersion: string) => Promise<PiDevReleaseInfo | undefined>;
117
147
  private readonly managedNodeModules: string;
118
148
 
119
149
  constructor(opts: PiCoreCheckerOptions = {}) {
120
150
  this.npmList = opts.npmList ?? defaultNpmList;
121
151
  this.fetchLatest = opts.fetchLatest ?? defaultFetchLatest;
152
+ this.fetchPiDevRelease = opts.fetchPiDevRelease ?? getLatestPiRelease;
122
153
  this.managedNodeModules = opts.managedDir
123
154
  ? path.join(opts.managedDir, "node_modules")
124
155
  : MANAGED_NODE_MODULES;
125
156
  }
126
157
 
127
- /** Invalidate the cache (e.g. after an update completes). */
158
+ /**
159
+ * Invalidate the cache (e.g. after an update completes).
160
+ *
161
+ * Also clears the changelog parser cache so the next
162
+ * `GET /api/pi-core/changelog` request reads the freshly-installed
163
+ * file from disk. See change: pi-update-whats-new-panel.
164
+ */
128
165
  invalidate(): void {
129
166
  this.cache = null;
167
+ invalidateChangelogCache();
130
168
  }
131
169
 
132
170
  /** Get version status. Returns cached data within 5 min unless `refresh`. */
@@ -144,15 +182,37 @@ export class PiCoreChecker {
144
182
  for (const entry of global) byName.set(entry.name, { version: entry.version, source: "global" });
145
183
  for (const entry of managed) byName.set(entry.name, { version: entry.version, source: "managed" });
146
184
 
147
- // Fetch latest versions in parallel.
185
+ // Fetch latest versions in parallel. For pi-coding-agent (and its
186
+ // declared scope-rename aliases), prefer pi.dev's authoritative
187
+ // version-check endpoint over the npm registry; fall back to npm
188
+ // registry on any failure so the dashboard never reports "unknown
189
+ // version" just because pi.dev had a hiccup. See change:
190
+ // improve-pi-update-detection.
148
191
  const entries = Array.from(byName.entries());
149
192
  const withLatest = await Promise.all(
150
193
  entries.map(async ([name, info]) => {
151
194
  let latest: string | null = null;
152
- try {
153
- latest = await this.fetchLatest(name);
154
- } catch {
155
- latest = null;
195
+ const isPi = name === PI_DEV_PACKAGE || dynamicPiAliases.has(name);
196
+ if (isPi) {
197
+ try {
198
+ const piDev = await this.fetchPiDevRelease(info.version);
199
+ if (piDev) {
200
+ latest = piDev.version;
201
+ // Record any new alias for next-time discovery.
202
+ if (piDev.packageName && !CORE_PACKAGE_NAMES.includes(piDev.packageName)) {
203
+ dynamicPiAliases.add(piDev.packageName);
204
+ }
205
+ }
206
+ } catch {
207
+ /* fall through to npm registry */
208
+ }
209
+ }
210
+ if (latest === null) {
211
+ try {
212
+ latest = await this.fetchLatest(name);
213
+ } catch {
214
+ latest = null;
215
+ }
156
216
  }
157
217
  const updateAvailable = latest !== null && latest !== info.version;
158
218
  const pkg: PiCorePackage = {
@@ -1,16 +1,24 @@
1
1
  /**
2
2
  * Pi core package updater.
3
3
  *
4
- * Runs `npm update -g <pkg>` for globally-installed packages or
5
- * `npm update <pkg>` in `~/.pi-dashboard/` for managed installs.
4
+ * Runs `npm install -g <pkg>@latest` for globally-installed packages or
5
+ * `npm install <pkg>@latest` in `~/.pi-dashboard/` for managed installs.
6
+ * The `@latest` suffix is required because the consuming `package.json`
7
+ * dependency range (e.g. `^0.70.0`) would otherwise pin updates to the
8
+ * same minor — breaking cross-minor upgrades that pi now ships routinely
9
+ * (0.71+ minors carry breaking changes per its CHANGELOG).
6
10
  * Coordinates with PackageManagerWrapper's busy-lock so extension
7
11
  * operations and core updates can't run concurrently.
12
+ *
13
+ * See change: fix-pi-core-update-cross-minor.
8
14
  */
9
15
  import { spawn } from "node:child_process"; // ban:child_process-ok npm-update streams stdout/stderr via pipe for progress events; refactor to platform/spawn Recipe is tracked tech debt
10
16
  import path from "node:path";
11
17
  import os from "node:os";
12
18
  import { existsSync } from "node:fs";
13
19
  import type { PiCorePackage, PiCoreUpdateResult } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
20
+ import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
21
+ import { prependManagedNodeToPath } from "@blackbelt-technology/pi-dashboard-shared/platform/managed-node-path.js";
14
22
  import type { PackageManagerWrapper } from "./package-manager-wrapper.js";
15
23
 
16
24
  const UPDATE_TIMEOUT_MS = 5 * 60 * 1000; // 5 min per package
@@ -33,16 +41,54 @@ export interface PiCoreUpdaterOptions {
33
41
  onAllComplete?: () => Promise<number>;
34
42
  }
35
43
 
36
- /** Default npm-update runner. */
37
- function defaultRunNpmUpdate(
44
+ /**
45
+ * Test seams for `defaultRunNpmUpdate`. Production callers omit
46
+ * `_seams`; tests inject fakes to avoid real spawns.
47
+ *
48
+ * `_resolveNpm` defaults to `getDefaultRegistry().resolveExecutor("npm")`.
49
+ * `_spawn` defaults to `node:child_process` `spawn`.
50
+ * `_envBuilder` defaults to `prependManagedNodeToPath(process.env)`.
51
+ */
52
+ export interface DefaultRunNpmUpdateSeams {
53
+ _resolveNpm?: () =>
54
+ | { ok: true; argv: string[] }
55
+ | { ok: false; reason: string };
56
+ _spawn?: typeof spawn;
57
+ _envBuilder?: () => NodeJS.ProcessEnv;
58
+ }
59
+
60
+ /**
61
+ * Default npm-update runner.
62
+ *
63
+ * After change `embed-managed-node-runtime`:
64
+ * - Resolves the `npm` binary via `ToolRegistry.resolve("npm")` so
65
+ * the managed-Node runtime (when installed) is preferred over the
66
+ * system PATH — the user-visible regression class this change
67
+ * exists to prevent (`npm update exited with code 1` on a fresh
68
+ * Windows install with no system Node).
69
+ * - Refuses to spawn a bare `"npm"` if the registry can't resolve
70
+ * it. Surfaces a clear `npm` unresolved error per the spec
71
+ * scenario "ToolRegistry resolution failure surfaces a clear
72
+ * error".
73
+ * - Prepends the managed Node directory to the spawned child's
74
+ * `PATH` via `prependManagedNodeToPath`, so any nested `node` /
75
+ * `npm` invocation inside the npm subprocess also resolves to the
76
+ * managed runtime.
77
+ */
78
+ export function defaultRunNpmUpdate(
38
79
  pkg: PiCorePackage,
39
80
  onOutput: (line: string) => void,
81
+ seams: DefaultRunNpmUpdateSeams = {},
40
82
  ): Promise<void> {
41
83
  return new Promise((resolve, reject) => {
84
+ // Always target the npm `latest` dist-tag — bypasses the
85
+ // consuming package.json range so cross-minor jumps work. See
86
+ // change: fix-pi-core-update-cross-minor.
87
+ const spec = `${pkg.name}@latest`;
42
88
  const args =
43
89
  pkg.installSource === "global"
44
- ? ["update", "-g", pkg.name]
45
- : ["update", pkg.name];
90
+ ? ["install", "-g", spec]
91
+ : ["install", spec];
46
92
  const cwd = pkg.installSource === "managed" ? MANAGED_DIR : process.cwd();
47
93
 
48
94
  if (pkg.installSource === "managed" && !existsSync(MANAGED_DIR)) {
@@ -50,15 +96,35 @@ function defaultRunNpmUpdate(
50
96
  return;
51
97
  }
52
98
 
53
- // On Windows, system npm is npm.cmd (batch wrapper) spawn("npm")
54
- // without the .cmd extension fails with ENOENT. shell:true routes
55
- // the invocation through cmd.exe which resolves via PATHEXT.
56
- // See change: route-kill-paths-through-platform (same class of bug).
57
- const child = spawn("npm", args, {
99
+ // Resolve npm via ToolRegistry: managed runtime > override > PATH.
100
+ // On unresolved, refuse do not fall back to bare spawn("npm").
101
+ const resolveNpm =
102
+ seams._resolveNpm ??
103
+ (() => {
104
+ const r = getDefaultRegistry().resolveExecutor("npm");
105
+ return r.ok && r.path
106
+ ? { ok: true as const, argv: r.argv }
107
+ : { ok: false as const, reason: "no override, no managed runtime, no npm on PATH" };
108
+ });
109
+ const npmRes = resolveNpm();
110
+ if (!npmRes.ok) {
111
+ reject(new Error(
112
+ `npm could not be resolved (${npmRes.reason}). ` +
113
+ "Install Node.js or run `pi-dashboard repair` to restore the managed Node runtime.",
114
+ ));
115
+ return;
116
+ }
117
+
118
+ // `argv` is ready-to-spawn: on Windows + an npm-cli.js resolution
119
+ // it is `[node.exe, npm-cli.js]` (bypasses the .cmd shim and the
120
+ // cmd.exe console flash); elsewhere it is `[npm]`.
121
+ const [cmd, ...argvPrefix] = npmRes.argv;
122
+ const spawnFn = seams._spawn ?? spawn;
123
+ const envFn = seams._envBuilder ?? (() => prependManagedNodeToPath(process.env));
124
+ const child = spawnFn(cmd, [...argvPrefix, ...args], {
58
125
  cwd,
59
126
  stdio: ["ignore", "pipe", "pipe"],
60
- env: process.env,
61
- shell: process.platform === "win32", // platform-branch-ok: shell:true required on Windows so PATHEXT resolves npm.cmd (spawn('npm') without .cmd ENOENTs)
127
+ env: envFn(),
62
128
  windowsHide: true,
63
129
  });
64
130
 
@@ -93,9 +159,9 @@ function defaultRunNpmUpdate(
93
159
  } else {
94
160
  const hint =
95
161
  pkg.installSource === "global" && /permission|EACCES|EPERM|EROFS/i.test(stderrBuf)
96
- ? ` (permission error — try: sudo npm update -g ${pkg.name})`
162
+ ? ` (permission error — try: sudo npm install -g ${pkg.name}@latest)`
97
163
  : "";
98
- reject(new Error(`npm update exited with code ${code}${hint}`));
164
+ reject(new Error(`npm install exited with code ${code}${hint}`));
99
165
  }
100
166
  });
101
167
  });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * pi.dev version-check client. Mirrors the implementation pi itself
3
+ * uses for self-update checks (see `@mariozechner/pi-coding-agent/dist/
4
+ * utils/version-check.js`). Returns `{ version, packageName? }` so the
5
+ * dashboard can:
6
+ * 1. Detect the genuinely-newest pi without npm-registry lag.
7
+ * 2. Pick up pi's npm-scope migration dynamically (the response's
8
+ * `packageName` field is the upstream's authoritative answer to
9
+ * "which package should be installed for the latest pi?").
10
+ *
11
+ * Falls back to `undefined` on any error so callers can degrade to
12
+ * the npm-registry path. Honours `PI_OFFLINE` and `PI_SKIP_VERSION_CHECK`
13
+ * envs identically to pi.
14
+ *
15
+ * See change: improve-pi-update-detection.
16
+ */
17
+
18
+ const LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
19
+ const DEFAULT_TIMEOUT_MS = 10_000;
20
+
21
+ export interface PiDevReleaseInfo {
22
+ /** Latest version string as published by pi.dev (e.g. `"0.74.0"`). */
23
+ version: string;
24
+ /**
25
+ * Authoritative package name for fresh installs. Pi 0.73.1+ returns
26
+ * this so consumers can follow npm-scope migrations without code
27
+ * changes. Absent for older pi.dev responses.
28
+ */
29
+ packageName?: string;
30
+ }
31
+
32
+ export interface PiDevVersionCheckOptions {
33
+ /** Override fetch timeout. Default 10 s, matching pi. */
34
+ timeoutMs?: number;
35
+ /** Test seam: override fetch implementation. */
36
+ fetchImpl?: typeof fetch;
37
+ }
38
+
39
+ interface ParsedSemver {
40
+ major: number;
41
+ minor: number;
42
+ patch: number;
43
+ prerelease?: string;
44
+ }
45
+
46
+ /**
47
+ * Parse a semver-ish version string. Mirrors pi's `parsePackageVersion`.
48
+ * Returns `undefined` for unparseable input so callers fall back to a
49
+ * conservative comparison.
50
+ */
51
+ export function parsePackageVersion(version: string): ParsedSemver | undefined {
52
+ const match = version
53
+ .trim()
54
+ .match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
55
+ if (!match) return undefined;
56
+ return {
57
+ major: Number.parseInt(match[1], 10),
58
+ minor: Number.parseInt(match[2], 10),
59
+ patch: Number.parseInt(match[3], 10),
60
+ prerelease: match[4],
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Compare two semver strings: -ve when left < right, 0 when equal,
66
+ * +ve when left > right. Returns `undefined` when either side is
67
+ * unparseable.
68
+ */
69
+ export function comparePackageVersions(
70
+ leftVersion: string,
71
+ rightVersion: string,
72
+ ): number | undefined {
73
+ const left = parsePackageVersion(leftVersion);
74
+ const right = parsePackageVersion(rightVersion);
75
+ if (!left || !right) return undefined;
76
+ if (left.major !== right.major) return left.major - right.major;
77
+ if (left.minor !== right.minor) return left.minor - right.minor;
78
+ if (left.patch !== right.patch) return left.patch - right.patch;
79
+ if (left.prerelease === right.prerelease) return 0;
80
+ if (!left.prerelease) return 1;
81
+ if (!right.prerelease) return -1;
82
+ return left.prerelease.localeCompare(right.prerelease);
83
+ }
84
+
85
+ /** True when `candidateVersion` is strictly newer than `currentVersion`. */
86
+ export function isNewerPackageVersion(
87
+ candidateVersion: string,
88
+ currentVersion: string,
89
+ ): boolean {
90
+ const cmp = comparePackageVersions(candidateVersion, currentVersion);
91
+ if (cmp !== undefined) return cmp > 0;
92
+ return candidateVersion.trim() !== currentVersion.trim();
93
+ }
94
+
95
+ /**
96
+ * Build the User-Agent string pi sends on its self-update calls.
97
+ * Format: `pi/<version> (<platform>; <runtime>; <arch>)`.
98
+ *
99
+ * `<runtime>` is `bun/<version>` when running under Bun, otherwise
100
+ * `node/<process.version>`. We don't identify the dashboard separately
101
+ * so pi.dev treats the request the same way as a self-update from pi.
102
+ */
103
+ export function getPiUserAgent(version: string, runtime?: string): string {
104
+ const rt =
105
+ runtime ??
106
+ ((globalThis as { Bun?: { version: string } }).Bun?.version
107
+ ? `bun/${(globalThis as { Bun?: { version: string } }).Bun!.version}`
108
+ : `node/${process.version}`);
109
+ return `pi/${version} (${process.platform}; ${rt}; ${process.arch})`;
110
+ }
111
+
112
+ /**
113
+ * Query pi.dev for the latest release info. Returns `undefined` on
114
+ * any of: env-skipped, network error, non-2xx response, malformed
115
+ * JSON, missing `version` field, timeout.
116
+ */
117
+ export async function getLatestPiRelease(
118
+ currentVersion: string,
119
+ opts: PiDevVersionCheckOptions = {},
120
+ ): Promise<PiDevReleaseInfo | undefined> {
121
+ if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) {
122
+ return undefined;
123
+ }
124
+ const fetchFn = opts.fetchImpl ?? fetch;
125
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
126
+ try {
127
+ const response = await fetchFn(LATEST_VERSION_URL, {
128
+ headers: {
129
+ "User-Agent": getPiUserAgent(currentVersion),
130
+ accept: "application/json",
131
+ },
132
+ signal: AbortSignal.timeout(timeoutMs),
133
+ });
134
+ if (!response.ok) return undefined;
135
+ const data = (await response.json()) as { version?: unknown; packageName?: unknown };
136
+ if (typeof data.version !== "string" || !data.version.trim()) return undefined;
137
+ const packageName =
138
+ typeof data.packageName === "string" && data.packageName.trim()
139
+ ? data.packageName.trim()
140
+ : undefined;
141
+ return { version: data.version.trim(), packageName };
142
+ } catch {
143
+ return undefined;
144
+ }
145
+ }
@@ -5,6 +5,7 @@ import { WebSocketServer, WebSocket } from "ws";
5
5
  import type { ExtensionToServerMessage, ServerToExtensionMessage } from "@blackbelt-technology/pi-dashboard-shared/protocol.js";
6
6
  import type { DashboardSession } from "@blackbelt-technology/pi-dashboard-shared/types.js";
7
7
  import type { SessionManager } from "./memory-session-manager.js";
8
+ import { getSpawnRegisterWatchdog } from "./spawn-register-watchdog.js";
8
9
 
9
10
  export const HEARTBEAT_TIMEOUT = 180_000;
10
11
  export const WS_PING_INTERVAL = 60_000;
@@ -273,6 +274,15 @@ export function createPiGateway(
273
274
  }
274
275
 
275
276
  if (msg.type === "session_register") {
277
+ // Clear spawn-register watchdog BEFORE any throwing logic. See change: spawn-failure-diagnostics.
278
+ // Priority: token > pid > cwd. Token is the strongest identity
279
+ // (spawn-correlation-token); pid catches headless without token;
280
+ // cwd is the legacy fallback for tmux/wt with neither.
281
+ const watchdog = getSpawnRegisterWatchdog();
282
+ if (msg.spawnToken) watchdog.clearByToken(msg.spawnToken);
283
+ if (msg.pid !== undefined) watchdog.clearByPid(msg.pid);
284
+ watchdog.clearByCwd(msg.cwd);
285
+
276
286
  // If session ID changed (e.g., after /reload), clean up the old placeholder
277
287
  if (currentSessionId && currentSessionId !== msg.sessionId) {
278
288
  const oldSession = sessionManager.get(currentSessionId);
@@ -98,10 +98,18 @@ export function readPiCompatibility(serverPkgJsonPath: string): Pick<
98
98
  export function readCurrentPiVersion(registry: ToolRegistry = getDefaultRegistry()): string | undefined {
99
99
  try {
100
100
  const req = createRequire(import.meta.url);
101
- const pkgJson = req.resolve("@mariozechner/pi-coding-agent/package.json");
102
- const raw = fs.readFileSync(pkgJson, "utf8");
103
- const parsed = JSON.parse(raw) as { version?: string };
104
- if (typeof parsed.version === "string") return parsed.version;
101
+ let pkgJson: string | undefined;
102
+ for (const name of ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"]) {
103
+ try {
104
+ pkgJson = req.resolve(`${name}/package.json`);
105
+ break;
106
+ } catch { /* try next alias */ }
107
+ }
108
+ if (pkgJson) {
109
+ const raw = fs.readFileSync(pkgJson, "utf8");
110
+ const parsed = JSON.parse(raw) as { version?: string };
111
+ if (typeof parsed.version === "string") return parsed.version;
112
+ }
105
113
  } catch {
106
114
  /* not resolvable yet */
107
115
  }