@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2
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 +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- 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-launch.test.ts +78 -0
- 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 +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- 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 +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -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 +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -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 +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- 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__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -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-ai-shape.test.ts +147 -0
- 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 +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- 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-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- 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 +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- 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 +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -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 +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -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 +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -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,10 +1,16 @@
|
|
|
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";
|
|
@@ -75,10 +81,14 @@ export function defaultRunNpmUpdate(
|
|
|
75
81
|
seams: DefaultRunNpmUpdateSeams = {},
|
|
76
82
|
): Promise<void> {
|
|
77
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`;
|
|
78
88
|
const args =
|
|
79
89
|
pkg.installSource === "global"
|
|
80
|
-
? ["
|
|
81
|
-
: ["
|
|
90
|
+
? ["install", "-g", spec]
|
|
91
|
+
: ["install", spec];
|
|
82
92
|
const cwd = pkg.installSource === "managed" ? MANAGED_DIR : process.cwd();
|
|
83
93
|
|
|
84
94
|
if (pkg.installSource === "managed" && !existsSync(MANAGED_DIR)) {
|
|
@@ -149,9 +159,9 @@ export function defaultRunNpmUpdate(
|
|
|
149
159
|
} else {
|
|
150
160
|
const hint =
|
|
151
161
|
pkg.installSource === "global" && /permission|EACCES|EPERM|EROFS/i.test(stderrBuf)
|
|
152
|
-
? ` (permission error — try: sudo npm
|
|
162
|
+
? ` (permission error — try: sudo npm install -g ${pkg.name}@latest)`
|
|
153
163
|
: "";
|
|
154
|
-
reject(new Error(`npm
|
|
164
|
+
reject(new Error(`npm install exited with code ${code}${hint}`));
|
|
155
165
|
}
|
|
156
166
|
});
|
|
157
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
|
+
}
|
|
@@ -275,7 +275,11 @@ export function createPiGateway(
|
|
|
275
275
|
|
|
276
276
|
if (msg.type === "session_register") {
|
|
277
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.
|
|
278
281
|
const watchdog = getSpawnRegisterWatchdog();
|
|
282
|
+
if (msg.spawnToken) watchdog.clearByToken(msg.spawnToken);
|
|
279
283
|
if (msg.pid !== undefined) watchdog.clearByPid(msg.pid);
|
|
280
284
|
watchdog.clearByCwd(msg.cwd);
|
|
281
285
|
|
|
@@ -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
|
}
|
|
@@ -23,6 +23,13 @@ import type { SpawnStrategy } from "@blackbelt-technology/pi-dashboard-shared/co
|
|
|
23
23
|
import { MANAGED_BIN } from "@blackbelt-technology/pi-dashboard-shared/managed-paths.js";
|
|
24
24
|
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js";
|
|
25
25
|
import { prependManagedNodeToPath } from "@blackbelt-technology/pi-dashboard-shared/platform/managed-node-path.js";
|
|
26
|
+
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
27
|
+
import { mintSpawnToken } from "./spawn-token.js";
|
|
28
|
+
import {
|
|
29
|
+
createKeeperManager,
|
|
30
|
+
type KeeperManager,
|
|
31
|
+
} from "./rpc-keeper/keeper-manager.js";
|
|
32
|
+
import { randomUUID } from "node:crypto";
|
|
26
33
|
import { execSync, spawnSync, buildSafeArgv } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
27
34
|
import {
|
|
28
35
|
spawnDetached,
|
|
@@ -51,12 +58,56 @@ export function resetResolver(): void {
|
|
|
51
58
|
resolver = new ToolResolver({ processExecPath: process.execPath });
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
// ── KeeperManager seam (injectable for tests) ──────────────────────────
|
|
62
|
+
|
|
63
|
+
let keeperManager: KeeperManager | null = null;
|
|
64
|
+
|
|
65
|
+
/** Inject a KeeperManager — used by tests. Production code lazy-inits below. */
|
|
66
|
+
export function setKeeperManager(km: KeeperManager | null): void {
|
|
67
|
+
keeperManager = km;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Public lazy accessor for the singleton `KeeperManager`. Exposed so the
|
|
72
|
+
* server-side dispatch handler (`rpc-keeper/dispatch-router.ts`) and
|
|
73
|
+
* `headlessPidRegistry.setKeeperWriter` can share the same instance the
|
|
74
|
+
* spawn path uses. Tests still inject via `setKeeperManager`.
|
|
75
|
+
* See change: add-rpc-stdin-dispatch-with-keeper-sidecar (Phase 6 + 8).
|
|
76
|
+
*/
|
|
77
|
+
export function getKeeperManager(): KeeperManager {
|
|
78
|
+
if (!keeperManager) keeperManager = createKeeperManager();
|
|
79
|
+
return keeperManager;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Hook used by tests to override the `useRpcKeeper` flag read from config
|
|
84
|
+
* without mutating `~/.pi/dashboard/config.json`. Returns `null` to defer
|
|
85
|
+
* to the real config.
|
|
86
|
+
*/
|
|
87
|
+
let useRpcKeeperOverride: boolean | null = null;
|
|
88
|
+
export function _setUseRpcKeeperOverrideForTests(v: boolean | null): void {
|
|
89
|
+
useRpcKeeperOverride = v;
|
|
90
|
+
}
|
|
91
|
+
function shouldUseRpcKeeper(): boolean {
|
|
92
|
+
if (useRpcKeeperOverride !== null) return useRpcKeeperOverride;
|
|
93
|
+
try { return loadConfig().useRpcKeeper === true; } catch { return false; }
|
|
94
|
+
}
|
|
95
|
+
|
|
54
96
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
55
97
|
|
|
56
98
|
export interface SessionOptions {
|
|
57
99
|
sessionFile?: string;
|
|
58
100
|
mode?: "continue" | "fork";
|
|
59
101
|
strategy?: SpawnStrategy;
|
|
102
|
+
/**
|
|
103
|
+
* Server-minted spawn correlation token. When provided, injected into
|
|
104
|
+
* the spawned process env as `PI_DASHBOARD_SPAWN_TOKEN`. The bridge
|
|
105
|
+
* echoes it back in the first `session_register` so the server can
|
|
106
|
+
* resolve identity precisely (linkByToken). When omitted, callers
|
|
107
|
+
* fall through to pid-link or cwd-FIFO matching.
|
|
108
|
+
* See change: spawn-correlation-token.
|
|
109
|
+
*/
|
|
110
|
+
spawnToken?: string;
|
|
60
111
|
}
|
|
61
112
|
|
|
62
113
|
export interface SpawnResult {
|
|
@@ -72,6 +123,22 @@ export interface SpawnResult {
|
|
|
72
123
|
stderr?: string;
|
|
73
124
|
/** Path to the per-session stderr log (Windows headless). Forwarded to watchdog. See change: spawn-failure-diagnostics. */
|
|
74
125
|
logPath?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Token minted by `spawnPiSession` and injected into the spawned process's
|
|
128
|
+
* env as `PI_DASHBOARD_SPAWN_TOKEN`. Returned so callers can register it
|
|
129
|
+
* with the headless-pid registry, watchdog, and pending-* registries.
|
|
130
|
+
* See change: spawn-correlation-token.
|
|
131
|
+
*/
|
|
132
|
+
spawnToken?: string;
|
|
133
|
+
/**
|
|
134
|
+
* RPC keeper UDS / named-pipe path. Set ONLY when the keeper-mediated
|
|
135
|
+
* spawn path was taken (`useRpcKeeper: true`). Callers pass this to
|
|
136
|
+
* `headlessPidRegistry.register(..., { keeperPid, keeperSockPath })` so
|
|
137
|
+
* later `writeRpc` / `killBySessionId` calls can locate the keeper.
|
|
138
|
+
* In keeper mode `pid` IS the keeper PID, so `keeperPid` is implicit.
|
|
139
|
+
* See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
140
|
+
*/
|
|
141
|
+
keeperSockPath?: string;
|
|
75
142
|
}
|
|
76
143
|
|
|
77
144
|
/**
|
|
@@ -87,8 +154,18 @@ export interface SpawnResult {
|
|
|
87
154
|
* lands at the very head of `PATH` — spawned children invoking plain
|
|
88
155
|
* `node` / `npm` resolve to the managed runtime first.
|
|
89
156
|
*/
|
|
90
|
-
export function buildSpawnEnv(
|
|
91
|
-
|
|
157
|
+
export function buildSpawnEnv(
|
|
158
|
+
baseEnv: NodeJS.ProcessEnv = process.env,
|
|
159
|
+
opts?: { spawnToken?: string },
|
|
160
|
+
): NodeJS.ProcessEnv {
|
|
161
|
+
const env = prependManagedNodeToPath(resolver.buildSpawnEnv(baseEnv));
|
|
162
|
+
if (opts?.spawnToken) {
|
|
163
|
+
// Inject the correlation token so the bridge inside the spawned pi
|
|
164
|
+
// process can read it and echo back in `session_register`.
|
|
165
|
+
// See change: spawn-correlation-token.
|
|
166
|
+
return { ...env, PI_DASHBOARD_SPAWN_TOKEN: opts.spawnToken };
|
|
167
|
+
}
|
|
168
|
+
return env;
|
|
92
169
|
}
|
|
93
170
|
|
|
94
171
|
/**
|
|
@@ -279,14 +356,25 @@ export async function spawnPiSession(
|
|
|
279
356
|
return { success: false, code: "DIR_MISSING", message: `Directory does not exist: ${cwd}` };
|
|
280
357
|
}
|
|
281
358
|
|
|
282
|
-
|
|
359
|
+
// Mint a spawn token if the caller didn't provide one. Token is injected
|
|
360
|
+
// into the spawned process's env (via buildSpawnEnv) and surfaced on
|
|
361
|
+
// SpawnResult so callers can register it with the registries.
|
|
362
|
+
// See change: spawn-correlation-token.
|
|
363
|
+
const spawnToken = options?.spawnToken ?? mintSpawnToken();
|
|
364
|
+
const opts: SessionOptions & { electronMode?: boolean } = { ...(options ?? {}), spawnToken };
|
|
283
365
|
|
|
366
|
+
const mechanism = chooseMechanism(opts, opts?.electronMode ?? false);
|
|
367
|
+
|
|
368
|
+
let result: SpawnResult;
|
|
284
369
|
switch (mechanism) {
|
|
285
|
-
case "tmux":
|
|
286
|
-
case "wt":
|
|
287
|
-
case "wsl-tmux":
|
|
288
|
-
case "headless":
|
|
370
|
+
case "tmux": result = spawnTmux(cwd, opts); break;
|
|
371
|
+
case "wt": result = await spawnWt(cwd, opts); break;
|
|
372
|
+
case "wsl-tmux": result = spawnWslTmux(cwd, opts); break;
|
|
373
|
+
case "headless": result = await spawnHeadless(cwd, opts); break;
|
|
289
374
|
}
|
|
375
|
+
// Surface the token on every result (success or failure) so callers
|
|
376
|
+
// can clean up registries deterministically.
|
|
377
|
+
return { ...result, spawnToken };
|
|
290
378
|
}
|
|
291
379
|
|
|
292
380
|
// ── Per-mechanism spawn ────────────────────────────────────────────────────
|
|
@@ -294,8 +382,12 @@ export async function spawnPiSession(
|
|
|
294
382
|
function spawnTmux(cwd: string, options?: SessionOptions): SpawnResult {
|
|
295
383
|
const exists = dashboardSessionExists();
|
|
296
384
|
const cmd = buildTmuxCommand(cwd, exists, options);
|
|
385
|
+
// Pass env explicitly so PI_DASHBOARD_SPAWN_TOKEN reaches the tmux pane's
|
|
386
|
+
// pi process (tmux inherits the caller's env into new windows/sessions).
|
|
387
|
+
// See change: spawn-correlation-token.
|
|
388
|
+
const env = buildSpawnEnv(process.env, { spawnToken: options?.spawnToken });
|
|
297
389
|
try {
|
|
298
|
-
execSync(cmd, { stdio: "ignore" });
|
|
390
|
+
execSync(cmd, { stdio: "ignore", env });
|
|
299
391
|
return {
|
|
300
392
|
success: true,
|
|
301
393
|
dashboardSpawned: true,
|
|
@@ -309,7 +401,8 @@ function spawnTmux(cwd: string, options?: SessionOptions): SpawnResult {
|
|
|
309
401
|
function spawnWslTmux(cwd: string, options?: SessionOptions): SpawnResult {
|
|
310
402
|
try {
|
|
311
403
|
const cmd = `wsl ${buildTmuxCommand(cwd, false, options)}`;
|
|
312
|
-
|
|
404
|
+
const env = buildSpawnEnv(process.env, { spawnToken: options?.spawnToken });
|
|
405
|
+
execSync(cmd, { stdio: "ignore", env });
|
|
313
406
|
return { success: true, dashboardSpawned: true, message: "Pi session spawned via WSL tmux" };
|
|
314
407
|
} catch (err: any) {
|
|
315
408
|
return { success: false, code: "TMUX_MISSING", message: `Failed to spawn via WSL tmux (wsl-tmux mechanism): ${err.message}` };
|
|
@@ -333,7 +426,7 @@ async function spawnWt(cwd: string, options?: SessionOptions): Promise<SpawnResu
|
|
|
333
426
|
cmd: wt,
|
|
334
427
|
args,
|
|
335
428
|
cwd,
|
|
336
|
-
env: buildSpawnEnv(),
|
|
429
|
+
env: buildSpawnEnv(process.env, { spawnToken: options?.spawnToken }),
|
|
337
430
|
});
|
|
338
431
|
|
|
339
432
|
if (!r.ok) {
|
|
@@ -351,7 +444,16 @@ async function spawnWt(cwd: string, options?: SessionOptions): Promise<SpawnResu
|
|
|
351
444
|
|
|
352
445
|
async function spawnHeadless(cwd: string, options?: SessionOptions): Promise<SpawnResult> {
|
|
353
446
|
const args = buildHeadlessArgs(options);
|
|
354
|
-
const env = buildSpawnEnv();
|
|
447
|
+
const env = buildSpawnEnv(process.env, { spawnToken: options?.spawnToken });
|
|
448
|
+
|
|
449
|
+
// RPC keeper sidecar path (feature-flagged). When enabled, both Unix and
|
|
450
|
+
// Windows go through the keeper (uniform durability across OSes). The
|
|
451
|
+
// keeper spawns pi internally via its own PATH lookup, so we do NOT need
|
|
452
|
+
// to resolve pi here. See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
453
|
+
if (shouldUseRpcKeeper()) {
|
|
454
|
+
return spawnHeadlessViaKeeper(cwd, env, args);
|
|
455
|
+
}
|
|
456
|
+
|
|
355
457
|
const piCmd = resolvePiCommand();
|
|
356
458
|
if (!piCmd) {
|
|
357
459
|
return { success: false, code: "PI_NOT_FOUND", message: `pi binary not found. Checked: ${MANAGED_BIN} and system PATH.` };
|
|
@@ -387,6 +489,75 @@ async function spawnHeadless(cwd: string, options?: SessionOptions): Promise<Spa
|
|
|
387
489
|
};
|
|
388
490
|
}
|
|
389
491
|
|
|
492
|
+
/**
|
|
493
|
+
* RPC keeper sidecar headless spawn. Uniform across Unix + Windows.
|
|
494
|
+
*
|
|
495
|
+
* The keeper itself is a CJS-pure Node script (`rpc-keeper/keeper.cjs`).
|
|
496
|
+
* It binds a per-session UDS / named pipe BEFORE spawning pi, then owns
|
|
497
|
+
* pi's stdin pipe so it survives dashboard server restarts.
|
|
498
|
+
*
|
|
499
|
+
* Returned `pid` is the KEEPER PID (not pi's). Pi's PID is linked later
|
|
500
|
+
* via the existing `session_register` token correlation path.
|
|
501
|
+
*
|
|
502
|
+
* Crash-detection window applies to KEEPER spawn only — the keeper itself
|
|
503
|
+
* runs a separate 300 ms window on its pi child internally (and surfaces
|
|
504
|
+
* the failure by exiting non-zero, which will be picked up by
|
|
505
|
+
* `headless-pid-registry`'s PID-death tracking).
|
|
506
|
+
*
|
|
507
|
+
* See change: add-rpc-stdin-dispatch-with-keeper-sidecar (Phase 5).
|
|
508
|
+
*/
|
|
509
|
+
async function spawnHeadlessViaKeeper(
|
|
510
|
+
cwd: string,
|
|
511
|
+
env: NodeJS.ProcessEnv,
|
|
512
|
+
piArgs: string[],
|
|
513
|
+
): Promise<SpawnResult> {
|
|
514
|
+
// sessionId is what the keeper uses to derive its UDS / named-pipe path.
|
|
515
|
+
// This is a TRANSPORT-side identifier, distinct from pi's session UUID
|
|
516
|
+
// (which only exists once pi's RPC mode boots). We mint a fresh one per
|
|
517
|
+
// spawn so the keeper's socket path is unique.
|
|
518
|
+
const transportId = randomUUID();
|
|
519
|
+
|
|
520
|
+
// piArgs already includes `--mode rpc` plus any per-spawn flags from
|
|
521
|
+
// `buildHeadlessArgs(options)` (e.g. `--session-file <path>` for resume,
|
|
522
|
+
// `--fork` for fork). Forwarding them through the keeper preserves the
|
|
523
|
+
// existing resume / fork contract. See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
524
|
+
const km = getKeeperManager();
|
|
525
|
+
const result = await km.spawnKeeperFor(transportId, cwd, env, piArgs);
|
|
526
|
+
if (!result.success || !result.pid || !result.process) {
|
|
527
|
+
return {
|
|
528
|
+
success: false,
|
|
529
|
+
code: "SPAWN_ERRNO",
|
|
530
|
+
message: `Failed to spawn RPC keeper: ${result.error ?? "unknown error"}`,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Crash-detection window on the keeper process itself. Keeper applies
|
|
535
|
+
// its own 300 ms window to pi internally; this catches keeper-side
|
|
536
|
+
// failures (bind failure, pi-spawn-error, etc.) that exit the keeper
|
|
537
|
+
// within the window.
|
|
538
|
+
const gate = await waitForNoCrash({ child: result.process, windowMs: 300 });
|
|
539
|
+
if (!gate.ok) {
|
|
540
|
+
return {
|
|
541
|
+
success: false,
|
|
542
|
+
code: "PI_CRASHED",
|
|
543
|
+
message:
|
|
544
|
+
`RPC keeper exited within crash window (code ${gate.exitCode}). ` +
|
|
545
|
+
`Check ~/.pi/dashboard/sessions/keeper-${transportId}.log for details.`,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
success: true,
|
|
551
|
+
dashboardSpawned: true,
|
|
552
|
+
message: `Pi session spawned via RPC keeper (keeper pid ${result.pid}, transport ${transportId.slice(0, 8)})`,
|
|
553
|
+
pid: result.pid,
|
|
554
|
+
process: result.process,
|
|
555
|
+
keeperSockPath: result.sockPath,
|
|
556
|
+
// spawnToken propagated by the outer wrapper; keeper-spawn doesn't
|
|
557
|
+
// mint its own. The token already lives in `env.PI_DASHBOARD_SPAWN_TOKEN`.
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
390
561
|
/**
|
|
391
562
|
* Windows headless spawn using the detached-spawn primitive.
|
|
392
563
|
*
|