@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.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 +87 -114
- package/README.md +408 -430
- package/docs/architecture.md +465 -12
- package/package.json +10 -5
- package/packages/extension/package.json +14 -4
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +40 -8
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- 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 +5 -4
- package/packages/extension/src/bridge.ts +171 -17
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +43 -0
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- 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 +83 -27
- package/packages/server/package.json +16 -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 +17 -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__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -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__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- 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__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +237 -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-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +111 -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__/terminal-manager.test.ts +41 -1
- 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 +13 -7
- 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 +8 -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 +310 -39
- 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 +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -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/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +207 -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 +141 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- 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 +211 -10
- 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 +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- 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 +56 -0
- 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__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -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 +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -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 +71 -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 +63 -46
- 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 +16 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -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/protocol.ts +23 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/state-replay.ts +9 -0
- package/packages/shared/src/tool-registry/definitions.ts +434 -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
|
@@ -2,12 +2,118 @@
|
|
|
2
2
|
* Thin adapter around pi's DefaultPackageManager.
|
|
3
3
|
* Serializes operations (one at a time), forwards progress events,
|
|
4
4
|
* and triggers session reload on success.
|
|
5
|
+
*
|
|
6
|
+
* Pi module resolution is delegated to the shared `ToolRegistry`
|
|
7
|
+
* (`resolveModule("pi-coding-agent")`). All strategy chains, caching,
|
|
8
|
+
* and diagnostic trails live there — see change: consolidate-tool-resolution.
|
|
5
9
|
*/
|
|
6
10
|
import * as os from "node:os";
|
|
7
11
|
import * as path from "node:path";
|
|
8
12
|
import * as crypto from "node:crypto";
|
|
9
|
-
import {
|
|
10
|
-
|
|
13
|
+
import {
|
|
14
|
+
getDefaultRegistry,
|
|
15
|
+
ModuleResolutionError,
|
|
16
|
+
type ToolRegistry,
|
|
17
|
+
type Resolution,
|
|
18
|
+
} from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
|
|
19
|
+
import {
|
|
20
|
+
getDefaultSubprocessAdapter,
|
|
21
|
+
type SubprocessAdapter,
|
|
22
|
+
} from "@blackbelt-technology/pi-dashboard-shared/platform/subprocess-adapter.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a command name through the tool registry's executor API.
|
|
26
|
+
* If the name is registered (e.g. "npm", "openspec", "pi"), returns
|
|
27
|
+
* the full executor argv — on Windows this is `[node.exe, <script>.js]`
|
|
28
|
+
* bypassing .cmd shims. Otherwise returns `[command, ...args]` verbatim
|
|
29
|
+
* so callers fall through to buildSafeArgv's generic handling.
|
|
30
|
+
*
|
|
31
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
32
|
+
*/
|
|
33
|
+
function resolveViaRegistry(
|
|
34
|
+
registry: ToolRegistry,
|
|
35
|
+
command: string,
|
|
36
|
+
args: readonly string[],
|
|
37
|
+
): string[] {
|
|
38
|
+
if (registry.has(command)) {
|
|
39
|
+
const exec = registry.resolveExecutor(command);
|
|
40
|
+
if (exec.ok && exec.argv.length > 0) {
|
|
41
|
+
return [...exec.argv, ...args];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [command, ...args];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Subclass of pi's `DefaultPackageManager` that routes every subprocess
|
|
49
|
+
* through our OS-aware `SubprocessAdapter`. Pi's upstream implementation
|
|
50
|
+
* spawns with `shell: process.platform === "win32"` and no `windowsHide`,
|
|
51
|
+
* which on Windows triggers Node issue #21825 — flashing cmd console
|
|
52
|
+
* every time pi shells out to npm / git / etc.
|
|
53
|
+
*
|
|
54
|
+
* This class overrides the three spawn methods pi exposes on its own
|
|
55
|
+
* class (`spawnCommand`, `spawnCaptureCommand`, `runCommandSync`) and
|
|
56
|
+
* delegates them to the adapter. Other methods inherit unchanged;
|
|
57
|
+
* pi's internal `runCommand` / `runCommandCapture` call the overridden
|
|
58
|
+
* methods via `this.spawnCommand(...)` so they pick up the safe
|
|
59
|
+
* behaviour automatically.
|
|
60
|
+
*
|
|
61
|
+
* Constructor factory takes the base `DefaultPackageManager` class as
|
|
62
|
+
* input so we can extend it dynamically at runtime (pi is loaded via
|
|
63
|
+
* the tool registry's `resolveModule`, not a static import).
|
|
64
|
+
*
|
|
65
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
66
|
+
*/
|
|
67
|
+
function createSafePackageManagerClass(
|
|
68
|
+
BaseClass: new (...args: any[]) => any,
|
|
69
|
+
adapter: SubprocessAdapter,
|
|
70
|
+
registry: ToolRegistry,
|
|
71
|
+
): new (...args: any[]) => any {
|
|
72
|
+
return class SafePackageManager extends BaseClass {
|
|
73
|
+
// `spawnCommand` — used by pi for fire-and-forget installs where
|
|
74
|
+
// stdout/stderr are inherited (or piped for capture). Returns the
|
|
75
|
+
// live ChildProcess.
|
|
76
|
+
//
|
|
77
|
+
// Registry resolution: `command` arrives as "npm" / "git" etc.
|
|
78
|
+
// For registered executor tools this becomes `[node.exe, cli.js]`
|
|
79
|
+
// on Windows, bypassing .cmd entirely.
|
|
80
|
+
spawnCommand(command: string, args: readonly string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
|
|
81
|
+
const [cmd, ...finalArgs] = resolveViaRegistry(registry, command, args);
|
|
82
|
+
return adapter.spawn(cmd, finalArgs, {
|
|
83
|
+
cwd: options?.cwd,
|
|
84
|
+
stdio: "inherit",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// `spawnCaptureCommand` — used by pi when it wants to read stdout /
|
|
89
|
+
// stderr programmatically (e.g. `npm root -g`, `npm view <pkg>`).
|
|
90
|
+
spawnCaptureCommand(command: string, args: readonly string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
|
|
91
|
+
const [cmd, ...finalArgs] = resolveViaRegistry(registry, command, args);
|
|
92
|
+
return adapter.spawn(cmd, finalArgs, {
|
|
93
|
+
cwd: options?.cwd,
|
|
94
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
95
|
+
env: options?.env ? { ...process.env, ...options.env } : process.env,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// `runCommandSync` — used for quick synchronous checks.
|
|
100
|
+
runCommandSync(command: string, args: readonly string[]) {
|
|
101
|
+
const [cmd, ...finalArgs] = resolveViaRegistry(registry, command, args);
|
|
102
|
+
const result = adapter.spawnSync<string>(cmd, finalArgs, {
|
|
103
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
});
|
|
106
|
+
if (result.status !== 0) {
|
|
107
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : String(result.stderr ?? "");
|
|
108
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : String(result.stdout ?? "");
|
|
109
|
+
throw new Error(`Failed to run ${command} ${args.join(" ")}: ${stderr || stdout}`);
|
|
110
|
+
}
|
|
111
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : String(result.stdout ?? "");
|
|
112
|
+
return stdout.trim();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
11
117
|
export interface ProgressEvent {
|
|
12
118
|
type: "start" | "progress" | "complete" | "error";
|
|
13
119
|
action: "install" | "remove" | "update" | "clone" | "pull";
|
|
@@ -15,52 +121,35 @@ export interface ProgressEvent {
|
|
|
15
121
|
message?: string;
|
|
16
122
|
}
|
|
17
123
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Try direct import first (works if installed as a dependency)
|
|
25
|
-
try {
|
|
26
|
-
const mod = await import("@mariozechner/pi-coding-agent") as any;
|
|
27
|
-
if (mod.DefaultPackageManager) {
|
|
28
|
-
piModuleCache = { DefaultPackageManager: mod.DefaultPackageManager, SettingsManager: mod.SettingsManager };
|
|
29
|
-
return piModuleCache;
|
|
30
|
-
}
|
|
31
|
-
} catch { /* fall through to global resolution */ }
|
|
32
|
-
|
|
33
|
-
// Try managed install at ~/.pi-dashboard/node_modules/ (Electron portable/standalone)
|
|
34
|
-
const managedDir = path.join(os.homedir(), ".pi-dashboard");
|
|
35
|
-
for (const pkgName of ["@mariozechner/pi-coding-agent", "@oh-my-pi/pi-coding-agent"]) {
|
|
36
|
-
try {
|
|
37
|
-
const entryPath = path.join(managedDir, "node_modules", pkgName, "dist", "index.js");
|
|
38
|
-
const mod = await import(pathToFileURL(entryPath).href);
|
|
39
|
-
if (mod.DefaultPackageManager) {
|
|
40
|
-
piModuleCache = { DefaultPackageManager: mod.DefaultPackageManager, SettingsManager: mod.SettingsManager };
|
|
41
|
-
return piModuleCache;
|
|
42
|
-
}
|
|
43
|
-
} catch { /* fall through */ }
|
|
44
|
-
}
|
|
124
|
+
/** Pi-coding-agent's public surface, as consumed by this wrapper. */
|
|
125
|
+
interface PiModule {
|
|
126
|
+
DefaultPackageManager: any;
|
|
127
|
+
SettingsManager: any;
|
|
128
|
+
}
|
|
45
129
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Resolve pi's package-manager API via the ToolRegistry. Surface the
|
|
132
|
+
* diagnostic trail on failure so callers (routes) can show the real
|
|
133
|
+
* reason instead of a generic "not installed" message.
|
|
134
|
+
*/
|
|
135
|
+
async function loadPiPackageManager(registry: ToolRegistry = getDefaultRegistry()): Promise<PiModule> {
|
|
136
|
+
const { module } = await registry.resolveModule<PiModule>("pi-coding-agent");
|
|
137
|
+
if (!module.DefaultPackageManager) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"pi-coding-agent resolved but does not export DefaultPackageManager (unexpected package version)",
|
|
140
|
+
);
|
|
57
141
|
}
|
|
142
|
+
return module;
|
|
143
|
+
}
|
|
58
144
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
145
|
+
/** Debug helper: expose the raw Resolution for diagnostic surfaces. */
|
|
146
|
+
export function diagnosePiPackageManager(registry: ToolRegistry = getDefaultRegistry()): Resolution {
|
|
147
|
+
return registry.resolve("pi-coding-agent");
|
|
62
148
|
}
|
|
63
149
|
|
|
150
|
+
/** Re-export so route handlers can `instanceof`-check for the rich error. */
|
|
151
|
+
export { ModuleResolutionError };
|
|
152
|
+
|
|
64
153
|
export type PackageScope = "global" | "local";
|
|
65
154
|
export type PackageAction = "install" | "remove" | "update";
|
|
66
155
|
|
|
@@ -78,6 +167,8 @@ export interface OperationResult {
|
|
|
78
167
|
scope: PackageScope;
|
|
79
168
|
success: boolean;
|
|
80
169
|
error?: string;
|
|
170
|
+
/** On failure: full resolution trail if pi couldn't be loaded. */
|
|
171
|
+
diagnostics?: Resolution;
|
|
81
172
|
}
|
|
82
173
|
|
|
83
174
|
export type ProgressListener = (operationId: string, event: ProgressEvent) => void;
|
|
@@ -91,6 +182,11 @@ export class PackageManagerWrapper {
|
|
|
91
182
|
private onComplete: CompleteListener | undefined;
|
|
92
183
|
/** Called after successful operation; returns number of sessions reloaded. */
|
|
93
184
|
private reloadSessions: (() => Promise<number>) | undefined;
|
|
185
|
+
private readonly registry: ToolRegistry;
|
|
186
|
+
|
|
187
|
+
constructor(registry: ToolRegistry = getDefaultRegistry()) {
|
|
188
|
+
this.registry = registry;
|
|
189
|
+
}
|
|
94
190
|
|
|
95
191
|
setProgressListener(listener: ProgressListener | undefined) {
|
|
96
192
|
this.onProgress = listener;
|
|
@@ -167,11 +263,63 @@ export class PackageManagerWrapper {
|
|
|
167
263
|
|
|
168
264
|
// ── Internal ────────────────────────────────────────────────────
|
|
169
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Per-cwd cache of DefaultPackageManager instances. Each instance holds
|
|
268
|
+
* its own SettingsManager + filesystem state; on Windows
|
|
269
|
+
* `listConfiguredPackages` can take several seconds on cold
|
|
270
|
+
* instantiation, so reusing the same instance across repeat calls
|
|
271
|
+
* (same cwd) eliminates the cost of the `/api/packages/recommended` +
|
|
272
|
+
* `/api/packages/installed` flows firing back-to-back.
|
|
273
|
+
*
|
|
274
|
+
* We also dedupe *in-flight* instantiations via `pmPending`: if two
|
|
275
|
+
* concurrent callers both ask for the same cwd before the first
|
|
276
|
+
* instantiation resolves, they share the same Promise instead of
|
|
277
|
+
* spawning parallel `DefaultPackageManager` constructions (which
|
|
278
|
+
* compete for the event loop and can double cold-start latency).
|
|
279
|
+
*
|
|
280
|
+
* `run()` invalidates the relevant entry after an install/remove/update
|
|
281
|
+
* so stale state never persists past a mutation.
|
|
282
|
+
*/
|
|
283
|
+
private readonly pmCache = new Map<string, unknown>();
|
|
284
|
+
private readonly pmPending = new Map<string, Promise<unknown>>();
|
|
285
|
+
|
|
170
286
|
private async createPackageManager(cwd?: string) {
|
|
171
|
-
const { DefaultPackageManager, SettingsManager } = await loadPiPackageManager();
|
|
172
287
|
const effectiveCwd = cwd ?? process.cwd();
|
|
173
|
-
const
|
|
174
|
-
|
|
288
|
+
const cached = this.pmCache.get(effectiveCwd);
|
|
289
|
+
if (cached) return cached as any;
|
|
290
|
+
const inflight = this.pmPending.get(effectiveCwd);
|
|
291
|
+
if (inflight) return inflight as any;
|
|
292
|
+
|
|
293
|
+
const promise = (async () => {
|
|
294
|
+
const { DefaultPackageManager, SettingsManager } = await loadPiPackageManager(this.registry);
|
|
295
|
+
const settingsManager = SettingsManager.create(effectiveCwd, AGENT_DIR);
|
|
296
|
+
// Wrap pi's DefaultPackageManager in our SafePackageManager so
|
|
297
|
+
// every internal `spawn` / `spawnSync` / `runCommandSync` call
|
|
298
|
+
// routes through the OS-aware SubprocessAdapter. This is THE
|
|
299
|
+
// fix for cmd.exe flashes on Windows caused by pi's upstream
|
|
300
|
+
// `shell: true + no windowsHide` spawn pattern.
|
|
301
|
+
const SafePM = createSafePackageManagerClass(
|
|
302
|
+
DefaultPackageManager,
|
|
303
|
+
getDefaultSubprocessAdapter(),
|
|
304
|
+
this.registry,
|
|
305
|
+
);
|
|
306
|
+
const pm = new SafePM({ cwd: effectiveCwd, agentDir: AGENT_DIR, settingsManager });
|
|
307
|
+
this.pmCache.set(effectiveCwd, pm);
|
|
308
|
+
return pm;
|
|
309
|
+
})();
|
|
310
|
+
this.pmPending.set(effectiveCwd, promise);
|
|
311
|
+
try {
|
|
312
|
+
return await promise;
|
|
313
|
+
} finally {
|
|
314
|
+
this.pmPending.delete(effectiveCwd);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Drop the cached package manager for a cwd (after install/remove/update). */
|
|
319
|
+
private invalidatePackageManager(cwd?: string): void {
|
|
320
|
+
const effectiveCwd = cwd ?? process.cwd();
|
|
321
|
+
this.pmCache.delete(effectiveCwd);
|
|
322
|
+
this.pmPending.delete(effectiveCwd);
|
|
175
323
|
}
|
|
176
324
|
|
|
177
325
|
private async executeOperation(operationId: string, req: OperationRequest): Promise<void> {
|
|
@@ -205,6 +353,10 @@ export class PackageManagerWrapper {
|
|
|
205
353
|
|
|
206
354
|
result.success = true;
|
|
207
355
|
|
|
356
|
+
// Invalidate the cached package manager for this cwd so future
|
|
357
|
+
// listInstalled() calls see the mutated settings.json.
|
|
358
|
+
this.invalidatePackageManager(req.cwd);
|
|
359
|
+
|
|
208
360
|
// Reload all sessions after successful operation
|
|
209
361
|
if (this.reloadSessions) {
|
|
210
362
|
try {
|
|
@@ -215,7 +367,15 @@ export class PackageManagerWrapper {
|
|
|
215
367
|
}
|
|
216
368
|
}
|
|
217
369
|
} catch (err: any) {
|
|
218
|
-
|
|
370
|
+
// Pi-not-found: surface the full Resolution trail to the caller
|
|
371
|
+
// so the UI can render per-strategy failure reasons instead of
|
|
372
|
+
// the old opaque "pi-coding-agent is not installed" message.
|
|
373
|
+
if (err instanceof ModuleResolutionError) {
|
|
374
|
+
result.error = err.message;
|
|
375
|
+
result.diagnostics = err.resolution;
|
|
376
|
+
} else {
|
|
377
|
+
result.error = err?.message ?? String(err);
|
|
378
|
+
}
|
|
219
379
|
} finally {
|
|
220
380
|
this.busy = false;
|
|
221
381
|
this.onComplete?.(result);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Version fetch reuses `fetchPackageMeta()` from the npm-search proxy.
|
|
17
17
|
* Results are cached for 5 minutes.
|
|
18
18
|
*/
|
|
19
|
-
import { execFile } from "node:child_process";
|
|
19
|
+
import { execFile } from "node:child_process"; // ban:child_process-ok pi-core check uses execFile + promisify for `npm list -g --json` output capture; refactoring to platform/spawn's Recipe engine is tracked tech debt
|
|
20
20
|
import { promisify } from "node:util";
|
|
21
21
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Coordinates with PackageManagerWrapper's busy-lock so extension
|
|
7
7
|
* operations and core updates can't run concurrently.
|
|
8
8
|
*/
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
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
10
|
import path from "node:path";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
@@ -50,10 +50,16 @@ function defaultRunNpmUpdate(
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
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).
|
|
53
57
|
const child = spawn("npm", args, {
|
|
54
58
|
cwd,
|
|
55
59
|
stdio: ["ignore", "pipe", "pipe"],
|
|
56
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,
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
const timer = setTimeout(() => {
|
|
@@ -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,207 @@
|
|
|
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
|
+
// `where` / `which` strategies typically return a symlinked npm bin
|
|
110
|
+
// launcher (e.g. ~/.nvm/.../bin/pi → ../lib/node_modules/@mariozechner/
|
|
111
|
+
// pi-coding-agent/dist/cli.js). Realpath the result first so the
|
|
112
|
+
// dirname math lands on the real pi module directory, not the
|
|
113
|
+
// bin-containing Node install prefix. See change: warn-pi-version-skew-in-cli.
|
|
114
|
+
try {
|
|
115
|
+
const res = registry.resolve("pi");
|
|
116
|
+
if (res.ok && res.path) {
|
|
117
|
+
let resolvedPath: string;
|
|
118
|
+
try {
|
|
119
|
+
resolvedPath = fs.realpathSync(res.path);
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const candidate = path.join(path.dirname(path.dirname(resolvedPath)), "package.json");
|
|
124
|
+
if (fs.existsSync(candidate)) {
|
|
125
|
+
const raw = fs.readFileSync(candidate, "utf8");
|
|
126
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
127
|
+
if (typeof parsed.version === "string") return parsed.version;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
/* ignore */
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Compute the `compatibility` snapshot from a compatibility range and
|
|
138
|
+
* the current pi version (or undefined when not yet installed). Pure
|
|
139
|
+
* function \u2014 all I/O is done by callers.
|
|
140
|
+
*/
|
|
141
|
+
export function computeCompatibility(
|
|
142
|
+
range: Pick<BootstrapCompatibility, "minimum" | "recommended" | "maximum">,
|
|
143
|
+
current: string | undefined,
|
|
144
|
+
): BootstrapCompatibility {
|
|
145
|
+
const out: BootstrapCompatibility = { ...range, current };
|
|
146
|
+
if (!current) return out;
|
|
147
|
+
if (isBelow(current, range.minimum)) {
|
|
148
|
+
// Minimum-violated is signalled by leaving `upgradeRecommended` true
|
|
149
|
+
// AND letting callers populate `bootstrapState.error` with the
|
|
150
|
+
// block-ops message.
|
|
151
|
+
out.upgradeRecommended = true;
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
if (isBelow(current, range.recommended)) {
|
|
155
|
+
out.upgradeRecommended = true;
|
|
156
|
+
}
|
|
157
|
+
if (range.maximum && isAbove(current, range.maximum)) {
|
|
158
|
+
out.upgradeDashboard = true;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface CacheEntry {
|
|
164
|
+
value: BootstrapCompatibility;
|
|
165
|
+
/** Milliseconds epoch when this entry should be discarded. */
|
|
166
|
+
expiresAt: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let cached: CacheEntry | undefined;
|
|
170
|
+
const CACHE_TTL_MS = 60_000;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Convenience wrapper: read range + current version, compute result,
|
|
174
|
+
* cache for 60 s. `store` is called with a structured compatibility
|
|
175
|
+
* update and (when minimum is violated) a blocking `error` message.
|
|
176
|
+
*/
|
|
177
|
+
export function updateBootstrapCompatibility(
|
|
178
|
+
store: BootstrapStateStore,
|
|
179
|
+
serverPkgJsonPath: string,
|
|
180
|
+
registry: ToolRegistry = getDefaultRegistry(),
|
|
181
|
+
now: () => number = Date.now,
|
|
182
|
+
): BootstrapCompatibility {
|
|
183
|
+
const t = now();
|
|
184
|
+
if (cached && t < cached.expiresAt) {
|
|
185
|
+
store.set({ compatibility: cached.value });
|
|
186
|
+
return cached.value;
|
|
187
|
+
}
|
|
188
|
+
const range = readPiCompatibility(serverPkgJsonPath);
|
|
189
|
+
const current = readCurrentPiVersion(registry);
|
|
190
|
+
const computed = computeCompatibility(range, current);
|
|
191
|
+
cached = { value: computed, expiresAt: t + CACHE_TTL_MS };
|
|
192
|
+
store.set({ compatibility: computed });
|
|
193
|
+
// Minimum-violated → block pi-dependent ops by setting `error`.
|
|
194
|
+
if (current && isBelow(current, range.minimum)) {
|
|
195
|
+
store.set({
|
|
196
|
+
error: {
|
|
197
|
+
message: `pi version ${current} is below minimum ${range.minimum}. Please run \`pi-dashboard upgrade-pi\`.`,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return computed;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Test helper: clear the 60-second cache between runs. */
|
|
205
|
+
export function _resetVersionSkewCache(): void {
|
|
206
|
+
cached = undefined;
|
|
207
|
+
}
|
|
@@ -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]);
|