@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2
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 +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `launchDashboardServer` — single shared spawn primitive for the
|
|
3
|
+
* dashboard server. Used by every starter (Bridge, Standalone CLI,
|
|
4
|
+
* Electron). Owns:
|
|
5
|
+
*
|
|
6
|
+
* - jiti loader resolution via `ToolResolver.resolveJiti({ anchor })`
|
|
7
|
+
* - argv construction via `spawnNodeScript` (which delegates to
|
|
8
|
+
* `buildNodeImportArgvParts` for the `--import` chunk)
|
|
9
|
+
* - env merge: `ToolResolver.buildSpawnEnv()` ∪ caller `env`
|
|
10
|
+
* (caller wins on conflict, e.g. `DASHBOARD_STARTER`)
|
|
11
|
+
* - log-file policy: caller-owned absolute path; we mkdir, open in
|
|
12
|
+
* append mode, write a header line, pass the fd, then close the
|
|
13
|
+
* parent's copy after spawn.
|
|
14
|
+
* - readiness policy: poll `isDashboardRunning(port)` and resolve /
|
|
15
|
+
* reject on the first of: health-ok, port-conflict, child early
|
|
16
|
+
* exit, or `healthTimeoutMs` elapsed.
|
|
17
|
+
*
|
|
18
|
+
* Does NOT own the log-file PATH — that's caller policy. Conventions:
|
|
19
|
+
* - extension: `stdio: "ignore"`
|
|
20
|
+
* - cli (`cmdStart`): `~/.pi/dashboard/server.log`
|
|
21
|
+
* - electron: existing electron log path
|
|
22
|
+
*
|
|
23
|
+
* See change: unify-server-launch-ts-loader.
|
|
24
|
+
*/
|
|
25
|
+
import { dirname } from "node:path";
|
|
26
|
+
import {
|
|
27
|
+
closeSync,
|
|
28
|
+
mkdirSync,
|
|
29
|
+
openSync,
|
|
30
|
+
writeSync,
|
|
31
|
+
} from "node:fs";
|
|
32
|
+
import type { ChildProcess, SpawnOptions } from "node:child_process"; // ban:child_process-ok — types only
|
|
33
|
+
import { spawnNodeScript } from "./platform/node-spawn.js";
|
|
34
|
+
import { ToolResolver } from "./platform/binary-lookup.js";
|
|
35
|
+
import { isDashboardRunning } from "./server-identity.js";
|
|
36
|
+
|
|
37
|
+
// ── Errors ──────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** No jiti install resolved at any anchor. */
|
|
40
|
+
export class JitiNotFoundError extends Error {
|
|
41
|
+
constructor(message =
|
|
42
|
+
"Cannot find pi's TypeScript loader (jiti). " +
|
|
43
|
+
"Is @earendil-works/pi-coding-agent or @mariozechner/pi-coding-agent installed?",
|
|
44
|
+
) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "JitiNotFoundError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Target port is occupied by a non-dashboard service. */
|
|
51
|
+
export class PortConflictError extends Error {
|
|
52
|
+
readonly port: number;
|
|
53
|
+
constructor(port: number) {
|
|
54
|
+
super(`Port ${port} is occupied by a non-dashboard service`);
|
|
55
|
+
this.name = "PortConflictError";
|
|
56
|
+
this.port = port;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Spawned child exited before reaching health-ok. */
|
|
61
|
+
export class EarlyExitError extends Error {
|
|
62
|
+
readonly code: number | null;
|
|
63
|
+
readonly signal: NodeJS.Signals | null;
|
|
64
|
+
constructor(code: number | null, signal: NodeJS.Signals | null = null) {
|
|
65
|
+
super(`Server child exited (code=${code}, signal=${signal}) before reaching health`);
|
|
66
|
+
this.name = "EarlyExitError";
|
|
67
|
+
this.code = code;
|
|
68
|
+
this.signal = signal;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Options + result ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export interface LaunchOpts {
|
|
75
|
+
/** Path to node binary. Defaults to `process.execPath`. */
|
|
76
|
+
nodeBin?: string;
|
|
77
|
+
/** Path to the dashboard server CLI script. */
|
|
78
|
+
cliPath: string;
|
|
79
|
+
/** Args appended after the entry script (e.g. `--port`, `--pi-port`, `start`). */
|
|
80
|
+
extraArgs?: readonly string[];
|
|
81
|
+
/** Caller-supplied jiti-resolution anchor (e.g. cliPath inside a node_modules tree). */
|
|
82
|
+
anchor?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Caller env overrides merged ON TOP of `ToolResolver.buildSpawnEnv()`.
|
|
85
|
+
* Conflicting keys: caller wins. Pass `DASHBOARD_STARTER` here.
|
|
86
|
+
* Omit to fall back to the resolver-merged process env.
|
|
87
|
+
*/
|
|
88
|
+
env?: Record<string, string | undefined>;
|
|
89
|
+
/**
|
|
90
|
+
* Stdio routing. `"ignore"` for fire-and-forget (extension); a
|
|
91
|
+
* `{ logFile }` object for caller-owned append-mode log capture.
|
|
92
|
+
*/
|
|
93
|
+
stdio: "ignore" | { logFile: string };
|
|
94
|
+
/**
|
|
95
|
+
* Optional starter label written to the log header line and (when
|
|
96
|
+
* present) injected as `DASHBOARD_STARTER` env var if `env` does not
|
|
97
|
+
* already supply it. Plain string ("Bridge", "Standalone", "Electron").
|
|
98
|
+
*/
|
|
99
|
+
starter?: string;
|
|
100
|
+
/** Health-check timeout in milliseconds. */
|
|
101
|
+
healthTimeoutMs: number;
|
|
102
|
+
/** Port to probe via `isDashboardRunning(port)`. */
|
|
103
|
+
port: number;
|
|
104
|
+
/**
|
|
105
|
+
* Whether the spawned server detaches from the parent's process
|
|
106
|
+
* group / Windows Job Object. Default: `true` (server outlives the
|
|
107
|
+
* launcher — correct for Bridge auto-spawn and Standalone CLI).
|
|
108
|
+
*
|
|
109
|
+
* Pass `false` when the caller deliberately ties the server's
|
|
110
|
+
* lifecycle to its own (Electron — server should die when Electron
|
|
111
|
+
* quits unless Electron explicitly decides to keep it).
|
|
112
|
+
*/
|
|
113
|
+
detach?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Working directory for the spawned process. Defaults to the
|
|
116
|
+
* launcher's own cwd. Electron passes the project directory.
|
|
117
|
+
*/
|
|
118
|
+
cwd?: string;
|
|
119
|
+
// ── Test seams (production omits) ────────────────────────────────────────
|
|
120
|
+
/** Replace `ToolResolver.resolveJiti` (returns loader URL or null). */
|
|
121
|
+
_resolveJiti?: () => string | null;
|
|
122
|
+
/** Replace `spawnNodeScript` (returns ChildProcess). */
|
|
123
|
+
_spawnNodeScript?: typeof spawnNodeScript;
|
|
124
|
+
/** Replace `isDashboardRunning`. */
|
|
125
|
+
_isDashboardRunning?: typeof isDashboardRunning;
|
|
126
|
+
/** Replace fs primitives used for log-file handling. */
|
|
127
|
+
_fs?: {
|
|
128
|
+
mkdirSync?: typeof mkdirSync;
|
|
129
|
+
openSync?: typeof openSync;
|
|
130
|
+
closeSync?: typeof closeSync;
|
|
131
|
+
writeSync?: typeof writeSync;
|
|
132
|
+
};
|
|
133
|
+
/** Override poll interval (ms). Default 300. */
|
|
134
|
+
_pollIntervalMs?: number;
|
|
135
|
+
/** Override `Date.now` for deterministic timeout testing. */
|
|
136
|
+
_now?: () => number;
|
|
137
|
+
/** Override the sleep function used between polls. */
|
|
138
|
+
_sleep?: (ms: number) => Promise<void>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface LaunchResult {
|
|
142
|
+
/** Spawned process pid (always present once spawn succeeded). */
|
|
143
|
+
childPid: number;
|
|
144
|
+
/** PID reported by `/api/health` (matches `dashboard.pid`); null if unavailable. */
|
|
145
|
+
reportedPid: number | null;
|
|
146
|
+
/** Always true when this resolves — readiness was confirmed. */
|
|
147
|
+
healthOk: true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Implementation ──────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const DEFAULT_POLL_MS = 300;
|
|
153
|
+
|
|
154
|
+
function defaultSleep(ms: number): Promise<void> {
|
|
155
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Filter out `undefined` values from an env-record (NodeJS.ProcessEnv
|
|
160
|
+
* tolerates undefined; child_process.spawn does not).
|
|
161
|
+
*/
|
|
162
|
+
function compactEnv(base: NodeJS.ProcessEnv): Record<string, string> {
|
|
163
|
+
const out: Record<string, string> = {};
|
|
164
|
+
for (const [k, v] of Object.entries(base)) {
|
|
165
|
+
if (typeof v === "string") out[k] = v;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Spawn the dashboard server and wait for `/api/health` to confirm
|
|
172
|
+
* identity. Resolves with `{ childPid, reportedPid, healthOk: true }`
|
|
173
|
+
* on success; rejects with `JitiNotFoundError` / `PortConflictError`
|
|
174
|
+
* / `EarlyExitError` / readiness-timeout `Error` per the spec.
|
|
175
|
+
*/
|
|
176
|
+
export async function launchDashboardServer(opts: LaunchOpts): Promise<LaunchResult> {
|
|
177
|
+
const nodeBin = opts.nodeBin ?? process.execPath;
|
|
178
|
+
const resolveJiti = opts._resolveJiti ?? (() => new ToolResolver({ processExecPath: nodeBin }).resolveJiti({ anchor: opts.anchor }));
|
|
179
|
+
const spawn = opts._spawnNodeScript ?? spawnNodeScript;
|
|
180
|
+
const probe = opts._isDashboardRunning ?? isDashboardRunning;
|
|
181
|
+
const pollIntervalMs = opts._pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
182
|
+
const now = opts._now ?? Date.now;
|
|
183
|
+
const sleep = opts._sleep ?? defaultSleep;
|
|
184
|
+
const fsMkdir = opts._fs?.mkdirSync ?? mkdirSync;
|
|
185
|
+
const fsOpen = opts._fs?.openSync ?? openSync;
|
|
186
|
+
const fsClose = opts._fs?.closeSync ?? closeSync;
|
|
187
|
+
const fsWrite = opts._fs?.writeSync ?? writeSync;
|
|
188
|
+
|
|
189
|
+
// 1. Loader resolution.
|
|
190
|
+
const loader = resolveJiti();
|
|
191
|
+
if (!loader) throw new JitiNotFoundError();
|
|
192
|
+
|
|
193
|
+
// 2. Env: ToolResolver.buildSpawnEnv() merged with caller env (caller wins).
|
|
194
|
+
const baseEnv = new ToolResolver({ processExecPath: nodeBin }).buildSpawnEnv(process.env);
|
|
195
|
+
const env: Record<string, string> = compactEnv(baseEnv);
|
|
196
|
+
if (opts.starter && !(opts.env && "DASHBOARD_STARTER" in opts.env)) {
|
|
197
|
+
env["DASHBOARD_STARTER"] = opts.starter;
|
|
198
|
+
}
|
|
199
|
+
if (opts.env) {
|
|
200
|
+
for (const [k, v] of Object.entries(opts.env)) {
|
|
201
|
+
if (typeof v === "string") env[k] = v;
|
|
202
|
+
else if (v === undefined) delete env[k];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 3. Stdio + log header.
|
|
207
|
+
let logFd: number | undefined;
|
|
208
|
+
let stdio: SpawnOptions["stdio"];
|
|
209
|
+
if (opts.stdio === "ignore") {
|
|
210
|
+
stdio = "ignore";
|
|
211
|
+
} else {
|
|
212
|
+
const { logFile } = opts.stdio;
|
|
213
|
+
fsMkdir(dirname(logFile), { recursive: true });
|
|
214
|
+
logFd = fsOpen(logFile, "a");
|
|
215
|
+
const header = `[${new Date().toISOString()}] ${opts.starter ?? "dashboard"} launch (parent pid ${process.pid}, port ${opts.port}, cli ${opts.cliPath})\n`;
|
|
216
|
+
try { fsWrite(logFd, header); } catch { /* best-effort */ }
|
|
217
|
+
stdio = ["ignore", logFd, logFd];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 4. Spawn. spawnNodeScript handles --import URL-wrapping + entry rule.
|
|
221
|
+
let child: ChildProcess;
|
|
222
|
+
try {
|
|
223
|
+
child = spawn({
|
|
224
|
+
nodeBin,
|
|
225
|
+
loader,
|
|
226
|
+
entry: opts.cliPath,
|
|
227
|
+
args: opts.extraArgs ? [...opts.extraArgs] : undefined,
|
|
228
|
+
spawnOptions: {
|
|
229
|
+
detached: opts.detach ?? true,
|
|
230
|
+
stdio,
|
|
231
|
+
env,
|
|
232
|
+
cwd: opts.cwd,
|
|
233
|
+
windowsHide: true,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
} finally {
|
|
237
|
+
// Always close the parent's copy of the log fd; the child has its own.
|
|
238
|
+
if (logFd !== undefined) {
|
|
239
|
+
try { fsClose(logFd); } catch { /* ignore */ }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try { child.unref(); } catch { /* ignore */ }
|
|
244
|
+
|
|
245
|
+
if (!child.pid) {
|
|
246
|
+
throw new EarlyExitError(child.exitCode ?? null, child.signalCode ?? null);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 5. Readiness loop.
|
|
250
|
+
const deadline = now() + opts.healthTimeoutMs;
|
|
251
|
+
while (true) {
|
|
252
|
+
// Early-exit detection (beats timeout per spec).
|
|
253
|
+
if (child.exitCode !== null) {
|
|
254
|
+
throw new EarlyExitError(child.exitCode, child.signalCode ?? null);
|
|
255
|
+
}
|
|
256
|
+
let status;
|
|
257
|
+
try {
|
|
258
|
+
status = await probe(opts.port);
|
|
259
|
+
} catch {
|
|
260
|
+
status = { running: false } as const;
|
|
261
|
+
}
|
|
262
|
+
if (status.running) {
|
|
263
|
+
return {
|
|
264
|
+
childPid: child.pid,
|
|
265
|
+
reportedPid: status.pid ?? null,
|
|
266
|
+
healthOk: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (status.portConflict) {
|
|
270
|
+
throw new PortConflictError(opts.port);
|
|
271
|
+
}
|
|
272
|
+
if (now() >= deadline) {
|
|
273
|
+
throw new Error("readiness timeout");
|
|
274
|
+
}
|
|
275
|
+
await sleep(pollIntervalMs);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill block parser & builder.
|
|
3
3
|
*
|
|
4
|
-
* Pi's `_expandSkillCommand` (in `@
|
|
4
|
+
* Pi's `_expandSkillCommand` (in `@earendil-works/pi-coding-agent`) wraps skill
|
|
5
5
|
* expansions in a `<skill name="..." location="...">…</skill>\n\nargs` envelope.
|
|
6
6
|
* The dashboard's bridge expander (`packages/extension/src/prompt-expander.ts`)
|
|
7
7
|
* aligns to the same byte format. This module is the single source of truth for
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration and resolution tests for the `pi-ai` module-kind tool.
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - Registry resolves pi-ai when ~/.pi-dashboard/node_modules/@mariozechner/pi-ai/dist/index.js exists (managed)
|
|
6
|
+
* - Falls back to npmGlobalStrategy when only globally installed
|
|
7
|
+
* - Returns failed resolution with diagnostic trail when none match
|
|
8
|
+
* - Override takes precedence
|
|
9
|
+
*
|
|
10
|
+
* Note: bareImportStrategy uses real module resolution (createRequire) and
|
|
11
|
+
* cannot be injected via StrategyDeps.resolveModule in moduleDefWithAliases.
|
|
12
|
+
* It is implicitly tested (fails gracefully when pi-ai isn't a project dep).
|
|
13
|
+
*
|
|
14
|
+
* See change: add-dashboard-model-proxy (task 2.1).
|
|
15
|
+
*/
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { describe, expect, it } from "vitest";
|
|
19
|
+
import {
|
|
20
|
+
ToolRegistry,
|
|
21
|
+
registerDefaultTools,
|
|
22
|
+
OverridesStore,
|
|
23
|
+
} from "../index.js";
|
|
24
|
+
|
|
25
|
+
const HOME = os.homedir();
|
|
26
|
+
const MANAGED_PATH = path.join(
|
|
27
|
+
HOME,
|
|
28
|
+
".pi-dashboard",
|
|
29
|
+
"node_modules",
|
|
30
|
+
"@mariozechner",
|
|
31
|
+
"pi-ai",
|
|
32
|
+
"dist",
|
|
33
|
+
"index.js",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function freshRegistry(opts: {
|
|
37
|
+
exists?: (p: string) => boolean;
|
|
38
|
+
which?: (name: string) => string | null;
|
|
39
|
+
npmRootGlobal?: () => string;
|
|
40
|
+
overrides?: Record<string, string>;
|
|
41
|
+
}) {
|
|
42
|
+
const store = new OverridesStore({
|
|
43
|
+
filePath: path.join(os.tmpdir(), `pi-ai-test-${Math.random()}.json`),
|
|
44
|
+
warn: () => {},
|
|
45
|
+
});
|
|
46
|
+
for (const [k, v] of Object.entries(opts.overrides ?? {})) store.set(k, v);
|
|
47
|
+
|
|
48
|
+
const r = new ToolRegistry({
|
|
49
|
+
overrides: store,
|
|
50
|
+
platform: "linux",
|
|
51
|
+
});
|
|
52
|
+
registerDefaultTools(r, {
|
|
53
|
+
exists: opts.exists ?? (() => false),
|
|
54
|
+
which: opts.which ?? (() => null),
|
|
55
|
+
npmRootGlobal: opts.npmRootGlobal ?? (() => ""),
|
|
56
|
+
});
|
|
57
|
+
return r;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("pi-ai: module registration", () => {
|
|
61
|
+
it("resolves via managed path when ~/.pi-dashboard/node_modules/@mariozechner/pi-ai exists", () => {
|
|
62
|
+
const r = freshRegistry({
|
|
63
|
+
exists: (p) => p === MANAGED_PATH,
|
|
64
|
+
});
|
|
65
|
+
const result = r.resolve("pi-ai");
|
|
66
|
+
expect(result.ok).toBe(true);
|
|
67
|
+
expect(result.path).toBe(MANAGED_PATH);
|
|
68
|
+
expect(result.source).toBe("managed");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("falls back to npm-global when only globally installed", () => {
|
|
72
|
+
const globalRoot = "/usr/lib/node_modules";
|
|
73
|
+
const globalPath = path.join(
|
|
74
|
+
globalRoot,
|
|
75
|
+
"@mariozechner",
|
|
76
|
+
"pi-ai",
|
|
77
|
+
"dist",
|
|
78
|
+
"index.js",
|
|
79
|
+
);
|
|
80
|
+
const r = freshRegistry({
|
|
81
|
+
exists: (p) => p === globalPath,
|
|
82
|
+
npmRootGlobal: () => globalRoot,
|
|
83
|
+
});
|
|
84
|
+
const result = r.resolve("pi-ai");
|
|
85
|
+
expect(result.ok).toBe(true);
|
|
86
|
+
expect(result.path).toBe(globalPath);
|
|
87
|
+
expect(result.source).toBe("npm-global");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns failed resolution with diagnostic trail when none match", () => {
|
|
91
|
+
const r = freshRegistry({});
|
|
92
|
+
const result = r.resolve("pi-ai");
|
|
93
|
+
expect(result.ok).toBe(false);
|
|
94
|
+
expect(result.tried).toBeDefined();
|
|
95
|
+
expect(result.tried!.length).toBeGreaterThan(0);
|
|
96
|
+
// Should have tried override, bare-import, managed, npm-global
|
|
97
|
+
const strategyNames = result.tried!.map((t) => t.strategy);
|
|
98
|
+
expect(strategyNames).toContain("override");
|
|
99
|
+
expect(strategyNames).toContain("bare-import");
|
|
100
|
+
expect(strategyNames).toContain("managed");
|
|
101
|
+
expect(strategyNames).toContain("npm-global");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("override takes precedence over managed", () => {
|
|
105
|
+
const overridePath = "/custom/pi-ai/dist/index.js";
|
|
106
|
+
const r = freshRegistry({
|
|
107
|
+
exists: (p) => p === overridePath || p === MANAGED_PATH,
|
|
108
|
+
overrides: { "pi-ai": overridePath },
|
|
109
|
+
});
|
|
110
|
+
const result = r.resolve("pi-ai");
|
|
111
|
+
expect(result.ok).toBe(true);
|
|
112
|
+
expect(result.path).toBe(overridePath);
|
|
113
|
+
expect(result.source).toBe("override");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("is registered and resolvable by name", () => {
|
|
117
|
+
const r = freshRegistry({
|
|
118
|
+
exists: (p) => p === MANAGED_PATH,
|
|
119
|
+
});
|
|
120
|
+
const result = r.resolve("pi-ai");
|
|
121
|
+
expect(result.name).toBe("pi-ai");
|
|
122
|
+
expect(result.ok).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -60,7 +60,7 @@ function binaryDef(binaryName: string, deps?: StrategyDeps): ToolDefinition {
|
|
|
60
60
|
|
|
61
61
|
// ── Module definitions ──────────────────────────────────────────────────────
|
|
62
62
|
|
|
63
|
-
/** Sibling probe for an aliased package name (pi: `@
|
|
63
|
+
/** Sibling probe for an aliased package name (pi: `@earendil-works/*` + `@mariozechner/*`). */
|
|
64
64
|
function moduleDefWithAliases(
|
|
65
65
|
canonicalName: string,
|
|
66
66
|
pkgNames: readonly string[],
|
|
@@ -188,7 +188,7 @@ const nodeScriptToArgv: ToolDefinition["toArgv"] = (resolvedPath, { platform, re
|
|
|
188
188
|
* On Unix, the chain finds `pi` on PATH; argv = [pi].
|
|
189
189
|
*/
|
|
190
190
|
function piExecutorDef(deps?: StrategyDeps): ToolDefinition {
|
|
191
|
-
const piPkgAliases = ["@
|
|
191
|
+
const piPkgAliases = ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"];
|
|
192
192
|
const cliEntry = path.join("dist", "cli.js");
|
|
193
193
|
|
|
194
194
|
const winStrategies = [
|
|
@@ -413,7 +413,19 @@ export function registerDefaultTools(registry: ToolRegistry, deps?: StrategyDeps
|
|
|
413
413
|
registry.register(
|
|
414
414
|
moduleDefWithAliases(
|
|
415
415
|
"pi-coding-agent",
|
|
416
|
-
["@
|
|
416
|
+
["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"],
|
|
417
|
+
path.join("dist", "index.js"),
|
|
418
|
+
deps,
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// pi-ai module — used by model-proxy to call upstream LLM providers.
|
|
423
|
+
// Aliases: @earendil-works/pi-ai (preferred) + @mariozechner/pi-ai (legacy fallback).
|
|
424
|
+
// See change: add-dashboard-model-proxy.
|
|
425
|
+
registry.register(
|
|
426
|
+
moduleDefWithAliases(
|
|
427
|
+
"pi-ai",
|
|
428
|
+
["@earendil-works/pi-ai", "@mariozechner/pi-ai"],
|
|
417
429
|
path.join("dist", "index.js"),
|
|
418
430
|
deps,
|
|
419
431
|
),
|
|
@@ -404,6 +404,40 @@ export interface OpenSpecChange {
|
|
|
404
404
|
* "Archive anyway" escape hatch when artifacts are authored but tasks remain unchecked.
|
|
405
405
|
*/
|
|
406
406
|
isComplete?: boolean;
|
|
407
|
+
/**
|
|
408
|
+
* Group assignment joined server-side from `<cwd>/openspec/groups/groups.json`.
|
|
409
|
+
* `null` or absent means Ungrouped. Clients SHALL NOT recompute the join.
|
|
410
|
+
* See change: add-openspec-change-grouping.
|
|
411
|
+
*/
|
|
412
|
+
groupId?: string | null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Schema version for the per-repo OpenSpec groups file at
|
|
416
|
+
* `<cwd>/openspec/groups/groups.json`. Bumped only on incompatible shape changes.
|
|
417
|
+
* See change: add-openspec-change-grouping. */
|
|
418
|
+
export const OPENSPEC_GROUPS_SCHEMA_VERSION = 1 as const;
|
|
419
|
+
|
|
420
|
+
/** A user-defined group of OpenSpec changes within a single repo.
|
|
421
|
+
* See change: add-openspec-change-grouping. */
|
|
422
|
+
export interface OpenSpecGroup {
|
|
423
|
+
/** Server-generated slug from `name` plus collision suffix. Stable across rename. */
|
|
424
|
+
id: string;
|
|
425
|
+
/** User-visible label; editable. */
|
|
426
|
+
name: string;
|
|
427
|
+
/** Optional CSS hex color (`#RRGGBB`). Clients fall back to a default palette when omitted. */
|
|
428
|
+
color?: string;
|
|
429
|
+
/** Display order; server keeps values contiguous `0..groups.length - 1` after every reorder. */
|
|
430
|
+
order: number;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Shape of the on-disk groups file at `<cwd>/openspec/groups/groups.json`.
|
|
434
|
+
* Single combined file for groups + assignments — one read, one write, atomic.
|
|
435
|
+
* See change: add-openspec-change-grouping. */
|
|
436
|
+
export interface OpenSpecGroupsFile {
|
|
437
|
+
schemaVersion: number;
|
|
438
|
+
groups: OpenSpecGroup[];
|
|
439
|
+
/** `changeName` → `groupId`. Unassigned changes have no entry. */
|
|
440
|
+
assignments: Record<string, string>;
|
|
407
441
|
}
|
|
408
442
|
|
|
409
443
|
/** Lifecycle state of an OpenSpec change, derived from artifacts + task status */
|
|
@@ -425,6 +459,12 @@ export function deriveChangeState(change: OpenSpecChange): ChangeState {
|
|
|
425
459
|
|
|
426
460
|
/** OpenSpec data for a session's project */
|
|
427
461
|
export interface OpenSpecData {
|
|
462
|
+
/**
|
|
463
|
+
* `openspec list` returned authoritative data for this cwd. Requires both
|
|
464
|
+
* `<cwd>/openspec/` AND `<cwd>/openspec/changes/` to exist AND the CLI to
|
|
465
|
+
* succeed. Does NOT distinguish "openspec project, no changes yet" from
|
|
466
|
+
* "truly not an openspec project" — see `hasOpenspecDir` for that.
|
|
467
|
+
*/
|
|
428
468
|
initialized: boolean;
|
|
429
469
|
changes: OpenSpecChange[];
|
|
430
470
|
/**
|
|
@@ -440,6 +480,21 @@ export interface OpenSpecData {
|
|
|
440
480
|
* See change: fix-cold-boot-openspec-protocol.
|
|
441
481
|
*/
|
|
442
482
|
pending?: boolean;
|
|
483
|
+
/**
|
|
484
|
+
* Whether `<cwd>/openspec/` directory exists. Strictly weaker than
|
|
485
|
+
* `initialized`: this can be `true` while `initialized` is `false` when
|
|
486
|
+
* the project is OpenSpec-initialized (`openspec init` was run) but
|
|
487
|
+
* `openspec/changes/` doesn't exist yet (no proposals authored). In that
|
|
488
|
+
* case `openspec list` errors out and `initialized` stays `false`, but
|
|
489
|
+
* the session card should still show the OPENSPEC subcard as an
|
|
490
|
+
* init/attach affordance.
|
|
491
|
+
*
|
|
492
|
+
* Optional for backwards compatibility — absence means "unknown, fall
|
|
493
|
+
* back to `initialized || pending`" on the client side.
|
|
494
|
+
*
|
|
495
|
+
* See change: auto-hide-empty-session-subcards.
|
|
496
|
+
*/
|
|
497
|
+
hasOpenspecDir?: boolean;
|
|
443
498
|
}
|
|
444
499
|
|
|
445
500
|
/** OpenSpec workflow phase detected from tool calls */
|
|
@@ -649,4 +704,11 @@ export interface ApiResponse<T = unknown> {
|
|
|
649
704
|
success: boolean;
|
|
650
705
|
data?: T;
|
|
651
706
|
error?: string;
|
|
707
|
+
/**
|
|
708
|
+
* Optional structured failure-classifier code paired with `error`.
|
|
709
|
+
* Lets clients render specific UI for known failure modes
|
|
710
|
+
* (e.g., `"FORK_EMPTY_SESSION"`).
|
|
711
|
+
* See change: fix-fork-empty-session-silent-timeout.
|
|
712
|
+
*/
|
|
713
|
+
code?: string;
|
|
652
714
|
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { buildJitiRegisterUrl, resolveJitiImport } from "../resolve-jiti.js";
|
|
3
|
-
|
|
4
|
-
describe("buildJitiRegisterUrl", () => {
|
|
5
|
-
// Pure function: given a jiti package.json path, return the file:// URL of
|
|
6
|
-
// its register hook. The URL contract is the critical invariant — Node's
|
|
7
|
-
// --import on Windows rejects raw drive-letter paths (parses "C:" as a
|
|
8
|
-
// URL scheme). See change: fix-windows-server-parity.
|
|
9
|
-
|
|
10
|
-
it("returns a file:// URL", () => {
|
|
11
|
-
const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
|
|
12
|
-
expect(url.startsWith("file://")).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("URL is parseable by new URL() without throwing", () => {
|
|
16
|
-
const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
|
|
17
|
-
expect(() => new URL(url)).not.toThrow();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("points at lib/jiti-register.mjs under the package dir", () => {
|
|
21
|
-
const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
|
|
22
|
-
expect(url.endsWith("/lib/jiti-register.mjs")).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("handles Windows drive-letter paths (regression for ERR_UNSUPPORTED_ESM_URL_SCHEME)", () => {
|
|
26
|
-
// This is the exact shape that crashed pre-fix: a raw path with a
|
|
27
|
-
// drive letter was passed to `node --import` and Node parsed "B:" as
|
|
28
|
-
// a URL scheme. A file:// URL sidesteps the parser entirely.
|
|
29
|
-
const url = buildJitiRegisterUrl("B:\\Dev\\Nodejs\\global\\node_modules\\@mariozechner\\jiti\\package.json");
|
|
30
|
-
expect(url.startsWith("file:///")).toBe(true);
|
|
31
|
-
expect(() => new URL(url)).not.toThrow();
|
|
32
|
-
expect(new URL(url).protocol).toBe("file:");
|
|
33
|
-
// The drive letter survives as part of the pathname, not as a protocol
|
|
34
|
-
expect(url.toLowerCase()).toContain("/b:/");
|
|
35
|
-
expect(url.endsWith("/lib/jiti-register.mjs")).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("resolveJitiImport", () => {
|
|
41
|
-
// Integration-lite: in vitest context (not inside pi's jiti loader),
|
|
42
|
-
// process.argv[1] points at the test runner, not pi — so peer-dep
|
|
43
|
-
// resolution fails and the function throws a helpful error. The
|
|
44
|
-
// URL-contract behavior is covered by buildJitiRegisterUrl above.
|
|
45
|
-
|
|
46
|
-
it("throws with clear error when pi-coding-agent is not resolvable", () => {
|
|
47
|
-
expect(() => resolveJitiImport()).toThrow("Cannot find pi's TypeScript loader");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("error message mentions pi-coding-agent", () => {
|
|
51
|
-
expect(() => resolveJitiImport()).toThrow("pi-coding-agent");
|
|
52
|
-
});
|
|
53
|
-
});
|