@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +87 -114
- package/README.md +408 -430
- package/docs/architecture.md +465 -12
- package/package.json +10 -5
- package/packages/extension/package.json +14 -4
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +40 -8
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +5 -4
- package/packages/extension/src/bridge.ts +171 -17
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +43 -0
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +83 -27
- package/packages/server/package.json +16 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +237 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +111 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +13 -7
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +8 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +310 -39
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +207 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +141 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +211 -10
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +56 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +71 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +63 -46
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +16 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/protocol.ts +23 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/state-replay.ts +9 -0
- package/packages/shared/src/tool-registry/definitions.ts +434 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
|
@@ -0,0 +1,434 @@
|
|
|
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
|
+
import type { Strategy } from "./types.js";
|
|
26
|
+
|
|
27
|
+
// ── Classifier ──────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** Classifier: strategies → Source. Shared across binary and module tools. */
|
|
30
|
+
function classify(strategyName: string): Source {
|
|
31
|
+
if (strategyName === "override") return "override";
|
|
32
|
+
if (strategyName === "managed") return "managed";
|
|
33
|
+
if (strategyName === "npm-global") return "npm-global";
|
|
34
|
+
if (strategyName === "bare-import") return "bare-import";
|
|
35
|
+
// `where` and anything else — resolved via PATH — classifies as system.
|
|
36
|
+
return "system";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Binary definitions ──────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function binaryDef(binaryName: string, deps?: StrategyDeps): ToolDefinition {
|
|
42
|
+
return {
|
|
43
|
+
name: binaryName,
|
|
44
|
+
kind: "binary",
|
|
45
|
+
strategies: [
|
|
46
|
+
overrideStrategy(binaryName, deps),
|
|
47
|
+
managedBinStrategy(binaryName, deps),
|
|
48
|
+
whereStrategy(binaryName, deps),
|
|
49
|
+
],
|
|
50
|
+
classify,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Module definitions ──────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** Sibling probe for an aliased package name (pi: `@mariozechner/*` + `@oh-my-pi/*`). */
|
|
57
|
+
function moduleDefWithAliases(
|
|
58
|
+
canonicalName: string,
|
|
59
|
+
pkgNames: readonly string[],
|
|
60
|
+
entry: string,
|
|
61
|
+
deps?: StrategyDeps,
|
|
62
|
+
): ToolDefinition {
|
|
63
|
+
const strategies = [overrideStrategy(canonicalName, deps)];
|
|
64
|
+
for (const pkg of pkgNames) strategies.push(bareImportStrategy(pkg));
|
|
65
|
+
for (const pkg of pkgNames) strategies.push(managedModuleStrategy(pkg, entry, deps));
|
|
66
|
+
for (const pkg of pkgNames) strategies.push(npmGlobalStrategy(pkg, entry, deps));
|
|
67
|
+
return { name: canonicalName, kind: "module", strategies, classify };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Build-time module definitions (electron, node-pty) ────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Bare-import strategy that resolves `<pkg>/package.json` and returns the
|
|
74
|
+
* containing directory. Used for build-time tools whose useful artifact is
|
|
75
|
+
* a sibling file of `package.json` (e.g. `electron/install.js`,
|
|
76
|
+
* `node-pty/prebuilds/`). Mirrors the semantics that build-time consumers
|
|
77
|
+
* (`publish.yml`, `Dockerfile.build`, `scripts/fix-pty-permissions.cjs`)
|
|
78
|
+
* need — see change: register-build-time-tools.
|
|
79
|
+
*
|
|
80
|
+
* `searchPaths` are passed to Node's resolver as the `paths` option,
|
|
81
|
+
* making the lookup work whether the package is hoisted to the repo root
|
|
82
|
+
* or nested under a workspace.
|
|
83
|
+
*/
|
|
84
|
+
function bareImportPackageDirStrategy(
|
|
85
|
+
pkgName: string,
|
|
86
|
+
searchPaths?: readonly string[],
|
|
87
|
+
deps?: StrategyDeps,
|
|
88
|
+
): Strategy {
|
|
89
|
+
const fallbackResolve = (id: string, from: string): string | null => {
|
|
90
|
+
try {
|
|
91
|
+
if (searchPaths && searchPaths.length > 0) {
|
|
92
|
+
const req = createRequire(from) as unknown as {
|
|
93
|
+
resolve(id: string, opts?: { paths?: readonly string[] }): string;
|
|
94
|
+
};
|
|
95
|
+
return req.resolve(id, { paths: searchPaths });
|
|
96
|
+
}
|
|
97
|
+
return createRequire(from).resolve(id);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const resolveModule = deps?.resolveModule ?? fallbackResolve;
|
|
103
|
+
return {
|
|
104
|
+
name: "bare-import",
|
|
105
|
+
run() {
|
|
106
|
+
const pkgJson = resolveModule(`${pkgName}/package.json`, import.meta.url);
|
|
107
|
+
if (!pkgJson) {
|
|
108
|
+
return { ok: false, reason: `cannot resolve ${pkgName}/package.json` };
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, path: path.dirname(pkgJson) };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Module def that returns the package directory (containing package.json). */
|
|
116
|
+
function packageDirModuleDef(
|
|
117
|
+
toolName: string,
|
|
118
|
+
pkgName: string,
|
|
119
|
+
options: { searchPaths?: readonly string[]; includeManaged?: boolean },
|
|
120
|
+
deps?: StrategyDeps,
|
|
121
|
+
): ToolDefinition {
|
|
122
|
+
const strategies: Strategy[] = [
|
|
123
|
+
overrideStrategy(toolName, deps),
|
|
124
|
+
bareImportPackageDirStrategy(pkgName, options.searchPaths, deps),
|
|
125
|
+
];
|
|
126
|
+
if (options.includeManaged) {
|
|
127
|
+
strategies.push(managedModuleStrategy(pkgName, "package.json", deps));
|
|
128
|
+
}
|
|
129
|
+
return { name: toolName, kind: "module", strategies, classify };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Registration ─────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
// Tools intentionally NOT registered:
|
|
135
|
+
// - `tsx` — a TypeScript *loader* used via `node --import tsx`,
|
|
136
|
+
// not a tool the dashboard spawns. When pi is installed, pi ships
|
|
137
|
+
// jiti which the server prefers; otherwise tsx is co-installed
|
|
138
|
+
// as a dev dep of the server package.
|
|
139
|
+
// - `pi-dashboard` — that's the package this code is part of.
|
|
140
|
+
// "Is it installed" is a bootstrap concern handled directly in
|
|
141
|
+
// `packages/electron/src/lib/dependency-detector.ts`.
|
|
142
|
+
//
|
|
143
|
+
// Build-time tools (see change: register-build-time-tools):
|
|
144
|
+
// - `electron` — module, returns the package directory containing
|
|
145
|
+
// `install.js`. Resolved with paths anchored at
|
|
146
|
+
// `packages/electron` to handle hoisted vs. nested
|
|
147
|
+
// layouts uniformly.
|
|
148
|
+
// - `node-pty` — module, returns the package directory containing
|
|
149
|
+
// `prebuilds/`. Standard module resolution suffices.
|
|
150
|
+
// See change: consolidate-tool-resolution (follow-up).
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Shared `toArgv` for Node-script executors (pi, openspec, npm).
|
|
154
|
+
*
|
|
155
|
+
* On Windows + `.js` resolved path → prepend node.exe to bypass the
|
|
156
|
+
* `.cmd` shim entirely (no cmd.exe in the spawn chain → no console
|
|
157
|
+
* flash). Elsewhere → direct invocation.
|
|
158
|
+
*
|
|
159
|
+
* This is the heart of the "no cmd flash" story: every CLI that ships
|
|
160
|
+
* as `.cmd` on Windows and is actually a Node script should be
|
|
161
|
+
* registered with this `toArgv` so the spawn becomes
|
|
162
|
+
* `node.exe <script.js>` (pure console-subsystem inherit, no new
|
|
163
|
+
* window ever).
|
|
164
|
+
*/
|
|
165
|
+
const nodeScriptToArgv: ToolDefinition["toArgv"] = (resolvedPath, { platform, registry }) => {
|
|
166
|
+
if (platform === "win32" && /\.js$/i.test(resolvedPath)) {
|
|
167
|
+
const node = registry.resolve("node");
|
|
168
|
+
if (node.ok && node.path) return [node.path, resolvedPath];
|
|
169
|
+
}
|
|
170
|
+
return [resolvedPath];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Executor definition for `pi` — ONE tool, OS dispatch inside.
|
|
175
|
+
*
|
|
176
|
+
* On Windows, the strategy chain finds pi-coding-agent's `dist/cli.js`
|
|
177
|
+
* (managed → bare-import → npm-global), and `toArgv` wraps it with
|
|
178
|
+
* `node.exe` to produce `[node.exe, cli.js]`. Falls back to `pi.cmd`
|
|
179
|
+
* on PATH when the cli.js is nowhere to be found.
|
|
180
|
+
*
|
|
181
|
+
* On Unix, the chain finds `pi` on PATH; argv = [pi].
|
|
182
|
+
*/
|
|
183
|
+
function piExecutorDef(deps?: StrategyDeps): ToolDefinition {
|
|
184
|
+
const piPkgAliases = ["@mariozechner/pi-coding-agent", "@oh-my-pi/pi-coding-agent"];
|
|
185
|
+
const cliEntry = path.join("dist", "cli.js");
|
|
186
|
+
|
|
187
|
+
const winStrategies = [
|
|
188
|
+
overrideStrategy("pi", deps),
|
|
189
|
+
...piPkgAliases.map((pkg) => bareImportCliStrategy(pkg, cliEntry, deps)),
|
|
190
|
+
...piPkgAliases.map((pkg) => managedModuleStrategy(pkg, cliEntry, deps)),
|
|
191
|
+
...piPkgAliases.map((pkg) => npmGlobalStrategy(pkg, cliEntry, deps)),
|
|
192
|
+
managedBinStrategy("pi", deps),
|
|
193
|
+
whereStrategy("pi", deps),
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const unixStrategies = [
|
|
197
|
+
overrideStrategy("pi", deps),
|
|
198
|
+
managedBinStrategy("pi", deps),
|
|
199
|
+
whereStrategy("pi", deps),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
name: "pi",
|
|
204
|
+
kind: "executor",
|
|
205
|
+
strategies: unixStrategies,
|
|
206
|
+
platformStrategies: { win32: winStrategies },
|
|
207
|
+
toArgv: nodeScriptToArgv,
|
|
208
|
+
classify,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Executor definition for `openspec`.
|
|
214
|
+
*
|
|
215
|
+
* On Windows: finds `@fission-ai/openspec/bin/openspec.js` via managed
|
|
216
|
+
* → bare-import → npm-global. `toArgv` wraps with node.exe.
|
|
217
|
+
* On Unix: finds `openspec` binary on PATH.
|
|
218
|
+
*/
|
|
219
|
+
function openspecExecutorDef(deps?: StrategyDeps): ToolDefinition {
|
|
220
|
+
const pkgName = "@fission-ai/openspec";
|
|
221
|
+
const cliEntry = path.join("bin", "openspec.js");
|
|
222
|
+
|
|
223
|
+
const winStrategies = [
|
|
224
|
+
overrideStrategy("openspec", deps),
|
|
225
|
+
bareImportCliStrategy(pkgName, cliEntry),
|
|
226
|
+
managedModuleStrategy(pkgName, cliEntry, deps),
|
|
227
|
+
npmGlobalStrategy(pkgName, cliEntry, deps),
|
|
228
|
+
managedBinStrategy("openspec", deps),
|
|
229
|
+
whereStrategy("openspec", deps),
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const unixStrategies = [
|
|
233
|
+
overrideStrategy("openspec", deps),
|
|
234
|
+
managedBinStrategy("openspec", deps),
|
|
235
|
+
whereStrategy("openspec", deps),
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
name: "openspec",
|
|
240
|
+
kind: "executor",
|
|
241
|
+
strategies: unixStrategies,
|
|
242
|
+
platformStrategies: { win32: winStrategies },
|
|
243
|
+
toArgv: nodeScriptToArgv,
|
|
244
|
+
classify,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Executor definition for `npm`.
|
|
250
|
+
*
|
|
251
|
+
* npm is bundled with Node itself, not a standalone npm install. On
|
|
252
|
+
* Windows: find `<node-dir>/node_modules/npm/bin/npm-cli.js` by
|
|
253
|
+
* looking beside the resolved `node.exe`. Fallback: PATH lookup
|
|
254
|
+
* (which returns npm.cmd).
|
|
255
|
+
* On Unix: find `npm` on PATH.
|
|
256
|
+
*
|
|
257
|
+
* Motivation: npm.cmd internally runs `node.exe npm-cli.js`, and the
|
|
258
|
+
* inner node.exe can allocate a new console (Node issue #21825). By
|
|
259
|
+
* resolving to npm-cli.js directly + spawning via node.exe ourselves,
|
|
260
|
+
* we bypass cmd.exe + npm.cmd entirely.
|
|
261
|
+
*/
|
|
262
|
+
function npmExecutorDef(deps?: StrategyDeps): ToolDefinition {
|
|
263
|
+
const npmRelativeToNode = path.join("node_modules", "npm", "bin", "npm-cli.js");
|
|
264
|
+
|
|
265
|
+
// Custom strategy: find npm-cli.js beside the resolved node.exe.
|
|
266
|
+
// We can't pre-compute the node path at definition time (the registry
|
|
267
|
+
// isn't fully constructed yet), so the strategy resolves node
|
|
268
|
+
// lazily at run time via the global registry hook.
|
|
269
|
+
const npmCliBesideNodeStrategy = {
|
|
270
|
+
name: "managed", // classified as managed because it ships with node
|
|
271
|
+
run(): { ok: true; path: string } | { ok: false; reason: string } {
|
|
272
|
+
// Find node.exe from process.execPath or environment.
|
|
273
|
+
const nodeExe = process.execPath;
|
|
274
|
+
if (!nodeExe) return { ok: false, reason: "process.execPath unset" };
|
|
275
|
+
const nodeDir = path.dirname(nodeExe);
|
|
276
|
+
const candidate = path.join(nodeDir, npmRelativeToNode);
|
|
277
|
+
try {
|
|
278
|
+
if (existsSync(candidate)) return { ok: true, path: candidate };
|
|
279
|
+
return { ok: false, reason: `missing: ${candidate}` };
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const winStrategies = [
|
|
287
|
+
overrideStrategy("npm", deps),
|
|
288
|
+
npmCliBesideNodeStrategy,
|
|
289
|
+
whereStrategy("npm", deps),
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const unixStrategies = [
|
|
293
|
+
overrideStrategy("npm", deps),
|
|
294
|
+
whereStrategy("npm", deps),
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
name: "npm",
|
|
299
|
+
kind: "executor",
|
|
300
|
+
strategies: unixStrategies,
|
|
301
|
+
platformStrategies: { win32: winStrategies },
|
|
302
|
+
toArgv: nodeScriptToArgv,
|
|
303
|
+
classify,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Helper: bare-import strategy that, on success, transforms the
|
|
309
|
+
* resolved `package.json` into the sibling `<entry>` path. Used by
|
|
310
|
+
* the pi executor to find pi-coding-agent's cli.js via the same
|
|
311
|
+
* module-resolution algorithm as `import()`.
|
|
312
|
+
*/
|
|
313
|
+
function bareImportCliStrategy(
|
|
314
|
+
pkgName: string,
|
|
315
|
+
entryRelative: string,
|
|
316
|
+
deps?: StrategyDeps,
|
|
317
|
+
) {
|
|
318
|
+
// Default uses the real module resolver anchored to this file;
|
|
319
|
+
// tests inject a fake via deps.resolveModule.
|
|
320
|
+
const resolveModule: NonNullable<StrategyDeps["resolveModule"]> =
|
|
321
|
+
deps?.resolveModule
|
|
322
|
+
?? ((id, from) => {
|
|
323
|
+
try {
|
|
324
|
+
return createRequire(from).resolve(id);
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
name: "bare-import",
|
|
331
|
+
run(): { ok: true; path: string } | { ok: false; reason: string } {
|
|
332
|
+
const pkgJson = resolveModule(`${pkgName}/package.json`, import.meta.url);
|
|
333
|
+
if (!pkgJson) {
|
|
334
|
+
return { ok: false, reason: `cannot resolve ${pkgName}/package.json` };
|
|
335
|
+
}
|
|
336
|
+
const entry = path.join(path.dirname(pkgJson), entryRelative);
|
|
337
|
+
return { ok: true, path: entry };
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Register the standard set of dashboard tools. Idempotent — callers
|
|
344
|
+
* may re-register to supply custom strategy deps (e.g. tests).
|
|
345
|
+
*/
|
|
346
|
+
export function registerDefaultTools(registry: ToolRegistry, deps?: StrategyDeps): void {
|
|
347
|
+
// Executor-kind tools — Node scripts shipped as .cmd shims on
|
|
348
|
+
// Windows. Each registers as [node.exe, <script>.js] to bypass
|
|
349
|
+
// cmd.exe and the console-flash chain (Node issue #21825).
|
|
350
|
+
registry.register(piExecutorDef(deps));
|
|
351
|
+
registry.register(openspecExecutorDef(deps));
|
|
352
|
+
registry.register(npmExecutorDef(deps));
|
|
353
|
+
|
|
354
|
+
// Native binaries — no interpreter needed.
|
|
355
|
+
registry.register(binaryDef("node", deps));
|
|
356
|
+
registry.register(binaryDef("git", deps));
|
|
357
|
+
registry.register(binaryDef("zrok", deps));
|
|
358
|
+
|
|
359
|
+
// Platform-conditional process-inspection utilities. These are only
|
|
360
|
+
// called by `packages/shared/src/platform/process.ts` on their native
|
|
361
|
+
// platform — registering the non-native tools would surface as red
|
|
362
|
+
// "not found" rows in the Settings → Tools UI even though the code
|
|
363
|
+
// never calls them there.
|
|
364
|
+
//
|
|
365
|
+
// Honours the registry's `platform` so tests that inject `platform:
|
|
366
|
+
// "win32"` from a Linux host still exercise the Windows tool set.
|
|
367
|
+
//
|
|
368
|
+
// Windows system utilities used by the bridge's process scanner.
|
|
369
|
+
// Registered so callers resolve to full `.exe` paths (e.g.
|
|
370
|
+
// `C:\Windows\System32\wbem\wmic.exe`) and spawn directly — no
|
|
371
|
+
// PATHEXT resolution, no cmd.exe wrapping, windowsHide:true honored
|
|
372
|
+
// all the way down. See change: consolidate-windows-spawn-and-platform-handlers.
|
|
373
|
+
if (registry.getPlatform() === "win32") {
|
|
374
|
+
registry.register(binaryDef("wmic", deps));
|
|
375
|
+
registry.register(binaryDef("powershell", deps));
|
|
376
|
+
registry.register(binaryDef("tasklist", deps));
|
|
377
|
+
registry.register(binaryDef("taskkill", deps));
|
|
378
|
+
} else {
|
|
379
|
+
// POSIX process-inspection utilities. Used by `isProcessRunning`,
|
|
380
|
+
// `findPidByMarker`, `isProcessLikePi` in platform/process.ts.
|
|
381
|
+
registry.register(binaryDef("ps", deps));
|
|
382
|
+
registry.register(binaryDef("pgrep", deps));
|
|
383
|
+
}
|
|
384
|
+
// Windows Terminal — optional, override + where only (not part of
|
|
385
|
+
// managed install, not on Unix).
|
|
386
|
+
registry.register({
|
|
387
|
+
name: "wt",
|
|
388
|
+
kind: "binary",
|
|
389
|
+
strategies: [
|
|
390
|
+
overrideStrategy("wt", deps),
|
|
391
|
+
whereStrategy("wt", deps),
|
|
392
|
+
],
|
|
393
|
+
classify,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Node module entry for pi-coding-agent — used by DefaultPackageManager
|
|
397
|
+
// to IMPORT pi as a library (not spawn it as a process). Distinct from
|
|
398
|
+
// the `pi` executor above.
|
|
399
|
+
registry.register(
|
|
400
|
+
moduleDefWithAliases(
|
|
401
|
+
"pi-coding-agent",
|
|
402
|
+
["@mariozechner/pi-coding-agent", "@oh-my-pi/pi-coding-agent"],
|
|
403
|
+
path.join("dist", "index.js"),
|
|
404
|
+
deps,
|
|
405
|
+
),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Build-time tools (see change: register-build-time-tools).
|
|
409
|
+
registry.register(
|
|
410
|
+
packageDirModuleDef(
|
|
411
|
+
"electron",
|
|
412
|
+
"electron",
|
|
413
|
+
{
|
|
414
|
+
searchPaths: [path.resolve("packages/electron")],
|
|
415
|
+
includeManaged: true,
|
|
416
|
+
},
|
|
417
|
+
deps,
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
registry.register(
|
|
421
|
+
packageDirModuleDef(
|
|
422
|
+
"node-pty",
|
|
423
|
+
"node-pty",
|
|
424
|
+
{ includeManaged: false },
|
|
425
|
+
deps,
|
|
426
|
+
),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Handy re-exports for callers that want raw definitions for testing. */
|
|
431
|
+
export const _internals = {
|
|
432
|
+
binaryDef,
|
|
433
|
+
moduleDefWithAliases,
|
|
434
|
+
};
|
|
@@ -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
|
+
}
|