@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,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolRegistry — single-source resolver for every external binary, module,
|
|
3
|
+
* and directory the dashboard depends on.
|
|
4
|
+
*
|
|
5
|
+
* Design (see change: consolidate-tool-resolution, design §1-§4):
|
|
6
|
+
* - Ordered strategy chain per tool (override → managed → bare-import →
|
|
7
|
+
* npm-global → where).
|
|
8
|
+
* - One Resolution record per tool, cached in the registry.
|
|
9
|
+
* - Rescan invalidates one or all cached Resolutions.
|
|
10
|
+
* - Module resolution dynamically imports the resolved entry and caches
|
|
11
|
+
* the loaded ES module alongside the Resolution.
|
|
12
|
+
*/
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
import {
|
|
15
|
+
type ExecutorResolution,
|
|
16
|
+
ModuleResolutionError,
|
|
17
|
+
type Resolution,
|
|
18
|
+
type Source,
|
|
19
|
+
type StrategyCtx,
|
|
20
|
+
type ToolDefinition,
|
|
21
|
+
UnknownToolError,
|
|
22
|
+
type TriedEntry,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
import { OverridesStore } from "./overrides.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Minimal platform-environment snapshot injected into strategies.
|
|
28
|
+
* Production leaves this undefined and strategies fall back to
|
|
29
|
+
* `os.homedir()` / `process.cwd()`. Tests inject fakes so the bootstrap
|
|
30
|
+
* harness can reason about alternate HOME directories.
|
|
31
|
+
*/
|
|
32
|
+
export interface PlatformEnv {
|
|
33
|
+
homedir?: string;
|
|
34
|
+
cwd?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ToolRegistryDeps {
|
|
38
|
+
overrides?: OverridesStore;
|
|
39
|
+
platform?: NodeJS.Platform;
|
|
40
|
+
/** Injected for tests; default uses native dynamic `import(url)`. */
|
|
41
|
+
importModule?: (url: string) => Promise<unknown>;
|
|
42
|
+
/** Clock injector (used by tests for deterministic `resolvedAt`). */
|
|
43
|
+
now?: () => number;
|
|
44
|
+
/** Environment overrides threaded into `StrategyCtx.env` (see types.ts). */
|
|
45
|
+
env?: PlatformEnv;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Default strategy → source mapping when a definition doesn't override it. */
|
|
49
|
+
const DEFAULT_CLASSIFY = (strategyName: string): Source => {
|
|
50
|
+
switch (strategyName) {
|
|
51
|
+
case "override":
|
|
52
|
+
return "override";
|
|
53
|
+
case "managed":
|
|
54
|
+
return "managed";
|
|
55
|
+
case "bare-import":
|
|
56
|
+
return "bare-import";
|
|
57
|
+
case "npm-global":
|
|
58
|
+
return "npm-global";
|
|
59
|
+
default:
|
|
60
|
+
return "system";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export class ToolRegistry {
|
|
65
|
+
private readonly definitions = new Map<string, ToolDefinition>();
|
|
66
|
+
private readonly cache = new Map<string, Resolution>();
|
|
67
|
+
private readonly moduleCache = new Map<string, unknown>();
|
|
68
|
+
private readonly overrides: OverridesStore;
|
|
69
|
+
private readonly platform: NodeJS.Platform;
|
|
70
|
+
private readonly importModule: (url: string) => Promise<unknown>;
|
|
71
|
+
private readonly now: () => number;
|
|
72
|
+
private readonly env: PlatformEnv | undefined;
|
|
73
|
+
|
|
74
|
+
constructor(deps: ToolRegistryDeps = {}) {
|
|
75
|
+
this.overrides = deps.overrides ?? new OverridesStore();
|
|
76
|
+
this.platform = deps.platform ?? process.platform;
|
|
77
|
+
this.importModule = deps.importModule ?? ((url) => import(/* @vite-ignore */ url));
|
|
78
|
+
this.now = deps.now ?? (() => Date.now());
|
|
79
|
+
this.env = deps.env;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Platform the registry was created for (or the current runtime's
|
|
84
|
+
* `process.platform`). Exposed so platform-conditional tool registration
|
|
85
|
+
* (e.g. skip `ps`/`pgrep` on Windows) in `registerDefaultTools` honours
|
|
86
|
+
* the test's injected platform instead of always reading the host.
|
|
87
|
+
*/
|
|
88
|
+
getPlatform(): NodeJS.Platform {
|
|
89
|
+
return this.platform;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Register a tool definition. Last registration wins (tests re-register). */
|
|
93
|
+
register(def: ToolDefinition): void {
|
|
94
|
+
this.definitions.set(def.name, def);
|
|
95
|
+
this.cache.delete(def.name);
|
|
96
|
+
this.moduleCache.delete(def.name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** True when the name has a registered definition. */
|
|
100
|
+
has(name: string): boolean {
|
|
101
|
+
return this.definitions.has(name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Snapshot of every registered tool's resolution. Triggers resolution as needed. */
|
|
105
|
+
list(): Resolution[] {
|
|
106
|
+
return Array.from(this.definitions.keys()).map((n) => this.resolve(n));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Resolve a binary/directory/module-path. Uses cached result when present. */
|
|
110
|
+
resolve(name: string): Resolution {
|
|
111
|
+
const def = this.definitions.get(name);
|
|
112
|
+
if (!def) throw new UnknownToolError(name);
|
|
113
|
+
|
|
114
|
+
const cached = this.cache.get(name);
|
|
115
|
+
if (cached) return cached;
|
|
116
|
+
|
|
117
|
+
const ctx: StrategyCtx = {
|
|
118
|
+
overrides: this.overrides.list(),
|
|
119
|
+
platform: this.platform,
|
|
120
|
+
env: this.env,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const tried: TriedEntry[] = [];
|
|
124
|
+
let winner: { strategy: string; path: string } | null = null;
|
|
125
|
+
|
|
126
|
+
// Platform-specific strategy chain overrides the default when
|
|
127
|
+
// present. Use case: tool resolution chain itself differs per OS
|
|
128
|
+
// (e.g. `pi` on Windows finds pi-coding-agent's cli.js; on Unix
|
|
129
|
+
// finds the `pi` binary on PATH).
|
|
130
|
+
const strategies = def.platformStrategies?.[this.platform] ?? def.strategies;
|
|
131
|
+
|
|
132
|
+
for (const strategy of strategies) {
|
|
133
|
+
const result = strategy.run(ctx);
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
tried.push({ strategy: strategy.name, result: result.reason });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Optional validation (existence check, "must end in dist/index.js", ...).
|
|
139
|
+
if (def.validate) {
|
|
140
|
+
const v = def.validate(result.path);
|
|
141
|
+
if (!v.ok) {
|
|
142
|
+
tried.push({ strategy: strategy.name, result: `invalid: ${v.reason}` });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
tried.push({ strategy: strategy.name, result: "ok" });
|
|
147
|
+
winner = { strategy: strategy.name, path: result.path };
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const classify = def.classify ?? DEFAULT_CLASSIFY;
|
|
152
|
+
const resolution: Resolution = winner
|
|
153
|
+
? {
|
|
154
|
+
name,
|
|
155
|
+
ok: true,
|
|
156
|
+
path: winner.path,
|
|
157
|
+
source: classify(winner.strategy),
|
|
158
|
+
tried,
|
|
159
|
+
resolvedAt: this.now(),
|
|
160
|
+
}
|
|
161
|
+
: {
|
|
162
|
+
name,
|
|
163
|
+
ok: false,
|
|
164
|
+
path: null,
|
|
165
|
+
source: null,
|
|
166
|
+
tried,
|
|
167
|
+
resolvedAt: this.now(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
this.cache.set(name, resolution);
|
|
171
|
+
return resolution;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve a tool and return its spawn-ready argv.
|
|
176
|
+
*
|
|
177
|
+
* Uses `resolve()` to find the artifact path, then applies the
|
|
178
|
+
* definition's `toArgv` transform (if any) to produce argv. Default:
|
|
179
|
+
* `argv = [path]` — appropriate for binary-kind tools resolved to an
|
|
180
|
+
* absolute path on PATH.
|
|
181
|
+
*
|
|
182
|
+
* For executor-kind tools with platform-specific interpreter needs
|
|
183
|
+
* (e.g. pi on Windows → `[node.exe, cli.js]`), `toArgv` does the
|
|
184
|
+
* assembly. `toArgv` may call `this.resolve(peer)` to find peer
|
|
185
|
+
* tools (e.g. `node`) and MUST fall back to `[path]` if peers are
|
|
186
|
+
* missing.
|
|
187
|
+
*
|
|
188
|
+
* Callers spawn via `spawn(argv[0], [...argv.slice(1), ...userArgs])`.
|
|
189
|
+
*
|
|
190
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
191
|
+
*/
|
|
192
|
+
resolveExecutor(name: string): ExecutorResolution {
|
|
193
|
+
const def = this.definitions.get(name);
|
|
194
|
+
if (!def) throw new UnknownToolError(name);
|
|
195
|
+
|
|
196
|
+
const resolution = this.resolve(name);
|
|
197
|
+
if (!resolution.ok || !resolution.path) {
|
|
198
|
+
return { ...resolution, argv: [] };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const argv = def.toArgv
|
|
202
|
+
? def.toArgv(resolution.path, { platform: this.platform, registry: this })
|
|
203
|
+
: [resolution.path];
|
|
204
|
+
|
|
205
|
+
return { ...resolution, argv };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve AND dynamically import a registered module-kind tool.
|
|
210
|
+
* Throws `ModuleResolutionError` with the full `tried[]` trail when
|
|
211
|
+
* every strategy fails. The loaded ES module is cached alongside the
|
|
212
|
+
* Resolution; `rescan(name)` invalidates both.
|
|
213
|
+
*/
|
|
214
|
+
async resolveModule<T = unknown>(name: string): Promise<{ resolution: Resolution; module: T }> {
|
|
215
|
+
const def = this.definitions.get(name);
|
|
216
|
+
if (!def) throw new UnknownToolError(name);
|
|
217
|
+
if (def.kind !== "module") {
|
|
218
|
+
throw new Error(`Tool "${name}" is not kind: "module"; use resolve() instead.`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const resolution = this.resolve(name);
|
|
222
|
+
if (!resolution.ok || !resolution.path) {
|
|
223
|
+
throw new ModuleResolutionError(resolution);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const cached = this.moduleCache.get(name) as T | undefined;
|
|
227
|
+
if (cached) return { resolution, module: cached };
|
|
228
|
+
|
|
229
|
+
const url = pathToFileURL(resolution.path).href;
|
|
230
|
+
const loaded = (await this.importModule(url)) as T;
|
|
231
|
+
this.moduleCache.set(name, loaded);
|
|
232
|
+
return { resolution, module: loaded };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Drop cached Resolution(s). Next resolve() re-runs strategies. */
|
|
236
|
+
rescan(name?: string): void {
|
|
237
|
+
if (name === undefined) {
|
|
238
|
+
this.cache.clear();
|
|
239
|
+
this.moduleCache.clear();
|
|
240
|
+
this.overrides.invalidate();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.cache.delete(name);
|
|
244
|
+
this.moduleCache.delete(name);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Set a path override. Invalidates the target's cache. */
|
|
248
|
+
setOverride(name: string, overridePath: string): void {
|
|
249
|
+
if (!this.definitions.has(name)) throw new UnknownToolError(name);
|
|
250
|
+
this.overrides.set(name, overridePath);
|
|
251
|
+
this.cache.delete(name);
|
|
252
|
+
this.moduleCache.delete(name);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Clear a path override. Invalidates the target's cache. */
|
|
256
|
+
clearOverride(name: string): void {
|
|
257
|
+
if (!this.definitions.has(name)) throw new UnknownToolError(name);
|
|
258
|
+
this.overrides.clear(name);
|
|
259
|
+
this.cache.delete(name);
|
|
260
|
+
this.moduleCache.delete(name);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable resolution strategies shared across tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* Strategies are pure functions over their `StrategyCtx` — filesystem
|
|
5
|
+
* access (`existsSync`) is the only side effect. They never spawn; PATH
|
|
6
|
+
* search delegates to `ToolResolver.which()` which is injectable for
|
|
7
|
+
* tests via the `lookup` parameter.
|
|
8
|
+
*
|
|
9
|
+
* See change: consolidate-tool-resolution (design §2).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { ToolResolver } from "../platform/binary-lookup.js";
|
|
15
|
+
import { getManagedBin, getManagedDir } from "../managed-paths.js";
|
|
16
|
+
import * as npm from "../platform/npm.js";
|
|
17
|
+
import type { Strategy, StrategyCtx, StrategyResult } from "./types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Injectable surfaces used by strategies.
|
|
21
|
+
*
|
|
22
|
+
* - `exists` — fs existence probe (memfs in tests).
|
|
23
|
+
* - `which` — PATH search.
|
|
24
|
+
* - `npmRootGlobal` — result of `npm root -g` (tests inject to avoid spawn).
|
|
25
|
+
* - `resolveModule` — node-module resolution (id, from) → absolute path.
|
|
26
|
+
* Production uses `createRequire(from).resolve(id)`; tests walk fake
|
|
27
|
+
* node_modules trees.
|
|
28
|
+
*/
|
|
29
|
+
export interface StrategyDeps {
|
|
30
|
+
exists?(p: string): boolean;
|
|
31
|
+
which?(name: string): string | null;
|
|
32
|
+
npmRootGlobal?(): string;
|
|
33
|
+
resolveModule?(id: string, from: string): string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultResolveModule(id: string, from: string): string | null {
|
|
37
|
+
try {
|
|
38
|
+
return createRequire(from).resolve(id);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function defaults(): Required<StrategyDeps> {
|
|
45
|
+
const resolver = new ToolResolver({
|
|
46
|
+
processExecPath: process.execPath,
|
|
47
|
+
useLoginShell: true,
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
exists: existsSync,
|
|
51
|
+
which: (name) => resolver.which(name),
|
|
52
|
+
npmRootGlobal: () => npm.rootGlobalOr(""),
|
|
53
|
+
resolveModule: defaultResolveModule,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Merge caller-supplied deps over the live defaults. */
|
|
58
|
+
function d(deps?: StrategyDeps): Required<StrategyDeps> {
|
|
59
|
+
const base = defaults();
|
|
60
|
+
if (!deps) return base;
|
|
61
|
+
return {
|
|
62
|
+
exists: deps.exists ?? base.exists,
|
|
63
|
+
which: deps.which ?? base.which,
|
|
64
|
+
npmRootGlobal: deps.npmRootGlobal ?? base.npmRootGlobal,
|
|
65
|
+
resolveModule: deps.resolveModule ?? base.resolveModule,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Strategies ──────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Look up a registered path override by tool name. Existence is checked
|
|
73
|
+
* here so invalid overrides fall through with reason `invalid: <...>`
|
|
74
|
+
* without requiring callers to wire a separate validator.
|
|
75
|
+
*/
|
|
76
|
+
export function overrideStrategy(toolName: string, deps?: StrategyDeps): Strategy {
|
|
77
|
+
const { exists } = d(deps);
|
|
78
|
+
return {
|
|
79
|
+
name: "override",
|
|
80
|
+
run(ctx): StrategyResult {
|
|
81
|
+
const p = ctx.overrides[toolName];
|
|
82
|
+
if (!p) return { ok: false, reason: "no override set" };
|
|
83
|
+
if (!exists(p)) return { ok: false, reason: `invalid: path does not exist: ${p}` };
|
|
84
|
+
return { ok: true, path: p };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Managed install: `~/.pi-dashboard/node_modules/.bin/<name>(.cmd)` for
|
|
91
|
+
* binaries, or any explicit relative path under `MANAGED_DIR` for
|
|
92
|
+
* modules/directories.
|
|
93
|
+
*/
|
|
94
|
+
export function managedBinStrategy(
|
|
95
|
+
binaryName: string,
|
|
96
|
+
deps?: StrategyDeps,
|
|
97
|
+
): Strategy {
|
|
98
|
+
const { exists } = d(deps);
|
|
99
|
+
return {
|
|
100
|
+
name: "managed",
|
|
101
|
+
run(ctx): StrategyResult {
|
|
102
|
+
const ext = ctx.platform === "win32" ? ".cmd" : "";
|
|
103
|
+
const candidate = path.join(getManagedBin(ctx.env), binaryName + ext);
|
|
104
|
+
if (exists(candidate)) return { ok: true, path: candidate };
|
|
105
|
+
return { ok: false, reason: `missing: ${candidate}` };
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Managed module entry: `~/.pi-dashboard/node_modules/<pkg>/dist/index.js`
|
|
112
|
+
* (or a caller-specified relative entry).
|
|
113
|
+
*/
|
|
114
|
+
export function managedModuleStrategy(
|
|
115
|
+
pkgName: string,
|
|
116
|
+
entryRelative: string = path.join("dist", "index.js"),
|
|
117
|
+
deps?: StrategyDeps,
|
|
118
|
+
): Strategy {
|
|
119
|
+
const { exists } = d(deps);
|
|
120
|
+
return {
|
|
121
|
+
name: "managed",
|
|
122
|
+
run(ctx: StrategyCtx): StrategyResult {
|
|
123
|
+
const candidate = path.join(getManagedDir(ctx.env), "node_modules", pkgName, entryRelative);
|
|
124
|
+
if (exists(candidate)) return { ok: true, path: candidate };
|
|
125
|
+
return { ok: false, reason: `missing: ${candidate}` };
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Global npm install: `<npm root -g>/<pkg>/<entry>`. Falls back to
|
|
132
|
+
* `{ ok: false }` when `npm root -g` fails or the file is absent.
|
|
133
|
+
*/
|
|
134
|
+
export function npmGlobalStrategy(
|
|
135
|
+
pkgName: string,
|
|
136
|
+
entryRelative: string = path.join("dist", "index.js"),
|
|
137
|
+
deps?: StrategyDeps,
|
|
138
|
+
): Strategy {
|
|
139
|
+
const { exists, npmRootGlobal } = d(deps);
|
|
140
|
+
return {
|
|
141
|
+
name: "npm-global",
|
|
142
|
+
run(): StrategyResult {
|
|
143
|
+
const root = npmRootGlobal();
|
|
144
|
+
if (!root) return { ok: false, reason: "npm root -g failed" };
|
|
145
|
+
const candidate = path.join(root, pkgName, entryRelative);
|
|
146
|
+
if (exists(candidate)) return { ok: true, path: candidate };
|
|
147
|
+
return { ok: false, reason: `missing: ${candidate}` };
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* PATH search via `ToolResolver.which()`. This is the plain-old "is it
|
|
154
|
+
* on PATH" strategy and should appear last in most chains.
|
|
155
|
+
*/
|
|
156
|
+
export function whereStrategy(binaryName: string, deps?: StrategyDeps): Strategy {
|
|
157
|
+
const { which } = d(deps);
|
|
158
|
+
return {
|
|
159
|
+
name: "where",
|
|
160
|
+
run(): StrategyResult {
|
|
161
|
+
const p = which(binaryName);
|
|
162
|
+
if (p) return { ok: true, path: p };
|
|
163
|
+
return { ok: false, reason: `not found on PATH` };
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Bare `import("<pkg>")` — succeeds when the package is reachable from
|
|
170
|
+
* the caller's node_modules tree. We probe synchronously via
|
|
171
|
+
* `createRequire(import.meta.url).resolve(pkgName)`, which follows the
|
|
172
|
+
* same module-resolution algorithm as `import()` but returns a path.
|
|
173
|
+
*
|
|
174
|
+
* The returned path is the resolved entry file; `resolveModule()` then
|
|
175
|
+
* dynamically imports it via `pathToFileURL`. This keeps strategies
|
|
176
|
+
* uniformly sync and keeps the diagnostic trail honest (if the package
|
|
177
|
+
* isn't resolvable, we record the reason here instead of letting it
|
|
178
|
+
* surface as an opaque `import()` throw later).
|
|
179
|
+
*
|
|
180
|
+
* `anchor` determines which node_modules tree we search. Default is
|
|
181
|
+
* this file's URL (i.e. the shared package) — which is typically what
|
|
182
|
+
* callers want: "is pi a dependency of the dashboard?"
|
|
183
|
+
*/
|
|
184
|
+
export function bareImportStrategy(
|
|
185
|
+
pkgName: string,
|
|
186
|
+
anchor: string = import.meta.url,
|
|
187
|
+
deps?: StrategyDeps,
|
|
188
|
+
): Strategy {
|
|
189
|
+
const { resolveModule } = d(deps);
|
|
190
|
+
return {
|
|
191
|
+
name: "bare-import",
|
|
192
|
+
run(): StrategyResult {
|
|
193
|
+
const resolved = resolveModule(pkgName, anchor);
|
|
194
|
+
if (!resolved) return { ok: false, reason: `cannot resolve ${pkgName} from ${anchor}` };
|
|
195
|
+
return { ok: true, path: resolved };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public type contract for the tool registry.
|
|
3
|
+
*
|
|
4
|
+
* The registry resolves every external binary, module, or directory the
|
|
5
|
+
* dashboard depends on through an ordered list of strategies. Each
|
|
6
|
+
* resolution records a diagnostic trail (what was tried + why it
|
|
7
|
+
* succeeded/failed) alongside the winning path.
|
|
8
|
+
*
|
|
9
|
+
* See change: consolidate-tool-resolution.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** What kind of artifact a tool definition resolves. */
|
|
13
|
+
export type ToolKind = "binary" | "module" | "directory" | "executor";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How a resolved path was obtained. Strategy name → source mapping is
|
|
17
|
+
* declared by each tool definition's `classify()` (see registry).
|
|
18
|
+
*/
|
|
19
|
+
export type Source =
|
|
20
|
+
| "override"
|
|
21
|
+
| "managed"
|
|
22
|
+
| "system"
|
|
23
|
+
| "npm-global"
|
|
24
|
+
| "bare-import";
|
|
25
|
+
|
|
26
|
+
/** Result returned by a single strategy attempt. */
|
|
27
|
+
export type StrategyResult =
|
|
28
|
+
| { ok: true; path: string }
|
|
29
|
+
| { ok: false; reason: string };
|
|
30
|
+
|
|
31
|
+
/** One attempt recorded on a Resolution's `tried[]` list. */
|
|
32
|
+
export interface TriedEntry {
|
|
33
|
+
/** Strategy name, e.g. "override", "managed", "npm-global". */
|
|
34
|
+
strategy: string;
|
|
35
|
+
/** "ok" on success, the strategy's failure reason on miss. */
|
|
36
|
+
result: "ok" | string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Output of `ToolRegistry.resolve(name)`. */
|
|
40
|
+
export interface Resolution {
|
|
41
|
+
/** Tool name as registered. */
|
|
42
|
+
name: string;
|
|
43
|
+
/** True if any strategy produced a valid path. */
|
|
44
|
+
ok: boolean;
|
|
45
|
+
/** Absolute path (binary / module entry / directory). Null on failure. */
|
|
46
|
+
path: string | null;
|
|
47
|
+
/** Source classification of the winning strategy. Null on failure. */
|
|
48
|
+
source: Source | null;
|
|
49
|
+
/** Ordered diagnostic trail — one entry per attempted strategy. */
|
|
50
|
+
tried: TriedEntry[];
|
|
51
|
+
/** Epoch ms when resolution completed. */
|
|
52
|
+
resolvedAt: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Output of `ToolRegistry.resolveExecutor(name)` — extends Resolution
|
|
57
|
+
* with `argv`, the ready-to-spawn command array.
|
|
58
|
+
*
|
|
59
|
+
* Callers spawn via `spawn(argv[0], [...argv.slice(1), ...userArgs])`.
|
|
60
|
+
* For simple binaries `argv = [path]`; for scripts that need an
|
|
61
|
+
* interpreter (e.g. pi's `node dist/cli.js` on Windows) `argv =
|
|
62
|
+
* [interpreter, scriptPath]`.
|
|
63
|
+
*
|
|
64
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
65
|
+
*/
|
|
66
|
+
export interface ExecutorResolution extends Resolution {
|
|
67
|
+
argv: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Context passed to every strategy function. */
|
|
71
|
+
export interface StrategyCtx {
|
|
72
|
+
/** Per-registry override map, { [toolName]: absolutePath }. */
|
|
73
|
+
overrides: Readonly<Record<string, string>>;
|
|
74
|
+
/** Platform discriminator (injectable for tests). */
|
|
75
|
+
platform: NodeJS.Platform;
|
|
76
|
+
/**
|
|
77
|
+
* Environment overrides used by HOME-sensitive strategies (managed/*,
|
|
78
|
+
* npm-global under APPDATA on win32, etc.). Production registries
|
|
79
|
+
* populate from `os.homedir()` + `process.cwd()`; tests inject fakes
|
|
80
|
+
* so the harness can reason about alternate HOME directories without
|
|
81
|
+
* mutating globals.
|
|
82
|
+
*/
|
|
83
|
+
env?: {
|
|
84
|
+
homedir?: string;
|
|
85
|
+
cwd?: string;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A single resolution strategy. Pure function of its ctx + the tool's data. */
|
|
90
|
+
export interface Strategy {
|
|
91
|
+
/** Name recorded in `tried[]`. */
|
|
92
|
+
name: string;
|
|
93
|
+
/** Attempt resolution. Never throws — signal failure via { ok: false }. */
|
|
94
|
+
run(ctx: StrategyCtx): StrategyResult;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Transform a successfully-resolved path into an argv ready to pass
|
|
99
|
+
* to `spawn(argv[0], argv.slice(1))`.
|
|
100
|
+
*
|
|
101
|
+
* Default behaviour (when a definition omits `toArgv`): `argv = [path]`.
|
|
102
|
+
* Scripts that need an interpreter (e.g. pi's `cli.js` on Windows)
|
|
103
|
+
* return `[interpreterPath, scriptPath]`.
|
|
104
|
+
*
|
|
105
|
+
* The function may call `registry.resolve(...)` to look up peer tools
|
|
106
|
+
* (e.g. `node`). It MUST NOT throw — return the path-only fallback
|
|
107
|
+
* `[path]` if a peer tool is missing.
|
|
108
|
+
*/
|
|
109
|
+
export type ToArgvFn = (resolvedPath: string, ctx: { platform: NodeJS.Platform; registry: ToolRegistryLike }) => string[];
|
|
110
|
+
|
|
111
|
+
/** Minimal interface used by `toArgv` functions to look up peer tools. */
|
|
112
|
+
export interface ToolRegistryLike {
|
|
113
|
+
resolve(name: string): Resolution;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Declarative tool registration. */
|
|
117
|
+
export interface ToolDefinition {
|
|
118
|
+
/** Registry key — unique within the registry. */
|
|
119
|
+
name: string;
|
|
120
|
+
/** Kind drives how `resolveModule()` / path validation behaves. */
|
|
121
|
+
kind: ToolKind;
|
|
122
|
+
/**
|
|
123
|
+
* Ordered strategies used when no platform-specific override exists
|
|
124
|
+
* for the current OS. First successful strategy wins.
|
|
125
|
+
*/
|
|
126
|
+
strategies: Strategy[];
|
|
127
|
+
/**
|
|
128
|
+
* Optional per-platform strategy override. Present key wins over
|
|
129
|
+
* `strategies`. Absent key falls back to `strategies`.
|
|
130
|
+
*
|
|
131
|
+
* Use this when the chain itself is OS-dependent — e.g. pi on
|
|
132
|
+
* Windows looks for a JS entry (dist/cli.js via module strategies),
|
|
133
|
+
* while on Unix it looks for a `pi` binary on PATH.
|
|
134
|
+
*/
|
|
135
|
+
platformStrategies?: Partial<Record<NodeJS.Platform, Strategy[]>>;
|
|
136
|
+
/**
|
|
137
|
+
* Optional transform from resolved path → ready-to-spawn argv.
|
|
138
|
+
* Used by `resolveExecutor(name)`. Omitted → argv defaults to `[path]`.
|
|
139
|
+
*/
|
|
140
|
+
toArgv?: ToArgvFn;
|
|
141
|
+
/**
|
|
142
|
+
* Map a winning strategy name → Source. Unknown strategy names fall
|
|
143
|
+
* back to the default ("system"). Definitions usually set this so
|
|
144
|
+
* that e.g. "managed" → "managed", "bare-import" → "bare-import".
|
|
145
|
+
*/
|
|
146
|
+
classify?(strategyName: string): Source;
|
|
147
|
+
/**
|
|
148
|
+
* Optional post-resolution validation (e.g. "dist/index.js exists").
|
|
149
|
+
* If provided and returns a reason, the strategy is demoted to a
|
|
150
|
+
* failure carrying that reason, and the next strategy is tried.
|
|
151
|
+
*/
|
|
152
|
+
validate?(resolvedPath: string): { ok: true } | { ok: false; reason: string };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Errors ──────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/** Thrown by `resolve()` / `resolveModule()` for unregistered names. */
|
|
158
|
+
export class UnknownToolError extends Error {
|
|
159
|
+
public readonly tool: string;
|
|
160
|
+
constructor(tool: string) {
|
|
161
|
+
super(`Unknown tool: ${tool}`);
|
|
162
|
+
this.name = "UnknownToolError";
|
|
163
|
+
this.tool = tool;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Thrown by `resolveModule()` when every strategy fails. */
|
|
168
|
+
export class ModuleResolutionError extends Error {
|
|
169
|
+
public readonly resolution: Resolution;
|
|
170
|
+
constructor(resolution: Resolution) {
|
|
171
|
+
const trail = resolution.tried
|
|
172
|
+
.map((t) => ` - ${t.strategy}: ${t.result}`)
|
|
173
|
+
.join("\n");
|
|
174
|
+
super(
|
|
175
|
+
`Could not resolve module "${resolution.name}". Tried:\n${trail}`,
|
|
176
|
+
);
|
|
177
|
+
this.name = "ModuleResolutionError";
|
|
178
|
+
this.resolution = resolution;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -140,6 +140,13 @@ export interface OpenSpecChange {
|
|
|
140
140
|
completedTasks: number;
|
|
141
141
|
totalTasks: number;
|
|
142
142
|
artifacts: OpenSpecArtifact[];
|
|
143
|
+
/**
|
|
144
|
+
* Artifact-authoring completeness reported by `openspec status --change <name> --json`.
|
|
145
|
+
* `true` when all required artifacts for the change's workflow are present/done.
|
|
146
|
+
* Orthogonal to task-tally completeness; used by the dashboard to surface an
|
|
147
|
+
* "Archive anyway" escape hatch when artifacts are authored but tasks remain unchecked.
|
|
148
|
+
*/
|
|
149
|
+
isComplete?: boolean;
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
/** Lifecycle state of an OpenSpec change, derived from artifacts + task status */
|