@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
|
@@ -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,39 +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 */ }
|
|
124
|
+
/** Pi-coding-agent's public surface, as consumed by this wrapper. */
|
|
125
|
+
interface PiModule {
|
|
126
|
+
DefaultPackageManager: any;
|
|
127
|
+
SettingsManager: any;
|
|
128
|
+
}
|
|
32
129
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
);
|
|
44
141
|
}
|
|
142
|
+
return module;
|
|
143
|
+
}
|
|
45
144
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
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");
|
|
49
148
|
}
|
|
50
149
|
|
|
150
|
+
/** Re-export so route handlers can `instanceof`-check for the rich error. */
|
|
151
|
+
export { ModuleResolutionError };
|
|
152
|
+
|
|
51
153
|
export type PackageScope = "global" | "local";
|
|
52
154
|
export type PackageAction = "install" | "remove" | "update";
|
|
53
155
|
|
|
@@ -65,6 +167,8 @@ export interface OperationResult {
|
|
|
65
167
|
scope: PackageScope;
|
|
66
168
|
success: boolean;
|
|
67
169
|
error?: string;
|
|
170
|
+
/** On failure: full resolution trail if pi couldn't be loaded. */
|
|
171
|
+
diagnostics?: Resolution;
|
|
68
172
|
}
|
|
69
173
|
|
|
70
174
|
export type ProgressListener = (operationId: string, event: ProgressEvent) => void;
|
|
@@ -78,6 +182,11 @@ export class PackageManagerWrapper {
|
|
|
78
182
|
private onComplete: CompleteListener | undefined;
|
|
79
183
|
/** Called after successful operation; returns number of sessions reloaded. */
|
|
80
184
|
private reloadSessions: (() => Promise<number>) | undefined;
|
|
185
|
+
private readonly registry: ToolRegistry;
|
|
186
|
+
|
|
187
|
+
constructor(registry: ToolRegistry = getDefaultRegistry()) {
|
|
188
|
+
this.registry = registry;
|
|
189
|
+
}
|
|
81
190
|
|
|
82
191
|
setProgressListener(listener: ProgressListener | undefined) {
|
|
83
192
|
this.onProgress = listener;
|
|
@@ -95,6 +204,24 @@ export class PackageManagerWrapper {
|
|
|
95
204
|
return this.busy;
|
|
96
205
|
}
|
|
97
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Run an arbitrary async operation under the wrapper's busy-lock.
|
|
209
|
+
* Used by adjacent subsystems (e.g. PiCoreUpdater) to coordinate with
|
|
210
|
+
* extension install/update operations. Throws PackageOperationBusyError
|
|
211
|
+
* if a package operation is already running.
|
|
212
|
+
*/
|
|
213
|
+
async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
|
|
214
|
+
if (this.busy) {
|
|
215
|
+
throw new PackageOperationBusyError();
|
|
216
|
+
}
|
|
217
|
+
this.busy = true;
|
|
218
|
+
try {
|
|
219
|
+
return await fn();
|
|
220
|
+
} finally {
|
|
221
|
+
this.busy = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
98
225
|
/**
|
|
99
226
|
* Start a package operation. Returns the operationId immediately.
|
|
100
227
|
* Progress and completion are delivered via listeners.
|
|
@@ -136,11 +263,63 @@ export class PackageManagerWrapper {
|
|
|
136
263
|
|
|
137
264
|
// ── Internal ────────────────────────────────────────────────────
|
|
138
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
|
+
|
|
139
286
|
private async createPackageManager(cwd?: string) {
|
|
140
|
-
const { DefaultPackageManager, SettingsManager } = await loadPiPackageManager();
|
|
141
287
|
const effectiveCwd = cwd ?? process.cwd();
|
|
142
|
-
const
|
|
143
|
-
|
|
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);
|
|
144
323
|
}
|
|
145
324
|
|
|
146
325
|
private async executeOperation(operationId: string, req: OperationRequest): Promise<void> {
|
|
@@ -174,6 +353,10 @@ export class PackageManagerWrapper {
|
|
|
174
353
|
|
|
175
354
|
result.success = true;
|
|
176
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
|
+
|
|
177
360
|
// Reload all sessions after successful operation
|
|
178
361
|
if (this.reloadSessions) {
|
|
179
362
|
try {
|
|
@@ -184,7 +367,15 @@ export class PackageManagerWrapper {
|
|
|
184
367
|
}
|
|
185
368
|
}
|
|
186
369
|
} catch (err: any) {
|
|
187
|
-
|
|
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
|
+
}
|
|
188
379
|
} finally {
|
|
189
380
|
this.busy = false;
|
|
190
381
|
this.onComplete?.(result);
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi core version checker.
|
|
3
|
+
*
|
|
4
|
+
* Discovers installed pi-ecosystem CORE packages (pi-coding-agent itself,
|
|
5
|
+
* pi-agent-dashboard, pi-model-proxy, and similar globally-installed CLI
|
|
6
|
+
* tooling) and compares their versions against the npm registry.
|
|
7
|
+
*
|
|
8
|
+
* Complements the existing PackageManagerWrapper, which only manages
|
|
9
|
+
* packages listed in `settings.json packages[]` (extensions, skills,
|
|
10
|
+
* prompts, themes).
|
|
11
|
+
*
|
|
12
|
+
* Discovery sources:
|
|
13
|
+
* 1. Global npm (`npm list -g --depth=0 --json`)
|
|
14
|
+
* 2. Managed install (`~/.pi-dashboard/node_modules/`) — Electron path
|
|
15
|
+
*
|
|
16
|
+
* Version fetch reuses `fetchPackageMeta()` from the npm-search proxy.
|
|
17
|
+
* Results are cached for 5 minutes.
|
|
18
|
+
*/
|
|
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
|
+
import { promisify } from "node:util";
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import { fetchPackageMeta } from "./npm-search-proxy.js";
|
|
25
|
+
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
27
|
+
|
|
28
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
29
|
+
const NPM_LIST_TIMEOUT_MS = 30_000;
|
|
30
|
+
|
|
31
|
+
/** ~/.pi-dashboard/ — Electron managed install dir */
|
|
32
|
+
const MANAGED_DIR = path.join(os.homedir(), ".pi-dashboard");
|
|
33
|
+
const MANAGED_NODE_MODULES = path.join(MANAGED_DIR, "node_modules");
|
|
34
|
+
|
|
35
|
+
/** Known core packages (not extensions). Order matters for display. */
|
|
36
|
+
export const CORE_PACKAGE_NAMES: readonly string[] = [
|
|
37
|
+
"@mariozechner/pi-coding-agent",
|
|
38
|
+
"@oh-my-pi/pi-coding-agent",
|
|
39
|
+
"@blackbelt-technology/pi-agent-dashboard",
|
|
40
|
+
"@blackbelt-technology/pi-model-proxy",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Display name mapping for known packages. Falls back to package name. */
|
|
44
|
+
const DISPLAY_NAMES: Readonly<Record<string, string>> = {
|
|
45
|
+
"@mariozechner/pi-coding-agent": "pi (core agent)",
|
|
46
|
+
"@oh-my-pi/pi-coding-agent": "pi (core agent — fork)",
|
|
47
|
+
"@blackbelt-technology/pi-agent-dashboard": "pi-dashboard",
|
|
48
|
+
"@blackbelt-technology/pi-model-proxy": "pi-model-proxy",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface PiCorePackage {
|
|
52
|
+
name: string;
|
|
53
|
+
displayName: string;
|
|
54
|
+
currentVersion: string;
|
|
55
|
+
latestVersion: string | null;
|
|
56
|
+
updateAvailable: boolean;
|
|
57
|
+
installSource: "global" | "managed";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PiCoreStatus {
|
|
61
|
+
packages: PiCorePackage[];
|
|
62
|
+
updatesAvailable: number;
|
|
63
|
+
lastChecked: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resolve display name for a package. */
|
|
67
|
+
function resolveDisplayName(name: string): string {
|
|
68
|
+
return DISPLAY_NAMES[name] ?? name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Heuristic to decide if a package is part of the pi ecosystem but NOT in
|
|
73
|
+
* the known-names list above. Matches bare-name pi packages on npm:
|
|
74
|
+
* - bare `pi-<name>`
|
|
75
|
+
* - scoped `@<scope>/pi-<name>`
|
|
76
|
+
* Note: extensions already managed by PackageManagerWrapper (via
|
|
77
|
+
* `settings.json packages[]`) are deliberately included if they are ALSO
|
|
78
|
+
* installed globally — the PiCoreChecker's discovery is a superset, and
|
|
79
|
+
* the UI layer decides which surface to show a package in.
|
|
80
|
+
*/
|
|
81
|
+
function looksLikePiEcosystem(name: string): boolean {
|
|
82
|
+
if (CORE_PACKAGE_NAMES.includes(name)) return true;
|
|
83
|
+
// `pi-foo` or `pi` bare-scoped
|
|
84
|
+
if (/^pi-[a-z0-9-]+$/i.test(name)) return true;
|
|
85
|
+
// scoped variant: `@scope/pi-foo`
|
|
86
|
+
if (/^@[^/]+\/pi-[a-z0-9-]+$/i.test(name)) return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface NpmListRunner {
|
|
91
|
+
/** Run `npm list -g --depth=0 --json` and return stdout. */
|
|
92
|
+
(): Promise<string>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface PiCoreCheckerOptions {
|
|
96
|
+
/** Inject npm-list runner (for tests). */
|
|
97
|
+
npmList?: NpmListRunner;
|
|
98
|
+
/** Inject version fetcher (for tests). */
|
|
99
|
+
fetchLatest?: (packageName: string) => Promise<string | null>;
|
|
100
|
+
/** Override managed directory (for tests). */
|
|
101
|
+
managedDir?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Default npm runner uses execFile for safety. */
|
|
105
|
+
const defaultNpmList: NpmListRunner = async () => {
|
|
106
|
+
const { stdout } = await execFileAsync("npm", ["list", "-g", "--depth=0", "--json"], {
|
|
107
|
+
timeout: NPM_LIST_TIMEOUT_MS,
|
|
108
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
109
|
+
});
|
|
110
|
+
return stdout;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const defaultFetchLatest = async (packageName: string): Promise<string | null> => {
|
|
114
|
+
const meta = await fetchPackageMeta(packageName);
|
|
115
|
+
return meta?.version ?? null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export class PiCoreChecker {
|
|
119
|
+
private cache: { at: number; data: PiCoreStatus } | null = null;
|
|
120
|
+
private readonly npmList: NpmListRunner;
|
|
121
|
+
private readonly fetchLatest: (packageName: string) => Promise<string | null>;
|
|
122
|
+
private readonly managedNodeModules: string;
|
|
123
|
+
|
|
124
|
+
constructor(opts: PiCoreCheckerOptions = {}) {
|
|
125
|
+
this.npmList = opts.npmList ?? defaultNpmList;
|
|
126
|
+
this.fetchLatest = opts.fetchLatest ?? defaultFetchLatest;
|
|
127
|
+
this.managedNodeModules = opts.managedDir
|
|
128
|
+
? path.join(opts.managedDir, "node_modules")
|
|
129
|
+
: MANAGED_NODE_MODULES;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Invalidate the cache (e.g. after an update completes). */
|
|
133
|
+
invalidate(): void {
|
|
134
|
+
this.cache = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Get version status. Returns cached data within 5 min unless `refresh`. */
|
|
138
|
+
async getStatus(refresh = false): Promise<PiCoreStatus> {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
if (!refresh && this.cache && now - this.cache.at < CACHE_TTL_MS) {
|
|
141
|
+
return this.cache.data;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Discover packages from both sources. Managed takes precedence on conflict.
|
|
145
|
+
const global = await this.discoverGlobal();
|
|
146
|
+
const managed = this.discoverManaged();
|
|
147
|
+
|
|
148
|
+
const byName = new Map<string, { version: string; source: "global" | "managed" }>();
|
|
149
|
+
for (const entry of global) byName.set(entry.name, { version: entry.version, source: "global" });
|
|
150
|
+
for (const entry of managed) byName.set(entry.name, { version: entry.version, source: "managed" });
|
|
151
|
+
|
|
152
|
+
// Fetch latest versions in parallel.
|
|
153
|
+
const entries = Array.from(byName.entries());
|
|
154
|
+
const withLatest = await Promise.all(
|
|
155
|
+
entries.map(async ([name, info]) => {
|
|
156
|
+
let latest: string | null = null;
|
|
157
|
+
try {
|
|
158
|
+
latest = await this.fetchLatest(name);
|
|
159
|
+
} catch {
|
|
160
|
+
latest = null;
|
|
161
|
+
}
|
|
162
|
+
const updateAvailable = latest !== null && latest !== info.version;
|
|
163
|
+
const pkg: PiCorePackage = {
|
|
164
|
+
name,
|
|
165
|
+
displayName: resolveDisplayName(name),
|
|
166
|
+
currentVersion: info.version,
|
|
167
|
+
latestVersion: latest,
|
|
168
|
+
updateAvailable,
|
|
169
|
+
installSource: info.source,
|
|
170
|
+
};
|
|
171
|
+
return pkg;
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Sort: known core packages first (in CORE_PACKAGE_NAMES order), then
|
|
176
|
+
// alphabetically. Then updates-available bubble up.
|
|
177
|
+
withLatest.sort((a, b) => {
|
|
178
|
+
const ai = CORE_PACKAGE_NAMES.indexOf(a.name);
|
|
179
|
+
const bi = CORE_PACKAGE_NAMES.indexOf(b.name);
|
|
180
|
+
if (ai !== -1 || bi !== -1) {
|
|
181
|
+
if (ai === -1) return 1;
|
|
182
|
+
if (bi === -1) return -1;
|
|
183
|
+
return ai - bi;
|
|
184
|
+
}
|
|
185
|
+
return a.name.localeCompare(b.name);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const status: PiCoreStatus = {
|
|
189
|
+
packages: withLatest,
|
|
190
|
+
updatesAvailable: withLatest.filter((p) => p.updateAvailable).length,
|
|
191
|
+
lastChecked: new Date().toISOString(),
|
|
192
|
+
};
|
|
193
|
+
this.cache = { at: now, data: status };
|
|
194
|
+
return status;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Discover pi-ecosystem packages installed via `npm -g`. */
|
|
198
|
+
private async discoverGlobal(): Promise<Array<{ name: string; version: string }>> {
|
|
199
|
+
let stdout = "";
|
|
200
|
+
try {
|
|
201
|
+
stdout = await this.npmList();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
// `npm list` exits non-zero when it has warnings — stdout may still be valid JSON.
|
|
204
|
+
// execFile throws with .stdout attached in that case.
|
|
205
|
+
const maybe = (err as { stdout?: string })?.stdout;
|
|
206
|
+
if (typeof maybe === "string" && maybe.length > 0) {
|
|
207
|
+
stdout = maybe;
|
|
208
|
+
} else {
|
|
209
|
+
console.warn("[pi-core-checker] npm list -g failed:", (err as Error).message);
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let parsed: unknown;
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(stdout);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.warn("[pi-core-checker] npm list -g: failed to parse JSON:", (err as Error).message);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const deps = (parsed as { dependencies?: Record<string, { version?: string; resolved?: string }> })?.dependencies;
|
|
223
|
+
if (!deps || typeof deps !== "object") return [];
|
|
224
|
+
|
|
225
|
+
const out: Array<{ name: string; version: string }> = [];
|
|
226
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
227
|
+
if (!looksLikePiEcosystem(name)) continue;
|
|
228
|
+
const version = typeof info?.version === "string" ? info.version : undefined;
|
|
229
|
+
if (!version) continue;
|
|
230
|
+
out.push({ name, version });
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Discover pi-ecosystem packages in ~/.pi-dashboard/node_modules/. */
|
|
236
|
+
private discoverManaged(): Array<{ name: string; version: string }> {
|
|
237
|
+
if (!existsSync(this.managedNodeModules)) return [];
|
|
238
|
+
const out: Array<{ name: string; version: string }> = [];
|
|
239
|
+
let entries: string[];
|
|
240
|
+
try {
|
|
241
|
+
entries = readdirSync(this.managedNodeModules);
|
|
242
|
+
} catch {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
if (entry.startsWith(".")) continue;
|
|
248
|
+
const full = path.join(this.managedNodeModules, entry);
|
|
249
|
+
if (entry.startsWith("@")) {
|
|
250
|
+
// Scoped: iterate one level deeper.
|
|
251
|
+
let sub: string[];
|
|
252
|
+
try {
|
|
253
|
+
sub = readdirSync(full);
|
|
254
|
+
} catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
for (const pkg of sub) {
|
|
258
|
+
const pkgName = `${entry}/${pkg}`;
|
|
259
|
+
if (!looksLikePiEcosystem(pkgName)) continue;
|
|
260
|
+
const v = this.readVersion(path.join(full, pkg));
|
|
261
|
+
if (v) out.push({ name: pkgName, version: v });
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
if (!looksLikePiEcosystem(entry)) continue;
|
|
265
|
+
const v = this.readVersion(full);
|
|
266
|
+
if (v) out.push({ name: entry, version: v });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private readVersion(pkgDir: string): string | null {
|
|
273
|
+
try {
|
|
274
|
+
const pj = path.join(pkgDir, "package.json");
|
|
275
|
+
if (!existsSync(pj)) return null;
|
|
276
|
+
if (!statSync(pj).isFile()) return null;
|
|
277
|
+
const parsed = JSON.parse(readFileSync(pj, "utf-8"));
|
|
278
|
+
return typeof parsed?.version === "string" ? parsed.version : null;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const _internal = {
|
|
286
|
+
looksLikePiEcosystem,
|
|
287
|
+
resolveDisplayName,
|
|
288
|
+
DISPLAY_NAMES,
|
|
289
|
+
MANAGED_NODE_MODULES,
|
|
290
|
+
};
|