@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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest globalSetup: tripwire + directory bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Wired via `test.globalSetup` in each package's `vitest.config.ts`. Runs ONCE
|
|
5
|
+
* at vitest boot, before any test file is loaded.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* 1. Tripwire — throws if `process.env.HOME` still points at the developer's
|
|
9
|
+
* real user home (meaning the root `npm test` script wasn't used and HOME
|
|
10
|
+
* wasn't overridden). Aborts the entire run before any destructive code
|
|
11
|
+
* can touch real ~/.pi/.
|
|
12
|
+
* 2. Pre-create `<HOME>/.pi/agent/sessions/` and `<HOME>/.pi/dashboard/` so
|
|
13
|
+
* production code that reads those paths finds empty but well-formed
|
|
14
|
+
* directories.
|
|
15
|
+
*
|
|
16
|
+
* Why globalSetup (not setupFiles):
|
|
17
|
+
* setupFiles' `beforeAll` can run AFTER a test file's top-level imports
|
|
18
|
+
* execute destructive module-level code. globalSetup runs strictly before
|
|
19
|
+
* ANY test file is loaded. Combined with the `npm test` process-level HOME
|
|
20
|
+
* override, there is zero window in which code can see real HOME.
|
|
21
|
+
*
|
|
22
|
+
* The process-level HOME override in package.json is the primary isolation
|
|
23
|
+
* layer; this module is the second-line tripwire that catches regressions.
|
|
24
|
+
*/
|
|
25
|
+
import { mkdirSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
|
|
29
|
+
/** Vitest globalSetup default export. Returns a teardown function. */
|
|
30
|
+
export default function setup() {
|
|
31
|
+
const currentHome = process.env.HOME ?? "";
|
|
32
|
+
const realHome = os.userInfo().homedir;
|
|
33
|
+
|
|
34
|
+
if (!currentHome) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"[test-isolation] process.env.HOME is empty. " +
|
|
37
|
+
"Run tests via `npm test` (which sets HOME to a tmp dir) " +
|
|
38
|
+
"or prefix manually: `HOME=$(mktemp -d) npx vitest run`.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (currentHome === realHome) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`[test-isolation] process.env.HOME (${currentHome}) equals the real user home ` +
|
|
45
|
+
`(${realHome}). This would let tests read and mutate ~/.pi/, potentially killing ` +
|
|
46
|
+
`live pi sessions. Run tests via \`npm test\` — it sets HOME to an ephemeral tmp dir. ` +
|
|
47
|
+
`If you invoked vitest directly, prefix with \`HOME=$(mktemp -d)\`.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!currentHome.startsWith(os.tmpdir())) {
|
|
52
|
+
// Not strictly fatal — developer may have pointed HOME at a custom scratch dir —
|
|
53
|
+
// but warn loudly because it's unusual.
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.warn(
|
|
56
|
+
`[test-isolation] HOME (${currentHome}) is not under os.tmpdir() (${os.tmpdir()}). ` +
|
|
57
|
+
`Tests will still run but this layout is unusual.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Pre-create expected .pi subdirectories so code that reads them finds empty dirs.
|
|
62
|
+
mkdirSync(join(currentHome, ".pi", "agent", "sessions"), { recursive: true });
|
|
63
|
+
mkdirSync(join(currentHome, ".pi", "dashboard"), { recursive: true });
|
|
64
|
+
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.log(`[test-isolation] HOME=${currentHome} (real=${realHome})`);
|
|
67
|
+
|
|
68
|
+
// Teardown: nothing to do. The tmp HOME was created by the npm script's
|
|
69
|
+
// `$(mktemp -d)`; leaving the dir on disk is fine (OS cleans tmpdir).
|
|
70
|
+
// Tests that need per-file isolation continue to use their own mkdtemp.
|
|
71
|
+
return () => {
|
|
72
|
+
/* no-op */
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registered tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* Each definition declares an ordered strategy chain. Individual
|
|
5
|
+
* strategies are responsible for validating their own resolved paths
|
|
6
|
+
* (they use the injected `exists` from StrategyDeps), so tests can
|
|
7
|
+
* inject fakes without triggering real `fs.existsSync` lookups.
|
|
8
|
+
*
|
|
9
|
+
* See change: consolidate-tool-resolution.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import type { ToolDefinition, Source } from "./types.js";
|
|
15
|
+
import type { ToolRegistry } from "./registry.js";
|
|
16
|
+
import {
|
|
17
|
+
type StrategyDeps,
|
|
18
|
+
bareImportStrategy,
|
|
19
|
+
managedBinStrategy,
|
|
20
|
+
managedModuleStrategy,
|
|
21
|
+
npmGlobalStrategy,
|
|
22
|
+
overrideStrategy,
|
|
23
|
+
whereStrategy,
|
|
24
|
+
} from "./strategies.js";
|
|
25
|
+
|
|
26
|
+
// ── Classifier ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/** Classifier: strategies → Source. Shared across binary and module tools. */
|
|
29
|
+
function classify(strategyName: string): Source {
|
|
30
|
+
if (strategyName === "override") return "override";
|
|
31
|
+
if (strategyName === "managed") return "managed";
|
|
32
|
+
if (strategyName === "npm-global") return "npm-global";
|
|
33
|
+
if (strategyName === "bare-import") return "bare-import";
|
|
34
|
+
// `where` and anything else — resolved via PATH — classifies as system.
|
|
35
|
+
return "system";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Binary definitions ──────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function binaryDef(binaryName: string, deps?: StrategyDeps): ToolDefinition {
|
|
41
|
+
return {
|
|
42
|
+
name: binaryName,
|
|
43
|
+
kind: "binary",
|
|
44
|
+
strategies: [
|
|
45
|
+
overrideStrategy(binaryName, deps),
|
|
46
|
+
managedBinStrategy(binaryName, deps),
|
|
47
|
+
whereStrategy(binaryName, deps),
|
|
48
|
+
],
|
|
49
|
+
classify,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Module definitions ──────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/** Sibling probe for an aliased package name (pi: `@mariozechner/*` + `@oh-my-pi/*`). */
|
|
56
|
+
function moduleDefWithAliases(
|
|
57
|
+
canonicalName: string,
|
|
58
|
+
pkgNames: readonly string[],
|
|
59
|
+
entry: string,
|
|
60
|
+
deps?: StrategyDeps,
|
|
61
|
+
): ToolDefinition {
|
|
62
|
+
const strategies = [overrideStrategy(canonicalName, deps)];
|
|
63
|
+
for (const pkg of pkgNames) strategies.push(bareImportStrategy(pkg));
|
|
64
|
+
for (const pkg of pkgNames) strategies.push(managedModuleStrategy(pkg, entry, deps));
|
|
65
|
+
for (const pkg of pkgNames) strategies.push(npmGlobalStrategy(pkg, entry, deps));
|
|
66
|
+
return { name: canonicalName, kind: "module", strategies, classify };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Registration ─────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
// Tools intentionally NOT registered:
|
|
72
|
+
// - `tsx` — a TypeScript *loader* used via `node --import tsx`,
|
|
73
|
+
// not a tool the dashboard spawns. When pi is installed, pi ships
|
|
74
|
+
// jiti which the server prefers; otherwise tsx is co-installed
|
|
75
|
+
// as a dev dep of the server package.
|
|
76
|
+
// - `pi-dashboard` — that's the package this code is part of.
|
|
77
|
+
// "Is it installed" is a bootstrap concern handled directly in
|
|
78
|
+
// `packages/electron/src/lib/dependency-detector.ts`.
|
|
79
|
+
// See change: consolidate-tool-resolution (follow-up).
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Shared `toArgv` for Node-script executors (pi, openspec, npm).
|
|
83
|
+
*
|
|
84
|
+
* On Windows + `.js` resolved path → prepend node.exe to bypass the
|
|
85
|
+
* `.cmd` shim entirely (no cmd.exe in the spawn chain → no console
|
|
86
|
+
* flash). Elsewhere → direct invocation.
|
|
87
|
+
*
|
|
88
|
+
* This is the heart of the "no cmd flash" story: every CLI that ships
|
|
89
|
+
* as `.cmd` on Windows and is actually a Node script should be
|
|
90
|
+
* registered with this `toArgv` so the spawn becomes
|
|
91
|
+
* `node.exe <script.js>` (pure console-subsystem inherit, no new
|
|
92
|
+
* window ever).
|
|
93
|
+
*/
|
|
94
|
+
const nodeScriptToArgv: ToolDefinition["toArgv"] = (resolvedPath, { platform, registry }) => {
|
|
95
|
+
if (platform === "win32" && /\.js$/i.test(resolvedPath)) {
|
|
96
|
+
const node = registry.resolve("node");
|
|
97
|
+
if (node.ok && node.path) return [node.path, resolvedPath];
|
|
98
|
+
}
|
|
99
|
+
return [resolvedPath];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Executor definition for `pi` — ONE tool, OS dispatch inside.
|
|
104
|
+
*
|
|
105
|
+
* On Windows, the strategy chain finds pi-coding-agent's `dist/cli.js`
|
|
106
|
+
* (managed → bare-import → npm-global), and `toArgv` wraps it with
|
|
107
|
+
* `node.exe` to produce `[node.exe, cli.js]`. Falls back to `pi.cmd`
|
|
108
|
+
* on PATH when the cli.js is nowhere to be found.
|
|
109
|
+
*
|
|
110
|
+
* On Unix, the chain finds `pi` on PATH; argv = [pi].
|
|
111
|
+
*/
|
|
112
|
+
function piExecutorDef(deps?: StrategyDeps): ToolDefinition {
|
|
113
|
+
const piPkgAliases = ["@mariozechner/pi-coding-agent", "@oh-my-pi/pi-coding-agent"];
|
|
114
|
+
const cliEntry = path.join("dist", "cli.js");
|
|
115
|
+
|
|
116
|
+
const winStrategies = [
|
|
117
|
+
overrideStrategy("pi", deps),
|
|
118
|
+
...piPkgAliases.map((pkg) => bareImportCliStrategy(pkg, cliEntry, deps)),
|
|
119
|
+
...piPkgAliases.map((pkg) => managedModuleStrategy(pkg, cliEntry, deps)),
|
|
120
|
+
...piPkgAliases.map((pkg) => npmGlobalStrategy(pkg, cliEntry, deps)),
|
|
121
|
+
managedBinStrategy("pi", deps),
|
|
122
|
+
whereStrategy("pi", deps),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const unixStrategies = [
|
|
126
|
+
overrideStrategy("pi", deps),
|
|
127
|
+
managedBinStrategy("pi", deps),
|
|
128
|
+
whereStrategy("pi", deps),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
name: "pi",
|
|
133
|
+
kind: "executor",
|
|
134
|
+
strategies: unixStrategies,
|
|
135
|
+
platformStrategies: { win32: winStrategies },
|
|
136
|
+
toArgv: nodeScriptToArgv,
|
|
137
|
+
classify,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Executor definition for `openspec`.
|
|
143
|
+
*
|
|
144
|
+
* On Windows: finds `@fission-ai/openspec/bin/openspec.js` via managed
|
|
145
|
+
* → bare-import → npm-global. `toArgv` wraps with node.exe.
|
|
146
|
+
* On Unix: finds `openspec` binary on PATH.
|
|
147
|
+
*/
|
|
148
|
+
function openspecExecutorDef(deps?: StrategyDeps): ToolDefinition {
|
|
149
|
+
const pkgName = "@fission-ai/openspec";
|
|
150
|
+
const cliEntry = path.join("bin", "openspec.js");
|
|
151
|
+
|
|
152
|
+
const winStrategies = [
|
|
153
|
+
overrideStrategy("openspec", deps),
|
|
154
|
+
bareImportCliStrategy(pkgName, cliEntry),
|
|
155
|
+
managedModuleStrategy(pkgName, cliEntry, deps),
|
|
156
|
+
npmGlobalStrategy(pkgName, cliEntry, deps),
|
|
157
|
+
managedBinStrategy("openspec", deps),
|
|
158
|
+
whereStrategy("openspec", deps),
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const unixStrategies = [
|
|
162
|
+
overrideStrategy("openspec", deps),
|
|
163
|
+
managedBinStrategy("openspec", deps),
|
|
164
|
+
whereStrategy("openspec", deps),
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
name: "openspec",
|
|
169
|
+
kind: "executor",
|
|
170
|
+
strategies: unixStrategies,
|
|
171
|
+
platformStrategies: { win32: winStrategies },
|
|
172
|
+
toArgv: nodeScriptToArgv,
|
|
173
|
+
classify,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Executor definition for `npm`.
|
|
179
|
+
*
|
|
180
|
+
* npm is bundled with Node itself, not a standalone npm install. On
|
|
181
|
+
* Windows: find `<node-dir>/node_modules/npm/bin/npm-cli.js` by
|
|
182
|
+
* looking beside the resolved `node.exe`. Fallback: PATH lookup
|
|
183
|
+
* (which returns npm.cmd).
|
|
184
|
+
* On Unix: find `npm` on PATH.
|
|
185
|
+
*
|
|
186
|
+
* Motivation: npm.cmd internally runs `node.exe npm-cli.js`, and the
|
|
187
|
+
* inner node.exe can allocate a new console (Node issue #21825). By
|
|
188
|
+
* resolving to npm-cli.js directly + spawning via node.exe ourselves,
|
|
189
|
+
* we bypass cmd.exe + npm.cmd entirely.
|
|
190
|
+
*/
|
|
191
|
+
function npmExecutorDef(deps?: StrategyDeps): ToolDefinition {
|
|
192
|
+
const npmRelativeToNode = path.join("node_modules", "npm", "bin", "npm-cli.js");
|
|
193
|
+
|
|
194
|
+
// Custom strategy: find npm-cli.js beside the resolved node.exe.
|
|
195
|
+
// We can't pre-compute the node path at definition time (the registry
|
|
196
|
+
// isn't fully constructed yet), so the strategy resolves node
|
|
197
|
+
// lazily at run time via the global registry hook.
|
|
198
|
+
const npmCliBesideNodeStrategy = {
|
|
199
|
+
name: "managed", // classified as managed because it ships with node
|
|
200
|
+
run(): { ok: true; path: string } | { ok: false; reason: string } {
|
|
201
|
+
// Find node.exe from process.execPath or environment.
|
|
202
|
+
const nodeExe = process.execPath;
|
|
203
|
+
if (!nodeExe) return { ok: false, reason: "process.execPath unset" };
|
|
204
|
+
const nodeDir = path.dirname(nodeExe);
|
|
205
|
+
const candidate = path.join(nodeDir, npmRelativeToNode);
|
|
206
|
+
try {
|
|
207
|
+
if (existsSync(candidate)) return { ok: true, path: candidate };
|
|
208
|
+
return { ok: false, reason: `missing: ${candidate}` };
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const winStrategies = [
|
|
216
|
+
overrideStrategy("npm", deps),
|
|
217
|
+
npmCliBesideNodeStrategy,
|
|
218
|
+
whereStrategy("npm", deps),
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const unixStrategies = [
|
|
222
|
+
overrideStrategy("npm", deps),
|
|
223
|
+
whereStrategy("npm", deps),
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
name: "npm",
|
|
228
|
+
kind: "executor",
|
|
229
|
+
strategies: unixStrategies,
|
|
230
|
+
platformStrategies: { win32: winStrategies },
|
|
231
|
+
toArgv: nodeScriptToArgv,
|
|
232
|
+
classify,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Helper: bare-import strategy that, on success, transforms the
|
|
238
|
+
* resolved `package.json` into the sibling `<entry>` path. Used by
|
|
239
|
+
* the pi executor to find pi-coding-agent's cli.js via the same
|
|
240
|
+
* module-resolution algorithm as `import()`.
|
|
241
|
+
*/
|
|
242
|
+
function bareImportCliStrategy(
|
|
243
|
+
pkgName: string,
|
|
244
|
+
entryRelative: string,
|
|
245
|
+
deps?: StrategyDeps,
|
|
246
|
+
) {
|
|
247
|
+
// Default uses the real module resolver anchored to this file;
|
|
248
|
+
// tests inject a fake via deps.resolveModule.
|
|
249
|
+
const resolveModule: NonNullable<StrategyDeps["resolveModule"]> =
|
|
250
|
+
deps?.resolveModule
|
|
251
|
+
?? ((id, from) => {
|
|
252
|
+
try {
|
|
253
|
+
return createRequire(from).resolve(id);
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
name: "bare-import",
|
|
260
|
+
run(): { ok: true; path: string } | { ok: false; reason: string } {
|
|
261
|
+
const pkgJson = resolveModule(`${pkgName}/package.json`, import.meta.url);
|
|
262
|
+
if (!pkgJson) {
|
|
263
|
+
return { ok: false, reason: `cannot resolve ${pkgName}/package.json` };
|
|
264
|
+
}
|
|
265
|
+
const entry = path.join(path.dirname(pkgJson), entryRelative);
|
|
266
|
+
return { ok: true, path: entry };
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Register the standard set of dashboard tools. Idempotent — callers
|
|
273
|
+
* may re-register to supply custom strategy deps (e.g. tests).
|
|
274
|
+
*/
|
|
275
|
+
export function registerDefaultTools(registry: ToolRegistry, deps?: StrategyDeps): void {
|
|
276
|
+
// Executor-kind tools — Node scripts shipped as .cmd shims on
|
|
277
|
+
// Windows. Each registers as [node.exe, <script>.js] to bypass
|
|
278
|
+
// cmd.exe and the console-flash chain (Node issue #21825).
|
|
279
|
+
registry.register(piExecutorDef(deps));
|
|
280
|
+
registry.register(openspecExecutorDef(deps));
|
|
281
|
+
registry.register(npmExecutorDef(deps));
|
|
282
|
+
|
|
283
|
+
// Native binaries — no interpreter needed.
|
|
284
|
+
registry.register(binaryDef("node", deps));
|
|
285
|
+
registry.register(binaryDef("git", deps));
|
|
286
|
+
registry.register(binaryDef("zrok", deps));
|
|
287
|
+
|
|
288
|
+
// Platform-conditional process-inspection utilities. These are only
|
|
289
|
+
// called by `packages/shared/src/platform/process.ts` on their native
|
|
290
|
+
// platform — registering the non-native tools would surface as red
|
|
291
|
+
// "not found" rows in the Settings → Tools UI even though the code
|
|
292
|
+
// never calls them there.
|
|
293
|
+
//
|
|
294
|
+
// Honours the registry's `platform` so tests that inject `platform:
|
|
295
|
+
// "win32"` from a Linux host still exercise the Windows tool set.
|
|
296
|
+
//
|
|
297
|
+
// Windows system utilities used by the bridge's process scanner.
|
|
298
|
+
// Registered so callers resolve to full `.exe` paths (e.g.
|
|
299
|
+
// `C:\Windows\System32\wbem\wmic.exe`) and spawn directly — no
|
|
300
|
+
// PATHEXT resolution, no cmd.exe wrapping, windowsHide:true honored
|
|
301
|
+
// all the way down. See change: consolidate-windows-spawn-and-platform-handlers.
|
|
302
|
+
if (registry.getPlatform() === "win32") {
|
|
303
|
+
registry.register(binaryDef("wmic", deps));
|
|
304
|
+
registry.register(binaryDef("powershell", deps));
|
|
305
|
+
registry.register(binaryDef("tasklist", deps));
|
|
306
|
+
registry.register(binaryDef("taskkill", deps));
|
|
307
|
+
} else {
|
|
308
|
+
// POSIX process-inspection utilities. Used by `isProcessRunning`,
|
|
309
|
+
// `findPidByMarker`, `isProcessLikePi` in platform/process.ts.
|
|
310
|
+
registry.register(binaryDef("ps", deps));
|
|
311
|
+
registry.register(binaryDef("pgrep", deps));
|
|
312
|
+
}
|
|
313
|
+
// Windows Terminal — optional, override + where only (not part of
|
|
314
|
+
// managed install, not on Unix).
|
|
315
|
+
registry.register({
|
|
316
|
+
name: "wt",
|
|
317
|
+
kind: "binary",
|
|
318
|
+
strategies: [
|
|
319
|
+
overrideStrategy("wt", deps),
|
|
320
|
+
whereStrategy("wt", deps),
|
|
321
|
+
],
|
|
322
|
+
classify,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Node module entry for pi-coding-agent — used by DefaultPackageManager
|
|
326
|
+
// to IMPORT pi as a library (not spawn it as a process). Distinct from
|
|
327
|
+
// the `pi` executor above.
|
|
328
|
+
registry.register(
|
|
329
|
+
moduleDefWithAliases(
|
|
330
|
+
"pi-coding-agent",
|
|
331
|
+
["@mariozechner/pi-coding-agent", "@oh-my-pi/pi-coding-agent"],
|
|
332
|
+
path.join("dist", "index.js"),
|
|
333
|
+
deps,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Handy re-exports for callers that want raw definitions for testing. */
|
|
339
|
+
export const _internals = {
|
|
340
|
+
binaryDef,
|
|
341
|
+
moduleDefWithAliases,
|
|
342
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry — single-source resolver for every external binary/module
|
|
3
|
+
* the dashboard depends on. See change: consolidate-tool-resolution.
|
|
4
|
+
*
|
|
5
|
+
* Quick start:
|
|
6
|
+
*
|
|
7
|
+
* import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry";
|
|
8
|
+
* const r = getDefaultRegistry().resolve("pi");
|
|
9
|
+
* if (r.ok) spawn(r.path!, args);
|
|
10
|
+
*/
|
|
11
|
+
export * from "./types.js";
|
|
12
|
+
export { OverridesStore, defaultOverridesPath } from "./overrides.js";
|
|
13
|
+
export { ToolRegistry } from "./registry.js";
|
|
14
|
+
export { registerDefaultTools } from "./definitions.js";
|
|
15
|
+
export * from "./strategies.js";
|
|
16
|
+
|
|
17
|
+
import { ToolRegistry } from "./registry.js";
|
|
18
|
+
import { registerDefaultTools } from "./definitions.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Lazily-constructed process-wide registry. Most callers should use this
|
|
22
|
+
* instead of constructing their own. Tests should pass a fresh
|
|
23
|
+
* `new ToolRegistry({...})` with injected deps.
|
|
24
|
+
*
|
|
25
|
+
* The registry is also published on `globalThis` under a symbol so that
|
|
26
|
+
* `platform/runner.ts` can pick it up synchronously without a module
|
|
27
|
+
* import (which would create a load-order cycle through `platform/npm.ts`).
|
|
28
|
+
*/
|
|
29
|
+
const GLOBAL_KEY = Symbol.for("pi-dashboard.tool-registry");
|
|
30
|
+
type GlobalSlot = { [GLOBAL_KEY]?: ToolRegistry };
|
|
31
|
+
|
|
32
|
+
let defaultRegistry: ToolRegistry | null = null;
|
|
33
|
+
export function getDefaultRegistry(): ToolRegistry {
|
|
34
|
+
if (!defaultRegistry) {
|
|
35
|
+
defaultRegistry = new ToolRegistry();
|
|
36
|
+
registerDefaultTools(defaultRegistry);
|
|
37
|
+
(globalThis as unknown as GlobalSlot)[GLOBAL_KEY] = defaultRegistry;
|
|
38
|
+
}
|
|
39
|
+
return defaultRegistry;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Global accessor for consumers that cannot import this module at the
|
|
44
|
+
* top level (i.e. `platform/runner.ts`, which is part of a load-order
|
|
45
|
+
* cycle). Returns `null` if `getDefaultRegistry()` hasn't been called
|
|
46
|
+
* yet anywhere in the process.
|
|
47
|
+
*/
|
|
48
|
+
export function peekGlobalRegistry(): ToolRegistry | null {
|
|
49
|
+
return (globalThis as unknown as GlobalSlot)[GLOBAL_KEY] ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Test-only: drop the process-wide registry so the next call rebuilds. */
|
|
53
|
+
export function _resetDefaultRegistry(): void {
|
|
54
|
+
defaultRegistry = null;
|
|
55
|
+
(globalThis as unknown as GlobalSlot)[GLOBAL_KEY] = undefined;
|
|
56
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent per-tool path overrides at `~/.pi/dashboard/tool-overrides.json`.
|
|
3
|
+
*
|
|
4
|
+
* Schema:
|
|
5
|
+
* { "version": 1, "overrides": { "<toolName>": { "path": "<abs>" } } }
|
|
6
|
+
*
|
|
7
|
+
* Design notes (see change: consolidate-tool-resolution, design §5):
|
|
8
|
+
* - Separate from `config.json` — path overrides are machine-local and
|
|
9
|
+
* should NOT follow a user's dotfiles across machines.
|
|
10
|
+
* - Atomic write via the same tmp+rename pattern used by
|
|
11
|
+
* `server/src/json-store.ts` (duplicated here to keep `shared`
|
|
12
|
+
* self-contained; the two live in different packages).
|
|
13
|
+
* - Malformed files are treated as empty. No throw, no crash.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
|
|
19
|
+
/** Path to the overrides file. Exposed for tests and the settings UI. */
|
|
20
|
+
export function defaultOverridesPath(): string {
|
|
21
|
+
return path.join(os.homedir(), ".pi", "dashboard", "tool-overrides.json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Internal shape persisted to disk. `version` lets us evolve later. */
|
|
25
|
+
interface OverridesFile {
|
|
26
|
+
version: 1;
|
|
27
|
+
overrides: Record<string, { path: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OverridesStoreDeps {
|
|
31
|
+
filePath?: string;
|
|
32
|
+
/** Logger hook (defaults to console.warn). Tests inject a sink. */
|
|
33
|
+
warn?(message: string): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read-through + write-through in-memory store. One instance per registry.
|
|
38
|
+
* Keeps the disk read lazy — the file is only touched on first access.
|
|
39
|
+
*/
|
|
40
|
+
export class OverridesStore {
|
|
41
|
+
private readonly filePath: string;
|
|
42
|
+
private readonly warn: (message: string) => void;
|
|
43
|
+
private cache: Record<string, string> | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(deps: OverridesStoreDeps = {}) {
|
|
46
|
+
this.filePath = deps.filePath ?? defaultOverridesPath();
|
|
47
|
+
this.warn = deps.warn ?? ((m) => console.warn(`[tool-registry] ${m}`));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Snapshot of current overrides. Lazy-loads from disk on first call. */
|
|
51
|
+
list(): Readonly<Record<string, string>> {
|
|
52
|
+
if (this.cache === null) this.cache = this.load();
|
|
53
|
+
return this.cache;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Set one override + persist. */
|
|
57
|
+
set(name: string, overridePath: string): void {
|
|
58
|
+
const current = this.cache ?? this.load();
|
|
59
|
+
current[name] = overridePath;
|
|
60
|
+
this.cache = current;
|
|
61
|
+
this.persist(current);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Remove one override + persist. No-op if absent. */
|
|
65
|
+
clear(name: string): void {
|
|
66
|
+
const current = this.cache ?? this.load();
|
|
67
|
+
if (!(name in current)) return;
|
|
68
|
+
delete current[name];
|
|
69
|
+
this.cache = current;
|
|
70
|
+
this.persist(current);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drop the in-memory cache; next `list()` re-reads the file. */
|
|
74
|
+
invalidate(): void {
|
|
75
|
+
this.cache = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Internal ─────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
private load(): Record<string, string> {
|
|
81
|
+
try {
|
|
82
|
+
if (!fs.existsSync(this.filePath)) return {};
|
|
83
|
+
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
84
|
+
if (!raw.trim()) return {};
|
|
85
|
+
const parsed = JSON.parse(raw) as Partial<OverridesFile>;
|
|
86
|
+
if (!parsed || typeof parsed !== "object" || !parsed.overrides) {
|
|
87
|
+
this.warn(`malformed overrides file at ${this.filePath}; ignoring`);
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
const out: Record<string, string> = {};
|
|
91
|
+
for (const [name, entry] of Object.entries(parsed.overrides)) {
|
|
92
|
+
if (entry && typeof entry === "object" && typeof (entry as { path?: unknown }).path === "string") {
|
|
93
|
+
out[name] = (entry as { path: string }).path;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
this.warn(
|
|
99
|
+
`failed to read overrides file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
100
|
+
);
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private persist(overrides: Record<string, string>): void {
|
|
106
|
+
const dir = path.dirname(this.filePath);
|
|
107
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
108
|
+
const data: OverridesFile = {
|
|
109
|
+
version: 1,
|
|
110
|
+
overrides: Object.fromEntries(
|
|
111
|
+
Object.entries(overrides).map(([k, v]) => [k, { path: v }]),
|
|
112
|
+
),
|
|
113
|
+
};
|
|
114
|
+
const tmpPath = this.filePath + ".tmp";
|
|
115
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n");
|
|
116
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
117
|
+
}
|
|
118
|
+
}
|