@blackbelt-technology/pi-agent-dashboard 0.2.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/prompt-expander.ts +25 -4
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/cors.test.ts +34 -2
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +108 -9
- package/packages/server/src/browser-gateway.ts +16 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +363 -26
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +59 -3
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +93 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +71 -49
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
- package/packages/shared/src/types.ts +7 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi core package updater.
|
|
3
|
+
*
|
|
4
|
+
* Runs `npm update -g <pkg>` for globally-installed packages or
|
|
5
|
+
* `npm update <pkg>` in `~/.pi-dashboard/` for managed installs.
|
|
6
|
+
* Coordinates with PackageManagerWrapper's busy-lock so extension
|
|
7
|
+
* operations and core updates can't run concurrently.
|
|
8
|
+
*/
|
|
9
|
+
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
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import type { PiCorePackage, PiCoreUpdateResult } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
14
|
+
import type { PackageManagerWrapper } from "./package-manager-wrapper.js";
|
|
15
|
+
|
|
16
|
+
const UPDATE_TIMEOUT_MS = 5 * 60 * 1000; // 5 min per package
|
|
17
|
+
|
|
18
|
+
const MANAGED_DIR = path.join(os.homedir(), ".pi-dashboard");
|
|
19
|
+
|
|
20
|
+
export interface UpdateProgressEvent {
|
|
21
|
+
name: string;
|
|
22
|
+
phase: "start" | "output" | "complete" | "error";
|
|
23
|
+
message?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type UpdateProgressListener = (event: UpdateProgressEvent) => void;
|
|
27
|
+
|
|
28
|
+
export interface PiCoreUpdaterOptions {
|
|
29
|
+
packageManagerWrapper: PackageManagerWrapper;
|
|
30
|
+
/** Test seam: override spawner. */
|
|
31
|
+
runNpmUpdate?: (pkg: PiCorePackage, onOutput: (line: string) => void) => Promise<void>;
|
|
32
|
+
/** Optional: called after successful update of at least one package. */
|
|
33
|
+
onAllComplete?: () => Promise<number>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Default npm-update runner. */
|
|
37
|
+
function defaultRunNpmUpdate(
|
|
38
|
+
pkg: PiCorePackage,
|
|
39
|
+
onOutput: (line: string) => void,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const args =
|
|
43
|
+
pkg.installSource === "global"
|
|
44
|
+
? ["update", "-g", pkg.name]
|
|
45
|
+
: ["update", pkg.name];
|
|
46
|
+
const cwd = pkg.installSource === "managed" ? MANAGED_DIR : process.cwd();
|
|
47
|
+
|
|
48
|
+
if (pkg.installSource === "managed" && !existsSync(MANAGED_DIR)) {
|
|
49
|
+
reject(new Error(`Managed install directory not found: ${MANAGED_DIR}`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
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, {
|
|
58
|
+
cwd,
|
|
59
|
+
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)
|
|
62
|
+
windowsHide: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
child.kill("SIGKILL");
|
|
67
|
+
reject(new Error(`npm update timed out after ${UPDATE_TIMEOUT_MS / 1000}s`));
|
|
68
|
+
}, UPDATE_TIMEOUT_MS);
|
|
69
|
+
|
|
70
|
+
let stderrBuf = "";
|
|
71
|
+
|
|
72
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
73
|
+
const lines = chunk.toString().split("\n").filter((l) => l.trim());
|
|
74
|
+
for (const line of lines) onOutput(line);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
78
|
+
const text = chunk.toString();
|
|
79
|
+
stderrBuf += text;
|
|
80
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
81
|
+
for (const line of lines) onOutput(line);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
child.on("error", (err) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
reject(err);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
child.on("close", (code) => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (code === 0) {
|
|
92
|
+
resolve();
|
|
93
|
+
} else {
|
|
94
|
+
const hint =
|
|
95
|
+
pkg.installSource === "global" && /permission|EACCES|EPERM|EROFS/i.test(stderrBuf)
|
|
96
|
+
? ` (permission error — try: sudo npm update -g ${pkg.name})`
|
|
97
|
+
: "";
|
|
98
|
+
reject(new Error(`npm update exited with code ${code}${hint}`));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class PiCoreUpdater {
|
|
105
|
+
private listener: UpdateProgressListener | undefined;
|
|
106
|
+
private readonly pmWrapper: PackageManagerWrapper;
|
|
107
|
+
private readonly runNpmUpdate: (
|
|
108
|
+
pkg: PiCorePackage,
|
|
109
|
+
onOutput: (line: string) => void,
|
|
110
|
+
) => Promise<void>;
|
|
111
|
+
private readonly onAllComplete: (() => Promise<number>) | undefined;
|
|
112
|
+
|
|
113
|
+
constructor(opts: PiCoreUpdaterOptions) {
|
|
114
|
+
this.pmWrapper = opts.packageManagerWrapper;
|
|
115
|
+
this.runNpmUpdate = opts.runNpmUpdate ?? defaultRunNpmUpdate;
|
|
116
|
+
this.onAllComplete = opts.onAllComplete;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setProgressListener(listener: UpdateProgressListener | undefined): void {
|
|
120
|
+
this.listener = listener;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update a set of core packages sequentially. Acquires the shared
|
|
125
|
+
* busy-lock via PackageManagerWrapper.runExclusive — will throw
|
|
126
|
+
* PackageOperationBusyError if an extension operation is running.
|
|
127
|
+
*
|
|
128
|
+
* Returns per-package results plus the count of sessions reloaded
|
|
129
|
+
* after a successful update.
|
|
130
|
+
*/
|
|
131
|
+
async update(
|
|
132
|
+
packages: PiCorePackage[],
|
|
133
|
+
): Promise<{ results: PiCoreUpdateResult[]; sessionsReloaded: number }> {
|
|
134
|
+
return this.pmWrapper.runExclusive(async () => {
|
|
135
|
+
const results: PiCoreUpdateResult[] = [];
|
|
136
|
+
|
|
137
|
+
for (const pkg of packages) {
|
|
138
|
+
this.emit({ name: pkg.name, phase: "start", message: `Updating ${pkg.name}...` });
|
|
139
|
+
try {
|
|
140
|
+
await this.runNpmUpdate(pkg, (line) => {
|
|
141
|
+
this.emit({ name: pkg.name, phase: "output", message: line });
|
|
142
|
+
});
|
|
143
|
+
results.push({ name: pkg.name, success: true });
|
|
144
|
+
this.emit({ name: pkg.name, phase: "complete", message: `Updated ${pkg.name}` });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const msg = (err as Error).message ?? String(err);
|
|
147
|
+
results.push({ name: pkg.name, success: false, error: msg });
|
|
148
|
+
this.emit({ name: pkg.name, phase: "error", message: msg });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let sessionsReloaded = 0;
|
|
153
|
+
if (results.some((r) => r.success) && this.onAllComplete) {
|
|
154
|
+
try {
|
|
155
|
+
sessionsReloaded = await this.onAllComplete();
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error("[pi-core-updater] session reload failed:", err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { results, sessionsReloaded };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private emit(event: UpdateProgressEvent): void {
|
|
166
|
+
try {
|
|
167
|
+
this.listener?.(event);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error("[pi-core-updater] progress listener error:", err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -17,6 +17,8 @@ export interface PiGatewayOptions {
|
|
|
17
17
|
export interface PiGateway {
|
|
18
18
|
start(port: number): void;
|
|
19
19
|
stop(): void;
|
|
20
|
+
/** Resolved listening port after start() (useful when start(0) is used). Returns null if not started or closed. */
|
|
21
|
+
address(): number | null;
|
|
20
22
|
sendToSession(sessionId: string, msg: ServerToExtensionMessage): boolean;
|
|
21
23
|
broadcast(msg: ServerToExtensionMessage): void;
|
|
22
24
|
connectionCount(): number;
|
|
@@ -172,6 +174,11 @@ export function createPiGateway(
|
|
|
172
174
|
onSessionCreated = handler;
|
|
173
175
|
},
|
|
174
176
|
|
|
177
|
+
address() {
|
|
178
|
+
const addr = wss?.address();
|
|
179
|
+
if (addr && typeof addr === "object") return addr.port;
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
175
182
|
start(port: number) {
|
|
176
183
|
wss = new WebSocketServer({ port });
|
|
177
184
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import * as os from "node:os";
|
|
8
|
-
import
|
|
8
|
+
import * as npm from "@blackbelt-technology/pi-dashboard-shared/platform/npm.js";
|
|
9
9
|
import type { PiResource, PiResourceScope, PiPackageInfo, PiResourcesResult } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
10
10
|
|
|
11
11
|
// ── Frontmatter Parsing ─────────────────────────────────────────────
|
|
@@ -179,13 +179,10 @@ let cachedNpmGlobalRoot: string | null = null;
|
|
|
179
179
|
|
|
180
180
|
function getNpmGlobalRoot(): string | null {
|
|
181
181
|
if (cachedNpmGlobalRoot !== null) return cachedNpmGlobalRoot;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
cachedNpmGlobalRoot = "";
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
182
|
+
// Delegate to shared npm module which caches the result itself and
|
|
183
|
+
// handles windowsHide / timeout. See change: platform-command-executor.
|
|
184
|
+
cachedNpmGlobalRoot = npm.rootGlobalOr("");
|
|
185
|
+
return cachedNpmGlobalRoot || null;
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
/** Visible for testing — reset cached npm root */
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version-skew detection for pi-coding-agent.
|
|
3
|
+
*
|
|
4
|
+
* Reads `piCompatibility` from `packages/server/package.json` and the
|
|
5
|
+
* currently-resolved pi version from its `package.json`, then populates
|
|
6
|
+
* `bootstrapState.compatibility` with hints the UI banner uses to show
|
|
7
|
+
* upgrade suggestions.
|
|
8
|
+
*
|
|
9
|
+
* See change: unified-bootstrap-install \u00a79.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import type { BootstrapCompatibility, BootstrapStateStore } from "./bootstrap-state.js";
|
|
15
|
+
import { getDefaultRegistry, type ToolRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a semver-ish string into its three numeric segments. Returns
|
|
19
|
+
* null when the string doesn't match `<n>.<n>.<n>` (with optional
|
|
20
|
+
* pre-release / build suffix which we ignore for comparison). This is
|
|
21
|
+
* deliberately minimal \u2014 pi versions have always been `0.x.y` and we
|
|
22
|
+
* don't want to pull in the `semver` dep.
|
|
23
|
+
*/
|
|
24
|
+
export function parseVersion(v: string): [number, number, number] | null {
|
|
25
|
+
const m = v.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
26
|
+
if (!m) return null;
|
|
27
|
+
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compare two version strings. Returns -1 if `a < b`, 0 if equal, 1 if
|
|
32
|
+
* `a > b`. Unparseable strings sort as equal (conservative \u2014 don't flag
|
|
33
|
+
* weird versions as outdated).
|
|
34
|
+
*/
|
|
35
|
+
export function compareVersions(a: string, b: string): -1 | 0 | 1 {
|
|
36
|
+
const A = parseVersion(a);
|
|
37
|
+
const B = parseVersion(b);
|
|
38
|
+
if (!A || !B) return 0;
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
if (A[i] < B[i]) return -1;
|
|
41
|
+
if (A[i] > B[i]) return 1;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return true if `version` is less than `threshold`. Delegates to
|
|
48
|
+
* `compareVersions` so unparseable strings never flag as "too old".
|
|
49
|
+
*/
|
|
50
|
+
export function isBelow(version: string, threshold: string): boolean {
|
|
51
|
+
return compareVersions(version, threshold) < 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return true if `version` is strictly above `threshold`. `threshold`
|
|
56
|
+
* may include a `.x` wildcard in the patch slot (e.g. `"0.9.x"`); in
|
|
57
|
+
* that case the wildcard matches any patch, so `"0.9.5"` is NOT above
|
|
58
|
+
* `"0.9.x"` but `"0.10.0"` is.
|
|
59
|
+
*/
|
|
60
|
+
export function isAbove(version: string, threshold: string): boolean {
|
|
61
|
+
const thresholdClean = threshold.replace(/\.x$/i, ".99999");
|
|
62
|
+
return compareVersions(version, thresholdClean) > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read the server's declared compatibility range from its own package.json.
|
|
67
|
+
* Falls back to the hard-coded defaults when the field is missing or
|
|
68
|
+
* malformed (shouldn't happen in practice).
|
|
69
|
+
*/
|
|
70
|
+
export function readPiCompatibility(serverPkgJsonPath: string): Pick<
|
|
71
|
+
BootstrapCompatibility,
|
|
72
|
+
"minimum" | "recommended" | "maximum"
|
|
73
|
+
> {
|
|
74
|
+
try {
|
|
75
|
+
const raw = fs.readFileSync(serverPkgJsonPath, "utf8");
|
|
76
|
+
const parsed = JSON.parse(raw) as {
|
|
77
|
+
piCompatibility?: { minimum?: string; recommended?: string; maximum?: string | null };
|
|
78
|
+
};
|
|
79
|
+
const c = parsed.piCompatibility;
|
|
80
|
+
if (c && typeof c.minimum === "string" && typeof c.recommended === "string") {
|
|
81
|
+
return {
|
|
82
|
+
minimum: c.minimum,
|
|
83
|
+
recommended: c.recommended,
|
|
84
|
+
maximum: c.maximum ?? null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
/* fall through */
|
|
89
|
+
}
|
|
90
|
+
return { minimum: "0.6.7", recommended: "0.6.7", maximum: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read the currently-resolved pi version from `<pi-module>/../package.json`.
|
|
95
|
+
* Returns undefined when pi isn't resolvable or the package.json can't
|
|
96
|
+
* be parsed.
|
|
97
|
+
*/
|
|
98
|
+
export function readCurrentPiVersion(registry: ToolRegistry = getDefaultRegistry()): string | undefined {
|
|
99
|
+
try {
|
|
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;
|
|
105
|
+
} catch {
|
|
106
|
+
/* not resolvable yet */
|
|
107
|
+
}
|
|
108
|
+
// Fall back to the registry's resolved path + ../package.json.
|
|
109
|
+
try {
|
|
110
|
+
const res = registry.resolve("pi");
|
|
111
|
+
if (res.ok && res.path) {
|
|
112
|
+
const candidate = path.join(path.dirname(path.dirname(res.path)), "package.json");
|
|
113
|
+
if (fs.existsSync(candidate)) {
|
|
114
|
+
const raw = fs.readFileSync(candidate, "utf8");
|
|
115
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
116
|
+
if (typeof parsed.version === "string") return parsed.version;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
/* ignore */
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compute the `compatibility` snapshot from a compatibility range and
|
|
127
|
+
* the current pi version (or undefined when not yet installed). Pure
|
|
128
|
+
* function \u2014 all I/O is done by callers.
|
|
129
|
+
*/
|
|
130
|
+
export function computeCompatibility(
|
|
131
|
+
range: Pick<BootstrapCompatibility, "minimum" | "recommended" | "maximum">,
|
|
132
|
+
current: string | undefined,
|
|
133
|
+
): BootstrapCompatibility {
|
|
134
|
+
const out: BootstrapCompatibility = { ...range, current };
|
|
135
|
+
if (!current) return out;
|
|
136
|
+
if (isBelow(current, range.minimum)) {
|
|
137
|
+
// Minimum-violated is signalled by leaving `upgradeRecommended` true
|
|
138
|
+
// AND letting callers populate `bootstrapState.error` with the
|
|
139
|
+
// block-ops message.
|
|
140
|
+
out.upgradeRecommended = true;
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
if (isBelow(current, range.recommended)) {
|
|
144
|
+
out.upgradeRecommended = true;
|
|
145
|
+
}
|
|
146
|
+
if (range.maximum && isAbove(current, range.maximum)) {
|
|
147
|
+
out.upgradeDashboard = true;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface CacheEntry {
|
|
153
|
+
value: BootstrapCompatibility;
|
|
154
|
+
/** Milliseconds epoch when this entry should be discarded. */
|
|
155
|
+
expiresAt: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let cached: CacheEntry | undefined;
|
|
159
|
+
const CACHE_TTL_MS = 60_000;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convenience wrapper: read range + current version, compute result,
|
|
163
|
+
* cache for 60 s. `store` is called with a structured compatibility
|
|
164
|
+
* update and (when minimum is violated) a blocking `error` message.
|
|
165
|
+
*/
|
|
166
|
+
export function updateBootstrapCompatibility(
|
|
167
|
+
store: BootstrapStateStore,
|
|
168
|
+
serverPkgJsonPath: string,
|
|
169
|
+
registry: ToolRegistry = getDefaultRegistry(),
|
|
170
|
+
now: () => number = Date.now,
|
|
171
|
+
): BootstrapCompatibility {
|
|
172
|
+
const t = now();
|
|
173
|
+
if (cached && t < cached.expiresAt) {
|
|
174
|
+
store.set({ compatibility: cached.value });
|
|
175
|
+
return cached.value;
|
|
176
|
+
}
|
|
177
|
+
const range = readPiCompatibility(serverPkgJsonPath);
|
|
178
|
+
const current = readCurrentPiVersion(registry);
|
|
179
|
+
const computed = computeCompatibility(range, current);
|
|
180
|
+
cached = { value: computed, expiresAt: t + CACHE_TTL_MS };
|
|
181
|
+
store.set({ compatibility: computed });
|
|
182
|
+
// Minimum-violated → block pi-dependent ops by setting `error`.
|
|
183
|
+
if (current && isBelow(current, range.minimum)) {
|
|
184
|
+
store.set({
|
|
185
|
+
error: {
|
|
186
|
+
message: `pi version ${current} is below minimum ${range.minimum}. Please run \`pi-dashboard upgrade-pi\`.`,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return computed;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Test helper: clear the 60-second cache between runs. */
|
|
194
|
+
export function _resetVersionSkewCache(): void {
|
|
195
|
+
cached = undefined;
|
|
196
|
+
}
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { CONFIG_DIR } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
8
8
|
import { readJsonFile, writeJsonFile } from "./json-store.js";
|
|
9
9
|
import { safeRealpathSync } from "./resolve-path.js";
|
|
10
|
+
import { normalizePath } from "@blackbelt-technology/pi-dashboard-shared/platform/paths.js";
|
|
10
11
|
|
|
11
12
|
export const PREFERENCES_FILE = path.join(CONFIG_DIR, "preferences.json");
|
|
12
13
|
|
|
@@ -32,10 +33,23 @@ const DEBOUNCE_MS = 1000;
|
|
|
32
33
|
export function createPreferencesStore(filePath: string = PREFERENCES_FILE): PreferencesStore {
|
|
33
34
|
const data: PreferencesData = readJsonFile<PreferencesData>(filePath, { sessionOrder: {}, pinnedDirectories: [] });
|
|
34
35
|
let sessionOrder: Record<string, string[]> = data.sessionOrder ?? {};
|
|
35
|
-
//
|
|
36
|
+
// Normalize + resolve symlinks in stored pinned paths on load. Normalize
|
|
37
|
+
// FIRST so cosmetic drift (trailing separator, mixed separators,
|
|
38
|
+
// drive-letter case on Windows) collapses before realpath — then
|
|
39
|
+
// realpath handles symlinks. Order matters: realpath can fail for
|
|
40
|
+
// not-yet-existing paths, so we keep its best-effort fallback.
|
|
41
|
+
// See change: platform-path-normalization.
|
|
36
42
|
const rawPinned = data.pinnedDirectories ?? [];
|
|
37
|
-
|
|
38
|
-
//
|
|
43
|
+
// IMPORTANT: wrap in arrow fn — `Array.prototype.map` passes `(element,
|
|
44
|
+
// index, array)`, and `normalizePath`'s 2nd param is a `platform:
|
|
45
|
+
// NodeJS.Platform`. Passing the index (a number) silently disables the
|
|
46
|
+
// Windows branch at runtime.
|
|
47
|
+
let pinnedDirectories: string[] = rawPinned
|
|
48
|
+
.map((p) => normalizePath(p))
|
|
49
|
+
.map((p) => safeRealpathSync(p));
|
|
50
|
+
// Deduplicate post-normalization. Two previously-different entries that
|
|
51
|
+
// collapse to the same canonical form (e.g., with and without trailing
|
|
52
|
+
// slash) become one stored entry.
|
|
39
53
|
pinnedDirectories = [...new Set(pinnedDirectories)];
|
|
40
54
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
41
55
|
let dirty = pinnedDirectories.length !== rawPinned.length || pinnedDirectories.some((p, i) => p !== rawPinned[i]);
|