@blackbelt-technology/pi-agent-dashboard 0.5.1 → 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 +30 -0
- package/docs/architecture.md +129 -1
- package/package.json +6 -6
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +10 -8
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/bridge-context.ts +67 -3
- package/packages/extension/src/bridge.ts +20 -8
- package/packages/extension/src/command-handler.ts +36 -13
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +6 -5
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +12 -18
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service.test.ts +1 -1
- 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__/headless-pid-registry.test.ts +233 -0
- 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__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- 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 +15 -7
- package/packages/server/src/browser-handlers/session-action-handler.ts +30 -4
- package/packages/server/src/cli.ts +61 -81
- 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 +31 -1
- package/packages/server/src/headless-pid-registry.ts +299 -41
- 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/process-manager.ts +128 -0
- package/packages/server/src/provider-auth-storage.ts +29 -47
- 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/provider-auth-routes.ts +3 -0
- package/packages/server/src/routes/provider-routes.ts +24 -1
- 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 +178 -2
- package/packages/server/src/session-api.ts +9 -1
- 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__/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.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/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +27 -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 +42 -5
- package/packages/shared/src/protocol.ts +19 -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/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/types.ts +55 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -184
- package/packages/shared/src/resolve-jiti.ts +0 -155
|
@@ -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 */
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
buildJitiRegisterUrl,
|
|
4
|
-
resolveJitiImport,
|
|
5
|
-
pickJitiRegisterUrl,
|
|
6
|
-
pickJitiFromAnchor,
|
|
7
|
-
JITI_PACKAGES,
|
|
8
|
-
} from "../resolve-jiti.js";
|
|
9
|
-
|
|
10
|
-
describe("buildJitiRegisterUrl", () => {
|
|
11
|
-
// Pure function: given a jiti package.json path, return the file:// URL of
|
|
12
|
-
// its register hook. The URL contract is the critical invariant — Node's
|
|
13
|
-
// --import on Windows rejects raw drive-letter paths (parses "C:" as a
|
|
14
|
-
// URL scheme). See change: fix-windows-server-parity.
|
|
15
|
-
|
|
16
|
-
it("returns a file:// URL", () => {
|
|
17
|
-
const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
|
|
18
|
-
expect(url.startsWith("file://")).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("URL is parseable by new URL() without throwing", () => {
|
|
22
|
-
const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
|
|
23
|
-
expect(() => new URL(url)).not.toThrow();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("points at lib/jiti-register.mjs under the package dir", () => {
|
|
27
|
-
const url = buildJitiRegisterUrl("/usr/lib/node_modules/@mariozechner/jiti/package.json");
|
|
28
|
-
expect(url.endsWith("/lib/jiti-register.mjs")).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("handles Windows drive-letter paths (regression for ERR_UNSUPPORTED_ESM_URL_SCHEME)", () => {
|
|
32
|
-
// This is the exact shape that crashed pre-fix: a raw path with a
|
|
33
|
-
// drive letter was passed to `node --import` and Node parsed "B:" as
|
|
34
|
-
// a URL scheme. A file:// URL sidesteps the parser entirely.
|
|
35
|
-
const url = buildJitiRegisterUrl("B:\\Dev\\Nodejs\\global\\node_modules\\@mariozechner\\jiti\\package.json");
|
|
36
|
-
expect(url.startsWith("file:///")).toBe(true);
|
|
37
|
-
expect(() => new URL(url)).not.toThrow();
|
|
38
|
-
expect(new URL(url).protocol).toBe("file:");
|
|
39
|
-
// The drive letter survives as part of the pathname, not as a protocol
|
|
40
|
-
expect(url.toLowerCase()).toContain("/b:/");
|
|
41
|
-
expect(url.endsWith("/lib/jiti-register.mjs")).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("resolveJitiImport", () => {
|
|
47
|
-
// Integration-lite: behaviour depends on what's resolvable from
|
|
48
|
-
// process.argv[1] (the vitest runner). Two valid outcomes:
|
|
49
|
-
// (a) vitest's own transitive `jiti` dep resolves → returns a URL.
|
|
50
|
-
// (b) nothing resolves → throws the documented error.
|
|
51
|
-
// The URL-contract behaviour is covered by buildJitiRegisterUrl above
|
|
52
|
-
// and the lookup-order behaviour by pickJitiRegisterUrl below. This
|
|
53
|
-
// describe block exercises only the runtime-anchor branch.
|
|
54
|
-
|
|
55
|
-
it("either returns a file:// URL or throws the documented error", () => {
|
|
56
|
-
let result: string | undefined;
|
|
57
|
-
let err: Error | undefined;
|
|
58
|
-
try {
|
|
59
|
-
result = resolveJitiImport();
|
|
60
|
-
} catch (e) {
|
|
61
|
-
err = e as Error;
|
|
62
|
-
}
|
|
63
|
-
if (result !== undefined) {
|
|
64
|
-
expect(result.startsWith("file://")).toBe(true);
|
|
65
|
-
expect(result.endsWith("/lib/jiti-register.mjs")).toBe(true);
|
|
66
|
-
} else {
|
|
67
|
-
expect(err).toBeDefined();
|
|
68
|
-
expect(err!.message).toContain("Cannot find pi's TypeScript loader");
|
|
69
|
-
expect(err!.message).toContain("pi-coding-agent");
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("JITI_PACKAGES contract", () => {
|
|
75
|
-
// The lookup-order contract: upstream first, legacy fork as fallback.
|
|
76
|
-
// Pinned so a future contributor doesn't accidentally re-order and
|
|
77
|
-
// silently change resolution priority for users mid-migration.
|
|
78
|
-
// See change: pi-fork migration to @earendil-works (drops @oh-my-pi).
|
|
79
|
-
it("contains the supported provider names in lookup order", () => {
|
|
80
|
-
expect(JITI_PACKAGES).toEqual(["jiti", "@mariozechner/jiti"]);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("pickJitiRegisterUrl (test seam)", () => {
|
|
85
|
-
// Mock a Node-style resolver. Returns a path when the spec matches
|
|
86
|
-
// a configured "installed" package; throws like Node's resolve() does
|
|
87
|
-
// for unfound modules.
|
|
88
|
-
function makeResolver(installed: Record<string, string>) {
|
|
89
|
-
return (spec: string): string => {
|
|
90
|
-
if (spec in installed) return installed[spec];
|
|
91
|
-
throw new Error(`Cannot find module '${spec}'`);
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
it("returns @mariozechner/jiti's URL when only the legacy fork is installed", () => {
|
|
96
|
-
const resolver = makeResolver({
|
|
97
|
-
"@mariozechner/jiti/package.json": "/r/node_modules/@mariozechner/jiti/package.json",
|
|
98
|
-
});
|
|
99
|
-
const url = pickJitiRegisterUrl(resolver);
|
|
100
|
-
expect(url).not.toBeNull();
|
|
101
|
-
expect(url!).toContain("@mariozechner/jiti");
|
|
102
|
-
expect(url!.endsWith("/lib/jiti-register.mjs")).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("returns upstream jiti's URL when only upstream is installed (pi 0.73.1+)", () => {
|
|
106
|
-
const resolver = makeResolver({
|
|
107
|
-
"jiti/package.json": "/r/node_modules/jiti/package.json",
|
|
108
|
-
});
|
|
109
|
-
const url = pickJitiRegisterUrl(resolver);
|
|
110
|
-
expect(url).not.toBeNull();
|
|
111
|
-
// Match `/jiti/lib/...` but NOT `/@mariozechner/jiti/lib/...`
|
|
112
|
-
expect(url!).toMatch(/\/jiti\/lib\/jiti-register\.mjs$/);
|
|
113
|
-
expect(url!).not.toContain("@mariozechner");
|
|
114
|
-
expect(url!).not.toContain("@oh-my-pi");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("prefers upstream jiti when BOTH upstream and legacy fork are present", () => {
|
|
118
|
-
const calls: string[] = [];
|
|
119
|
-
const resolver = (spec: string): string => {
|
|
120
|
-
calls.push(spec);
|
|
121
|
-
if (
|
|
122
|
-
spec === "@mariozechner/jiti/package.json" ||
|
|
123
|
-
spec === "jiti/package.json"
|
|
124
|
-
) {
|
|
125
|
-
return `/r/node_modules/${spec}`;
|
|
126
|
-
}
|
|
127
|
-
throw new Error("nope");
|
|
128
|
-
};
|
|
129
|
-
const url = pickJitiRegisterUrl(resolver);
|
|
130
|
-
// Match `/jiti/lib/...` but NOT `/@mariozechner/jiti/lib/...`
|
|
131
|
-
expect(url!).toMatch(/\/jiti\/lib\/jiti-register\.mjs$/);
|
|
132
|
-
expect(url!).not.toContain("@mariozechner");
|
|
133
|
-
// Crucially: the resolver was NOT asked for the legacy fork because
|
|
134
|
-
// upstream won first.
|
|
135
|
-
expect(calls).toEqual(["jiti/package.json"]);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("returns null when no provider resolves", () => {
|
|
139
|
-
const resolver = makeResolver({});
|
|
140
|
-
expect(pickJitiRegisterUrl(resolver)).toBeNull();
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe("pickJitiFromAnchor (test seam)", () => {
|
|
145
|
-
function makeResolver(installed: Record<string, string>) {
|
|
146
|
-
return (spec: string): string => {
|
|
147
|
-
if (spec in installed) return installed[spec];
|
|
148
|
-
throw new Error(`Cannot find module '${spec}'`);
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
it("returns upstream jiti's URL when only upstream is on the anchor's chain", () => {
|
|
153
|
-
const resolver = makeResolver({
|
|
154
|
-
"jiti/package.json": "/anchor/node_modules/jiti/package.json",
|
|
155
|
-
});
|
|
156
|
-
const pathExists = (p: string): boolean =>
|
|
157
|
-
p === "/anchor/node_modules/jiti/lib/jiti-register.mjs";
|
|
158
|
-
const url = pickJitiFromAnchor(resolver, pathExists);
|
|
159
|
-
expect(url).not.toBeNull();
|
|
160
|
-
expect(url!).toMatch(/\/jiti\/lib\/jiti-register\.mjs$/);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("skips a provider whose register file does not exist on disk", () => {
|
|
164
|
-
// Resolver finds package.json but jiti-register.mjs is missing
|
|
165
|
-
// (corrupt install). Expect skip-to-next, ultimately null.
|
|
166
|
-
const resolver = makeResolver({
|
|
167
|
-
"@mariozechner/jiti/package.json":
|
|
168
|
-
"/anchor/node_modules/@mariozechner/jiti/package.json",
|
|
169
|
-
});
|
|
170
|
-
const pathExists = (): boolean => false;
|
|
171
|
-
expect(pickJitiFromAnchor(resolver, pathExists)).toBeNull();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("returns null when nothing resolves", () => {
|
|
175
|
-
expect(
|
|
176
|
-
pickJitiFromAnchor(
|
|
177
|
-
() => {
|
|
178
|
-
throw new Error("nope");
|
|
179
|
-
},
|
|
180
|
-
() => true,
|
|
181
|
-
),
|
|
182
|
-
).toBeNull();
|
|
183
|
-
});
|
|
184
|
-
});
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve the jiti register hook from pi's process context.
|
|
3
|
-
*
|
|
4
|
-
* The bridge extension runs inside pi's Node.js process. process.argv[1]
|
|
5
|
-
* points to pi's CLI entry (e.g., pi-coding-agent/dist/cli.js). Since
|
|
6
|
-
* jiti is a dependency of pi-coding-agent, createRequire(process.argv[1])
|
|
7
|
-
* can resolve it directly.
|
|
8
|
-
*
|
|
9
|
-
* Supported jiti providers, in lookup order:
|
|
10
|
-
* 1. `@mariozechner/jiti` — legacy fork shipped with pi ≤ 0.73.0.
|
|
11
|
-
* 2. `@oh-my-pi/jiti` — fork shipped with `@oh-my-pi/pi-coding-agent`.
|
|
12
|
-
* 3. `jiti` — upstream package. Pi 0.73.1+ dropped the
|
|
13
|
-
* fork in favour of upstream jiti 2.7,
|
|
14
|
-
* which ships the same `lib/jiti-register.mjs`
|
|
15
|
-
* layout the helpers below assume.
|
|
16
|
-
*
|
|
17
|
-
* Forks are tried first to preserve behaviour for users on older pi
|
|
18
|
-
* versions; upstream is the fallthrough for pi 0.73.1+. See change:
|
|
19
|
-
* support-upstream-jiti-resolution.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { createRequire } from "node:module";
|
|
23
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
24
|
-
import path from "node:path";
|
|
25
|
-
import { pathToFileURL } from "node:url";
|
|
26
|
-
|
|
27
|
-
export const JITI_PACKAGES = [
|
|
28
|
-
// @earendil-works/pi-coding-agent depends on plain `jiti`. Try the
|
|
29
|
-
// bare name first; fall back to the legacy namespaced fork.
|
|
30
|
-
"jiti",
|
|
31
|
-
"@mariozechner/jiti",
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Pure helper: given a jiti package.json path, return the file:// URL of
|
|
36
|
-
* its register hook. Exported for testing — no I/O.
|
|
37
|
-
*
|
|
38
|
-
* Returns a file:// URL (not a raw path) because Node >= 20 on Windows
|
|
39
|
-
* rejects raw absolute paths with a drive letter for --import (parses
|
|
40
|
-
* "C:" / "B:" as a URL scheme → ERR_UNSUPPORTED_ESM_URL_SCHEME). file://
|
|
41
|
-
* URLs are accepted on every OS.
|
|
42
|
-
* See change: fix-windows-server-parity.
|
|
43
|
-
*/
|
|
44
|
-
export function buildJitiRegisterUrl(pkgJsonPath: string): string {
|
|
45
|
-
// Detect Windows-style input (drive letter + backslash) regardless of
|
|
46
|
-
// host OS, so unit tests can exercise the Windows path contract on macOS/Linux.
|
|
47
|
-
// Production behaviour is unchanged because the host-OS `path`/`pathToFileURL`
|
|
48
|
-
// match the input style automatically.
|
|
49
|
-
const isWindowsStyle = /^[A-Za-z]:[\\/]/.test(pkgJsonPath);
|
|
50
|
-
if (isWindowsStyle) {
|
|
51
|
-
// Manually build file:///C:/path/lib/jiti-register.mjs — pathToFileURL on
|
|
52
|
-
// POSIX hosts URL-encodes backslashes rather than treating them as
|
|
53
|
-
// separators. Do the join with path.win32 and format the URL ourselves.
|
|
54
|
-
const registerPath = path.win32.join(path.win32.dirname(pkgJsonPath), "lib", "jiti-register.mjs");
|
|
55
|
-
return `file:///${registerPath.replace(/\\/g, "/")}`;
|
|
56
|
-
}
|
|
57
|
-
const registerPath = path.join(path.dirname(pkgJsonPath), "lib", "jiti-register.mjs");
|
|
58
|
-
return pathToFileURL(registerPath).href;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Test seam: a function that takes a package specifier (e.g.
|
|
63
|
-
* `"jiti/package.json"`) and returns the resolved path. Production
|
|
64
|
-
* supplies `createRequire(realpath(process.argv[1])).resolve`; tests
|
|
65
|
-
* supply a stub.
|
|
66
|
-
*/
|
|
67
|
-
export type JitiResolver = (specifier: string) => string;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Internal: walk the JITI_PACKAGES list using the given resolver and
|
|
71
|
-
* return the first hit's register URL. Pure function once the
|
|
72
|
-
* resolver is supplied. Returns null when no provider resolves.
|
|
73
|
-
*/
|
|
74
|
-
export function pickJitiRegisterUrl(resolver: JitiResolver): string | null {
|
|
75
|
-
for (const jiti of JITI_PACKAGES) {
|
|
76
|
-
try {
|
|
77
|
-
const pkgJson = resolver(`${jiti}/package.json`);
|
|
78
|
-
return buildJitiRegisterUrl(pkgJson);
|
|
79
|
-
} catch { /* next */ }
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Returns jiti's register hook as a file:// URL suitable for `node --import`.
|
|
86
|
-
* Uses process.argv[1] (pi's entry point) to anchor module resolution.
|
|
87
|
-
*
|
|
88
|
-
* The return value is ALWAYS a file:// URL (never a raw path). See
|
|
89
|
-
* buildJitiRegisterUrl for the URL contract rationale.
|
|
90
|
-
*/
|
|
91
|
-
export function resolveJitiImport(): string {
|
|
92
|
-
const anchor = process.argv[1];
|
|
93
|
-
if (anchor) {
|
|
94
|
-
try {
|
|
95
|
-
// Resolve symlinks — process.argv[1] may be a symlink (e.g., bin/pi → dist/cli.js)
|
|
96
|
-
const resolved = realpathSync(anchor);
|
|
97
|
-
const req = createRequire(resolved);
|
|
98
|
-
const url = pickJitiRegisterUrl((spec) => req.resolve(spec));
|
|
99
|
-
if (url) return url;
|
|
100
|
-
} catch { /* fall through */ }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
throw new Error(
|
|
104
|
-
"Cannot find pi's TypeScript loader (jiti). " +
|
|
105
|
-
"Is @earendil-works/pi-coding-agent or @mariozechner/pi-coding-agent installed?"
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Resolve jiti's register hook from an arbitrary anchor path (e.g. a
|
|
111
|
-
* pi-coding-agent package.json in a managed install, or a pi binary on
|
|
112
|
-
* the system PATH). Returns a file:// URL or null if jiti cannot be
|
|
113
|
-
* resolved from the anchor.
|
|
114
|
-
*
|
|
115
|
-
* This is the Electron/managed-install variant of `resolveJitiImport`
|
|
116
|
-
* — the difference is the caller supplies the anchor explicitly
|
|
117
|
-
* instead of using `process.argv[1]`. Consolidates what used to be a
|
|
118
|
-
* duplicate `resolveJitiFromAnchor` in
|
|
119
|
-
* `packages/electron/src/lib/server-lifecycle.ts`.
|
|
120
|
-
* See change: consolidate-platform-handlers.
|
|
121
|
-
*/
|
|
122
|
-
export function resolveJitiFromAnchor(anchorPath: string): string | null {
|
|
123
|
-
if (!existsSync(anchorPath)) return null;
|
|
124
|
-
try {
|
|
125
|
-
const req = createRequire(anchorPath);
|
|
126
|
-
for (const jiti of JITI_PACKAGES) {
|
|
127
|
-
try {
|
|
128
|
-
const pkgJson = req.resolve(`${jiti}/package.json`);
|
|
129
|
-
const registerPath = path.join(path.dirname(pkgJson), "lib", "jiti-register.mjs");
|
|
130
|
-
if (existsSync(registerPath)) return pathToFileURL(registerPath).href;
|
|
131
|
-
} catch { /* next */ }
|
|
132
|
-
}
|
|
133
|
-
} catch { /* ignore */ }
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Test seam for `resolveJitiFromAnchor`. Pure function: given a resolver
|
|
139
|
-
* and a `pathExists` predicate, walk JITI_PACKAGES and return the first
|
|
140
|
-
* hit's register URL. Production wires `createRequire(anchor).resolve`
|
|
141
|
-
* and `existsSync`; tests inject stubs.
|
|
142
|
-
*/
|
|
143
|
-
export function pickJitiFromAnchor(
|
|
144
|
-
resolver: JitiResolver,
|
|
145
|
-
pathExists: (p: string) => boolean,
|
|
146
|
-
): string | null {
|
|
147
|
-
for (const jiti of JITI_PACKAGES) {
|
|
148
|
-
try {
|
|
149
|
-
const pkgJson = resolver(`${jiti}/package.json`);
|
|
150
|
-
const registerPath = path.join(path.dirname(pkgJson), "lib", "jiti-register.mjs");
|
|
151
|
-
if (pathExists(registerPath)) return pathToFileURL(registerPath).href;
|
|
152
|
-
} catch { /* next */ }
|
|
153
|
-
}
|
|
154
|
-
return null;
|
|
155
|
-
}
|