@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.
- package/AGENTS.md +339 -190
- package/README.md +50 -7
- package/docs/architecture.md +238 -23
- package/package.json +23 -13
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +1 -1
- package/packages/extension/src/bridge.ts +169 -4
- package/packages/extension/src/command-handler.ts +65 -2
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/markdown-image-inliner.ts +268 -0
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +50 -2
- package/packages/extension/src/provider-register.ts +132 -8
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +18 -1
- package/packages/extension/src/session-sync.ts +15 -1
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/package.json +6 -6
- package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
- package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
- package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +22 -4
- package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +1 -1
- package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
- package/packages/server/src/__tests__/health-shape.test.ts +43 -0
- package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +236 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
- package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
- package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
- package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
- package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
- package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
- package/packages/server/src/bootstrap-install-from-list.ts +232 -0
- package/packages/server/src/bootstrap-state.ts +18 -0
- package/packages/server/src/browser-gateway.ts +70 -24
- package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
- package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +23 -2
- package/packages/server/src/directory-service.ts +31 -0
- package/packages/server/src/event-wiring.ts +105 -5
- package/packages/server/src/headless-pid-registry.ts +54 -5
- package/packages/server/src/home-lock.d.ts +124 -0
- package/packages/server/src/home-lock.js +330 -0
- package/packages/server/src/home-lock.js.map +1 -0
- package/packages/server/src/idle-timer.ts +15 -1
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +81 -15
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +10 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +115 -21
- package/packages/server/src/provider-auth-handlers.ts +9 -0
- package/packages/server/src/provider-auth-storage.ts +83 -51
- package/packages/server/src/provider-catalogue-cache.ts +47 -0
- package/packages/server/src/routes/doctor-routes.ts +140 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +14 -1
- package/packages/server/src/routes/provider-routes.ts +4 -4
- package/packages/server/src/routes/system-routes.ts +38 -1
- package/packages/server/src/server.ts +85 -66
- package/packages/server/src/session-api.ts +54 -3
- package/packages/server/src/session-bootstrap.ts +27 -12
- package/packages/server/src/session-discovery.ts +11 -4
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/session-scanner.ts +4 -2
- package/packages/server/src/spawn-failure-log.ts +130 -0
- package/packages/server/src/spawn-preflight.ts +82 -0
- package/packages/server/src/spawn-register-watchdog.ts +291 -0
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/terminal-manager.ts +12 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +6 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config.test.ts +48 -0
- package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
- package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
- package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
- package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
- package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
- package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
- package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
- package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
- package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +197 -3
- package/packages/shared/src/browser-protocol.ts +155 -1
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +15 -0
- package/packages/shared/src/dashboard-starter.ts +33 -0
- package/packages/shared/src/doctor-core.ts +821 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/installable-list.ts +152 -0
- package/packages/shared/src/launch-source-flag.ts +14 -0
- package/packages/shared/src/launch-source-types.ts +18 -0
- package/packages/shared/src/openspec-activity-detector.ts +25 -7
- package/packages/shared/src/platform/detached-spawn.ts +13 -2
- package/packages/shared/src/platform/managed-node-path.ts +77 -0
- package/packages/shared/src/platform/node-spawn.ts +29 -21
- package/packages/shared/src/protocol.ts +54 -2
- package/packages/shared/src/resolve-jiti.ts +62 -9
- package/packages/shared/src/rest-api.ts +4 -0
- package/packages/shared/src/skill-block-parser.ts +115 -0
- package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
- package/packages/shared/src/tool-registry/definitions.ts +33 -8
- package/packages/shared/src/tool-registry/strategies.ts +42 -0
- 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
|
-
"@
|
|
46
|
-
"@
|
|
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
|
|
74
|
-
* previous `pi-*` name-prefix heuristic was
|
|
75
|
-
* recommended-extension packages (e.g.
|
|
76
|
-
* `@tintinweb/pi-subagents`) to appear in BOTH the
|
|
77
|
-
* and the Installed Packages panel. Recommended
|
|
78
|
-
* surfaced exclusively through
|
|
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
|
-
/**
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
5
|
-
* `npm
|
|
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
|
-
/**
|
|
37
|
-
|
|
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
|
-
? ["
|
|
45
|
-
: ["
|
|
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
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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:
|
|
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
|
|
162
|
+
? ` (permission error — try: sudo npm install -g ${pkg.name}@latest)`
|
|
97
163
|
: "";
|
|
98
|
-
reject(new Error(`npm
|
|
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
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
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
|
}
|