@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,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ToolRegistry (packages/shared/src/tool-registry/registry.ts).
|
|
3
|
+
*
|
|
4
|
+
* Covered scenarios (from `specs/tool-registry/spec.md`):
|
|
5
|
+
* - Resolve a registered binary
|
|
6
|
+
* - Resolve an unregistered name throws UnknownToolError
|
|
7
|
+
* - Cached Resolution is referentially equal on second resolve()
|
|
8
|
+
* - rescan(name) invalidates one; rescan() invalidates all
|
|
9
|
+
* - First strategy wins; subsequent strategies not executed
|
|
10
|
+
* - Failing strategies recorded in tried[] and iteration continues
|
|
11
|
+
* - All-fail produces ok:false with full tried[] trail
|
|
12
|
+
* - resolveModule: caches loaded module; throws ModuleResolutionError on fail
|
|
13
|
+
* - setOverride / clearOverride invalidate cached Resolution
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from "vitest";
|
|
16
|
+
import {
|
|
17
|
+
ToolRegistry,
|
|
18
|
+
UnknownToolError,
|
|
19
|
+
ModuleResolutionError,
|
|
20
|
+
type Strategy,
|
|
21
|
+
type ToolDefinition,
|
|
22
|
+
} from "../tool-registry/index.js";
|
|
23
|
+
import { OverridesStore } from "../tool-registry/overrides.js";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
|
|
28
|
+
// ── Test helpers ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** Make a strategy that always returns the given path. */
|
|
31
|
+
function fixedOk(name: string, p: string): Strategy {
|
|
32
|
+
return { name, run: () => ({ ok: true, path: p }) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Make a strategy that records its invocation for "not executed" assertions. */
|
|
36
|
+
function spyOk(name: string, p: string, tag: { called: boolean }): Strategy {
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
run: () => {
|
|
40
|
+
tag.called = true;
|
|
41
|
+
return { ok: true, path: p };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Make a strategy that always fails with the given reason. */
|
|
47
|
+
function fail(name: string, reason: string): Strategy {
|
|
48
|
+
return { name, run: () => ({ ok: false, reason }) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** In-memory OverridesStore backed by a tmp file (for set/clear flow). */
|
|
52
|
+
function tmpOverridesStore(): OverridesStore {
|
|
53
|
+
const fp = path.join(
|
|
54
|
+
os.tmpdir(),
|
|
55
|
+
`tool-overrides-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
|
|
56
|
+
);
|
|
57
|
+
return new OverridesStore({ filePath: fp, warn: () => {} });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function binaryDef(name: string, strategies: Strategy[]): ToolDefinition {
|
|
61
|
+
return { name, kind: "binary", strategies };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function moduleDef(name: string, strategies: Strategy[]): ToolDefinition {
|
|
65
|
+
return { name, kind: "module", strategies };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
describe("ToolRegistry.resolve", () => {
|
|
71
|
+
it("returns a Resolution object for a registered binary", () => {
|
|
72
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
73
|
+
r.register(binaryDef("pi", [fixedOk("where", "/usr/local/bin/pi")]));
|
|
74
|
+
|
|
75
|
+
const res = r.resolve("pi");
|
|
76
|
+
expect(res.name).toBe("pi");
|
|
77
|
+
expect(res.ok).toBe(true);
|
|
78
|
+
expect(res.path).toBe("/usr/local/bin/pi");
|
|
79
|
+
expect(res.tried).toEqual([{ strategy: "where", result: "ok" }]);
|
|
80
|
+
expect(typeof res.resolvedAt).toBe("number");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("throws UnknownToolError for an unregistered name", () => {
|
|
84
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
85
|
+
expect(() => r.resolve("nope")).toThrowError(UnknownToolError);
|
|
86
|
+
try { r.resolve("nope"); } catch (e) {
|
|
87
|
+
expect((e as UnknownToolError).tool).toBe("nope");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns the same cached Resolution on a second call (referentially equal)", () => {
|
|
92
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
93
|
+
r.register(binaryDef("pi", [fixedOk("where", "/usr/bin/pi")]));
|
|
94
|
+
|
|
95
|
+
const a = r.resolve("pi");
|
|
96
|
+
const b = r.resolve("pi");
|
|
97
|
+
expect(a).toBe(b);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("ToolRegistry strategy chain", () => {
|
|
102
|
+
it("first-successful-strategy wins and short-circuits the chain", () => {
|
|
103
|
+
const second = { called: false };
|
|
104
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
105
|
+
r.register(
|
|
106
|
+
binaryDef("pi", [
|
|
107
|
+
fixedOk("managed", "/managed/pi"),
|
|
108
|
+
spyOk("where", "/usr/bin/pi", second),
|
|
109
|
+
]),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const res = r.resolve("pi");
|
|
113
|
+
expect(res.ok).toBe(true);
|
|
114
|
+
expect(res.path).toBe("/managed/pi");
|
|
115
|
+
expect(res.source).toBe("managed");
|
|
116
|
+
expect(res.tried).toEqual([{ strategy: "managed", result: "ok" }]);
|
|
117
|
+
expect(second.called).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("records failing strategies in tried[] and continues", () => {
|
|
121
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
122
|
+
r.register(
|
|
123
|
+
binaryDef("pi", [
|
|
124
|
+
fail("override", "no override set"),
|
|
125
|
+
fail("managed", "missing: /bad/path"),
|
|
126
|
+
fixedOk("where", "/usr/bin/pi"),
|
|
127
|
+
]),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const res = r.resolve("pi");
|
|
131
|
+
expect(res.ok).toBe(true);
|
|
132
|
+
expect(res.source).toBe("system");
|
|
133
|
+
expect(res.tried).toEqual([
|
|
134
|
+
{ strategy: "override", result: "no override set" },
|
|
135
|
+
{ strategy: "managed", result: "missing: /bad/path" },
|
|
136
|
+
{ strategy: "where", result: "ok" },
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("produces ok:false with full trail when every strategy fails", () => {
|
|
141
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
142
|
+
r.register(
|
|
143
|
+
binaryDef("pi", [fail("a", "reason a"), fail("b", "reason b")]),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const res = r.resolve("pi");
|
|
147
|
+
expect(res.ok).toBe(false);
|
|
148
|
+
expect(res.path).toBeNull();
|
|
149
|
+
expect(res.source).toBeNull();
|
|
150
|
+
expect(res.tried).toEqual([
|
|
151
|
+
{ strategy: "a", result: "reason a" },
|
|
152
|
+
{ strategy: "b", result: "reason b" },
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("validate() demotes strategy to failure with 'invalid: <reason>'", () => {
|
|
157
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
158
|
+
r.register({
|
|
159
|
+
...binaryDef("pi", [fixedOk("override", "/bogus"), fixedOk("where", "/usr/bin/pi")]),
|
|
160
|
+
validate: (p) =>
|
|
161
|
+
p === "/bogus" ? { ok: false, reason: "not a file" } : { ok: true },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const res = r.resolve("pi");
|
|
165
|
+
expect(res.ok).toBe(true);
|
|
166
|
+
expect(res.path).toBe("/usr/bin/pi");
|
|
167
|
+
expect(res.tried[0]).toEqual({ strategy: "override", result: "invalid: not a file" });
|
|
168
|
+
expect(res.tried[1]).toEqual({ strategy: "where", result: "ok" });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("ToolRegistry.rescan", () => {
|
|
173
|
+
it("rescan(name) clears just that tool's cache", () => {
|
|
174
|
+
let callCount = 0;
|
|
175
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
176
|
+
r.register(
|
|
177
|
+
binaryDef("pi", [
|
|
178
|
+
{
|
|
179
|
+
name: "where",
|
|
180
|
+
run: () => ({ ok: true, path: `/usr/bin/pi${++callCount}` }),
|
|
181
|
+
},
|
|
182
|
+
]),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const first = r.resolve("pi");
|
|
186
|
+
expect(first.path).toBe("/usr/bin/pi1");
|
|
187
|
+
|
|
188
|
+
r.rescan("pi");
|
|
189
|
+
const second = r.resolve("pi");
|
|
190
|
+
expect(second.path).toBe("/usr/bin/pi2");
|
|
191
|
+
expect(second).not.toBe(first);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("rescan() without arg clears everything", () => {
|
|
195
|
+
let a = 0, b = 0;
|
|
196
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
197
|
+
r.register(binaryDef("pi", [{ name: "where", run: () => ({ ok: true, path: `/pi${++a}` }) }]));
|
|
198
|
+
r.register(binaryDef("git", [{ name: "where", run: () => ({ ok: true, path: `/git${++b}` }) }]));
|
|
199
|
+
|
|
200
|
+
r.resolve("pi"); r.resolve("git");
|
|
201
|
+
r.rescan();
|
|
202
|
+
expect(r.resolve("pi").path).toBe("/pi2");
|
|
203
|
+
expect(r.resolve("git").path).toBe("/git2");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("ToolRegistry.resolveModule", () => {
|
|
208
|
+
it("caches the loaded module and returns the same reference on second call", async () => {
|
|
209
|
+
const fakeModule = { DefaultPackageManager: () => "dpm" };
|
|
210
|
+
let importCalls = 0;
|
|
211
|
+
const r = new ToolRegistry({
|
|
212
|
+
overrides: tmpOverridesStore(),
|
|
213
|
+
importModule: async () => {
|
|
214
|
+
importCalls++;
|
|
215
|
+
return fakeModule;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
r.register(moduleDef("pi-coding-agent", [fixedOk("managed", "/managed/pi/dist/index.js")]));
|
|
219
|
+
|
|
220
|
+
const a = await r.resolveModule("pi-coding-agent");
|
|
221
|
+
const b = await r.resolveModule("pi-coding-agent");
|
|
222
|
+
expect(a.module).toBe(fakeModule);
|
|
223
|
+
expect(b.module).toBe(fakeModule);
|
|
224
|
+
expect(importCalls).toBe(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("throws ModuleResolutionError with trail when every strategy fails", async () => {
|
|
228
|
+
const r = new ToolRegistry({
|
|
229
|
+
overrides: tmpOverridesStore(),
|
|
230
|
+
importModule: async () => { throw new Error("should not import"); },
|
|
231
|
+
});
|
|
232
|
+
r.register(moduleDef("pi-coding-agent", [fail("a", "nope"), fail("b", "also nope")]));
|
|
233
|
+
|
|
234
|
+
await expect(r.resolveModule("pi-coding-agent")).rejects.toBeInstanceOf(ModuleResolutionError);
|
|
235
|
+
try {
|
|
236
|
+
await r.resolveModule("pi-coding-agent");
|
|
237
|
+
} catch (e) {
|
|
238
|
+
const err = e as ModuleResolutionError;
|
|
239
|
+
expect(err.resolution.tried).toEqual([
|
|
240
|
+
{ strategy: "a", result: "nope" },
|
|
241
|
+
{ strategy: "b", result: "also nope" },
|
|
242
|
+
]);
|
|
243
|
+
expect(err.message).toContain("a: nope");
|
|
244
|
+
expect(err.message).toContain("b: also nope");
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("refuses to resolve a non-module tool", async () => {
|
|
249
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
250
|
+
r.register(binaryDef("pi", [fixedOk("where", "/usr/bin/pi")]));
|
|
251
|
+
await expect(r.resolveModule("pi")).rejects.toThrow(/not kind: "module"/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("rescan(name) drops the cached module so the next call re-imports", async () => {
|
|
255
|
+
let importCalls = 0;
|
|
256
|
+
const r = new ToolRegistry({
|
|
257
|
+
overrides: tmpOverridesStore(),
|
|
258
|
+
importModule: async () => ({ n: ++importCalls }),
|
|
259
|
+
});
|
|
260
|
+
r.register(moduleDef("pi-coding-agent", [fixedOk("managed", "/x/dist/index.js")]));
|
|
261
|
+
|
|
262
|
+
const a = await r.resolveModule("pi-coding-agent");
|
|
263
|
+
r.rescan("pi-coding-agent");
|
|
264
|
+
const b = await r.resolveModule("pi-coding-agent");
|
|
265
|
+
expect((a.module as { n: number }).n).toBe(1);
|
|
266
|
+
expect((b.module as { n: number }).n).toBe(2);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("ToolRegistry overrides", () => {
|
|
271
|
+
it("setOverride invalidates cache and the next resolve() picks override source", () => {
|
|
272
|
+
const store = tmpOverridesStore();
|
|
273
|
+
const r = new ToolRegistry({ overrides: store });
|
|
274
|
+
r.register(
|
|
275
|
+
binaryDef("pi", [
|
|
276
|
+
{
|
|
277
|
+
name: "override",
|
|
278
|
+
run: (ctx) => ctx.overrides["pi"]
|
|
279
|
+
? { ok: true, path: ctx.overrides["pi"] }
|
|
280
|
+
: { ok: false, reason: "no override set" },
|
|
281
|
+
},
|
|
282
|
+
fixedOk("where", "/usr/bin/pi"),
|
|
283
|
+
]),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(r.resolve("pi").path).toBe("/usr/bin/pi");
|
|
287
|
+
r.setOverride("pi", "/custom/pi");
|
|
288
|
+
const next = r.resolve("pi");
|
|
289
|
+
expect(next.path).toBe("/custom/pi");
|
|
290
|
+
expect(next.source).toBe("override");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("clearOverride removes the entry and falls back to next strategy", () => {
|
|
294
|
+
const store = tmpOverridesStore();
|
|
295
|
+
store.set("pi", "/custom/pi");
|
|
296
|
+
const r = new ToolRegistry({ overrides: store });
|
|
297
|
+
r.register(
|
|
298
|
+
binaryDef("pi", [
|
|
299
|
+
{
|
|
300
|
+
name: "override",
|
|
301
|
+
run: (ctx) => ctx.overrides["pi"]
|
|
302
|
+
? { ok: true, path: ctx.overrides["pi"] }
|
|
303
|
+
: { ok: false, reason: "no override set" },
|
|
304
|
+
},
|
|
305
|
+
fixedOk("where", "/usr/bin/pi"),
|
|
306
|
+
]),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(r.resolve("pi").path).toBe("/custom/pi");
|
|
310
|
+
r.clearOverride("pi");
|
|
311
|
+
expect(r.resolve("pi").path).toBe("/usr/bin/pi");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("setOverride throws UnknownToolError for unregistered names", () => {
|
|
315
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
316
|
+
expect(() => r.setOverride("ghost", "/x")).toThrow(UnknownToolError);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("ToolRegistry.list", () => {
|
|
321
|
+
it("returns one Resolution per registered tool", () => {
|
|
322
|
+
const r = new ToolRegistry({ overrides: tmpOverridesStore() });
|
|
323
|
+
r.register(binaryDef("pi", [fixedOk("where", "/pi")]));
|
|
324
|
+
r.register(binaryDef("git", [fail("where", "not found")]));
|
|
325
|
+
|
|
326
|
+
const all = r.list();
|
|
327
|
+
expect(all.map((x) => x.name).sort()).toEqual(["git", "pi"]);
|
|
328
|
+
expect(all.find((x) => x.name === "pi")!.ok).toBe(true);
|
|
329
|
+
expect(all.find((x) => x.name === "git")!.ok).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Clean up any stray tmp files the tmpOverridesStore helper might leave.
|
|
334
|
+
afterAll();
|
|
335
|
+
function afterAll() {
|
|
336
|
+
try {
|
|
337
|
+
for (const f of fs.readdirSync(os.tmpdir())) {
|
|
338
|
+
if (f.startsWith("tool-overrides-test-")) {
|
|
339
|
+
try { fs.unlinkSync(path.join(os.tmpdir(), f)); } catch {}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared bootstrap installer — single entry point for installing pi,
|
|
3
|
+
* openspec, tsx, and recommended packages into the managed directory
|
|
4
|
+
* (~/.pi-dashboard/). Callable from any entry point: Electron wizard,
|
|
5
|
+
* `pi-dashboard` CLI first-run, `pi-dashboard upgrade-pi` subcommand,
|
|
6
|
+
* and the `POST /api/bootstrap/upgrade-pi` REST handler.
|
|
7
|
+
*
|
|
8
|
+
* This module is deliberately free of Electron-specific concerns
|
|
9
|
+
* (bundled-node, offline-bundle cacache, resourcesPath). Those remain
|
|
10
|
+
* in `packages/electron/src/lib/dependency-installer.ts` which now
|
|
11
|
+
* delegates its "install from npm registry" step to this function.
|
|
12
|
+
*
|
|
13
|
+
* See change: unified-bootstrap-install.
|
|
14
|
+
*/
|
|
15
|
+
import { spawn as cpSpawn } from "./platform/exec.js";
|
|
16
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { getManagedDir } from "./managed-paths.js";
|
|
19
|
+
import { getDefaultRegistry, type ToolRegistry } from "./tool-registry/index.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Per-package progress tick. Mirrors the Electron `InstallProgress`
|
|
23
|
+
* shape so existing wizard UI code needs no changes.
|
|
24
|
+
*/
|
|
25
|
+
export interface InstallProgress {
|
|
26
|
+
step: string;
|
|
27
|
+
status: "pending" | "running" | "done" | "error";
|
|
28
|
+
error?: string;
|
|
29
|
+
/** Last line of npm output (for streaming progress). */
|
|
30
|
+
output?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ProgressCallback = (progress: InstallProgress) => void;
|
|
34
|
+
|
|
35
|
+
export interface BootstrapInstallOptions {
|
|
36
|
+
/** Packages to install via `npm install <pkg>` (registry fetch). */
|
|
37
|
+
packages: string[];
|
|
38
|
+
/** Root of the managed install. Defaults to `getManagedDir()`. */
|
|
39
|
+
managedDir?: string;
|
|
40
|
+
/** Called on every progress tick (pending/running/done/error). */
|
|
41
|
+
progress?: ProgressCallback;
|
|
42
|
+
/**
|
|
43
|
+
* Optional override of the npm invocation. By default the function
|
|
44
|
+
* resolves the `npm` tool via `ToolRegistry.resolve("npm")` and
|
|
45
|
+
* falls back to the plain `npm` / `npm.cmd` binary on PATH. When
|
|
46
|
+
* Electron wants to steer the install to bundled Node + npm-cli.js,
|
|
47
|
+
* it passes the full argv prefix (e.g. `["<path>/node", "<path>/npm-cli.js"]`).
|
|
48
|
+
*/
|
|
49
|
+
npmArgv?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Optional environment overrides merged into the child process env.
|
|
52
|
+
* Electron uses this to put bundled Node on PATH for postinstall
|
|
53
|
+
* scripts.
|
|
54
|
+
*/
|
|
55
|
+
env?: NodeJS.ProcessEnv;
|
|
56
|
+
/**
|
|
57
|
+
* Inject a tool registry (tests). Defaults to `getDefaultRegistry()`.
|
|
58
|
+
*/
|
|
59
|
+
registry?: ToolRegistry;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BootstrapInstallSuccess {
|
|
63
|
+
ok: true;
|
|
64
|
+
installed: string[];
|
|
65
|
+
managedDir: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BootstrapInstallFailure {
|
|
69
|
+
ok: false;
|
|
70
|
+
error: string;
|
|
71
|
+
installed: string[];
|
|
72
|
+
managedDir: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type BootstrapInstallResult = BootstrapInstallSuccess | BootstrapInstallFailure;
|
|
76
|
+
|
|
77
|
+
/** Ensure the managed directory exists with a package.json. */
|
|
78
|
+
export function ensureManagedDir(managedDir: string): void {
|
|
79
|
+
mkdirSync(managedDir, { recursive: true });
|
|
80
|
+
const pkgPath = path.join(managedDir, "package.json");
|
|
81
|
+
if (!existsSync(pkgPath)) {
|
|
82
|
+
writeFileSync(
|
|
83
|
+
pkgPath,
|
|
84
|
+
JSON.stringify({ name: "pi-dashboard-managed", private: true, type: "module" }, null, 2),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the npm invocation used for bootstrap installs.
|
|
91
|
+
*
|
|
92
|
+
* Order:
|
|
93
|
+
* 1. Explicit `npmArgv` override (Electron bundled-node case).
|
|
94
|
+
* 2. `ToolRegistry.resolve("npm")`.
|
|
95
|
+
* 3. Plain `npm` (Unix) or `npm.cmd` (Windows) on PATH.
|
|
96
|
+
*
|
|
97
|
+
* Returns the argv list that will have `install <packages...>` appended.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveNpmArgv(
|
|
100
|
+
opts: Pick<BootstrapInstallOptions, "npmArgv" | "registry">,
|
|
101
|
+
): string[] {
|
|
102
|
+
if (opts.npmArgv && opts.npmArgv.length > 0) return [...opts.npmArgv];
|
|
103
|
+
|
|
104
|
+
const registry = opts.registry ?? getDefaultRegistry();
|
|
105
|
+
if (registry.has("npm")) {
|
|
106
|
+
const res = registry.resolve("npm");
|
|
107
|
+
if (res.ok && res.path) return [res.path];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Last resort: rely on PATH. On Windows the .cmd shim is required
|
|
111
|
+
// because spawn doesn't auto-append extensions.
|
|
112
|
+
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm"; // platform-branch-ok
|
|
113
|
+
return [npmBin];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Internal: spawn npm with a given argv + packages; stream progress. */
|
|
117
|
+
function runNpmOnce(
|
|
118
|
+
argvBase: string[],
|
|
119
|
+
packages: string[],
|
|
120
|
+
cwd: string,
|
|
121
|
+
env: NodeJS.ProcessEnv,
|
|
122
|
+
onOutput?: (line: string) => void,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const [cmd, ...baseArgs] = argvBase;
|
|
126
|
+
if (!cmd) {
|
|
127
|
+
reject(new Error("resolveNpmArgv returned an empty argv"));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const args = [...baseArgs, "install", ...packages];
|
|
131
|
+
|
|
132
|
+
const child = cpSpawn(cmd, args, {
|
|
133
|
+
cwd,
|
|
134
|
+
env,
|
|
135
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
136
|
+
timeout: 300_000,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let tail = "";
|
|
140
|
+
|
|
141
|
+
const handleData = (data: Buffer): void => {
|
|
142
|
+
const text = data.toString();
|
|
143
|
+
tail += text;
|
|
144
|
+
if (tail.length > 4096) tail = tail.slice(-4096);
|
|
145
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
146
|
+
const last = lines[lines.length - 1];
|
|
147
|
+
if (last && onOutput) onOutput(last.trim().substring(0, 120));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
child.stdout?.on("data", handleData);
|
|
151
|
+
child.stderr?.on("data", handleData);
|
|
152
|
+
|
|
153
|
+
child.on("error", (err) => reject(new Error(err.message)));
|
|
154
|
+
child.on("close", (code) => {
|
|
155
|
+
if (code !== 0) {
|
|
156
|
+
reject(new Error(tail.slice(-500) || `npm install exited with code ${code}`));
|
|
157
|
+
} else {
|
|
158
|
+
resolve();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Install the given packages into the managed directory.
|
|
166
|
+
*
|
|
167
|
+
* Per-package progress is reported via `progress`. Installation is
|
|
168
|
+
* sequential (not concurrent) so a failure stops the chain — matching
|
|
169
|
+
* the behavior of the Electron wizard today. The return value reports
|
|
170
|
+
* which packages completed successfully before any failure.
|
|
171
|
+
*/
|
|
172
|
+
export async function bootstrapInstall(
|
|
173
|
+
opts: BootstrapInstallOptions,
|
|
174
|
+
): Promise<BootstrapInstallResult> {
|
|
175
|
+
const managedDir = opts.managedDir ?? getManagedDir();
|
|
176
|
+
ensureManagedDir(managedDir);
|
|
177
|
+
|
|
178
|
+
const argvBase = resolveNpmArgv(opts);
|
|
179
|
+
const env = { ...process.env, ...(opts.env ?? {}) };
|
|
180
|
+
|
|
181
|
+
const installed: string[] = [];
|
|
182
|
+
for (const pkg of opts.packages) {
|
|
183
|
+
const step = pkg.split("/").pop() || pkg;
|
|
184
|
+
opts.progress?.({ step, status: "running" });
|
|
185
|
+
try {
|
|
186
|
+
await runNpmOnce(argvBase, [pkg], managedDir, env, (output) => {
|
|
187
|
+
opts.progress?.({ step, status: "running", output });
|
|
188
|
+
});
|
|
189
|
+
opts.progress?.({ step, status: "done" });
|
|
190
|
+
installed.push(pkg);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
193
|
+
opts.progress?.({ step, status: "error", error: message });
|
|
194
|
+
return { ok: false, error: message, installed, managedDir };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { ok: true, installed, managedDir };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Convenience wrapper: install pi, openspec, tsx into the default
|
|
203
|
+
* managed directory. Used by the CLI degraded-mode first-run path.
|
|
204
|
+
*/
|
|
205
|
+
export async function bootstrapInstallDefaults(
|
|
206
|
+
progress?: ProgressCallback,
|
|
207
|
+
): Promise<BootstrapInstallResult> {
|
|
208
|
+
return bootstrapInstall({
|
|
209
|
+
packages: ["@mariozechner/pi-coding-agent", "@fission-ai/openspec", "tsx"],
|
|
210
|
+
progress,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -10,29 +10,92 @@
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import os from "node:os";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Returns null if:
|
|
19
|
-
* - Directory not found
|
|
20
|
-
* - No package.json in the directory
|
|
21
|
-
* - Path is under /tmp/.mount_* (unstable AppImage mount)
|
|
16
|
+
* Check that a candidate path is a valid, stable extension directory.
|
|
17
|
+
* Returns true when the directory exists, contains a package.json, and
|
|
18
|
+
* is NOT under /tmp/.mount_* (unstable AppImage mount).
|
|
22
19
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!fs.existsSync(
|
|
26
|
-
|
|
20
|
+
function isValidExtensionPath(candidate: string): boolean {
|
|
21
|
+
if (!fs.existsSync(candidate)) return false;
|
|
22
|
+
if (!fs.existsSync(path.join(candidate, "package.json"))) return false;
|
|
23
|
+
if (candidate.includes("/tmp/.mount_")) {
|
|
24
|
+
console.warn(
|
|
25
|
+
"[dashboard] AppImage detected — extension path is temporary, skipping registration:",
|
|
26
|
+
candidate,
|
|
27
|
+
);
|
|
28
|
+
return false;
|
|
27
29
|
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Optional dependency injection for `findBundledExtension`. Tests pass
|
|
35
|
+
* `{ resolvePackage: () => null }` to disable the node-resolver fallback.
|
|
36
|
+
*/
|
|
37
|
+
export interface FindExtensionDeps {
|
|
38
|
+
/**
|
|
39
|
+
* Resolve `@blackbelt-technology/pi-dashboard-extension/package.json`
|
|
40
|
+
* via Node's module resolver. Return the absolute package.json path
|
|
41
|
+
* or null. Defaults to `createRequire(import.meta.url).resolve(...)`.
|
|
42
|
+
*/
|
|
43
|
+
resolvePackage?: () => string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function defaultResolvePackage(): string | null {
|
|
47
|
+
try {
|
|
48
|
+
const req = createRequire(import.meta.url);
|
|
49
|
+
return req.resolve("@blackbelt-technology/pi-dashboard-extension/package.json");
|
|
50
|
+
} catch {
|
|
32
51
|
return null;
|
|
33
52
|
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Find the bundled extension directory.
|
|
57
|
+
*
|
|
58
|
+
* Resolution order:
|
|
59
|
+
* 1. Monorepo layout: `<baseDir>/packages/extension/`.
|
|
60
|
+
* 2. Node module resolution: `@blackbelt-technology/pi-dashboard-extension/package.json`
|
|
61
|
+
* via `require.resolve` from this module. Works in ANY install layout
|
|
62
|
+
* (flat `node_modules/`, scoped, nested, pnpm, npm-g). This is the
|
|
63
|
+
* canonical identity-based lookup and the only reliable strategy
|
|
64
|
+
* when pi-dashboard is installed via `npm i -g`.
|
|
65
|
+
*
|
|
66
|
+
* Returns null if both strategies fail, the resolved directory doesn't
|
|
67
|
+
* have a package.json, or the path is under /tmp/.mount_* (AppImage).
|
|
68
|
+
*
|
|
69
|
+
* See change: unified-bootstrap-install.
|
|
70
|
+
*/
|
|
71
|
+
export function findBundledExtension(
|
|
72
|
+
baseDir: string,
|
|
73
|
+
deps: FindExtensionDeps = {},
|
|
74
|
+
): string | null {
|
|
75
|
+
// Strategy 1: monorepo sibling layout.
|
|
76
|
+
const monorepoCandidate = path.resolve(baseDir, "packages", "extension");
|
|
77
|
+
if (isValidExtensionPath(monorepoCandidate)) return monorepoCandidate;
|
|
78
|
+
|
|
79
|
+
// Strategy 2: Node module resolver. This works for the `npm i -g
|
|
80
|
+
// pi-dashboard` layout where the extension is shipped as a runtime dep
|
|
81
|
+
// of pi-dashboard-server.
|
|
82
|
+
const resolver = deps.resolvePackage ?? defaultResolvePackage;
|
|
83
|
+
const extPkgJson = resolver();
|
|
84
|
+
if (extPkgJson) {
|
|
85
|
+
const extDir = path.dirname(extPkgJson);
|
|
86
|
+
if (isValidExtensionPath(extDir)) return extDir;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
34
91
|
|
|
35
|
-
|
|
92
|
+
/** Optional overrides for testing / multi-HOME scenarios. */
|
|
93
|
+
export interface BridgeRegisterOptions {
|
|
94
|
+
/**
|
|
95
|
+
* Override the HOME used to locate settings.json. When omitted,
|
|
96
|
+
* falls back to `$HOME || $USERPROFILE || os.homedir()` (existing behavior).
|
|
97
|
+
*/
|
|
98
|
+
homedir?: string;
|
|
36
99
|
}
|
|
37
100
|
|
|
38
101
|
/**
|
|
@@ -44,12 +107,16 @@ export function findBundledExtension(baseDir: string): string | null {
|
|
|
44
107
|
*
|
|
45
108
|
* No-op if the path is already registered.
|
|
46
109
|
*/
|
|
47
|
-
export function registerBridgeExtension(
|
|
110
|
+
export function registerBridgeExtension(
|
|
111
|
+
extensionPath: string,
|
|
112
|
+
opts: BridgeRegisterOptions = {},
|
|
113
|
+
): void {
|
|
48
114
|
// Compute at call time so tests can override HOME
|
|
49
|
-
const
|
|
50
|
-
process.env.HOME
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
const home = opts.homedir
|
|
116
|
+
?? process.env.HOME
|
|
117
|
+
?? process.env.USERPROFILE
|
|
118
|
+
?? os.homedir();
|
|
119
|
+
const settingsPath = path.join(home, ".pi", "agent", "settings.json");
|
|
53
120
|
const settingsDir = path.dirname(settingsPath);
|
|
54
121
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
55
122
|
|