@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +339 -190
- package/README.md +50 -7
- package/docs/architecture.md +238 -23
- package/package.json +23 -13
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
- 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.test.ts +24 -1
- 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 +1 -1
- package/packages/extension/src/bridge.ts +169 -4
- package/packages/extension/src/command-handler.ts +65 -2
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/markdown-image-inliner.ts +268 -0
- 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 +50 -2
- package/packages/extension/src/provider-register.ts +132 -8
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +18 -1
- package/packages/extension/src/session-sync.ts +15 -1
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/package.json +6 -6
- package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
- package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
- package/packages/server/src/__tests__/build-auth-status.test.ts +190 -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 +22 -4
- package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -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 +1 -1
- package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
- 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__/has-openspec-dir.test.ts +64 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
- package/packages/server/src/__tests__/health-shape.test.ts +43 -0
- package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -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-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 +236 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- 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-codes.test.ts +80 -0
- package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
- package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
- package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
- package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
- package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
- package/packages/server/src/bootstrap-install-from-list.ts +232 -0
- package/packages/server/src/bootstrap-state.ts +18 -0
- package/packages/server/src/browser-gateway.ts +70 -24
- package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
- package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
- 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 +23 -2
- package/packages/server/src/directory-service.ts +31 -0
- package/packages/server/src/event-wiring.ts +105 -5
- package/packages/server/src/headless-pid-registry.ts +54 -5
- package/packages/server/src/home-lock.d.ts +124 -0
- package/packages/server/src/home-lock.js +330 -0
- package/packages/server/src/home-lock.js.map +1 -0
- package/packages/server/src/idle-timer.ts +15 -1
- 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 +81 -15
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +10 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +115 -21
- package/packages/server/src/provider-auth-handlers.ts +9 -0
- package/packages/server/src/provider-auth-storage.ts +83 -51
- package/packages/server/src/provider-catalogue-cache.ts +47 -0
- package/packages/server/src/routes/doctor-routes.ts +140 -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 +14 -1
- package/packages/server/src/routes/provider-routes.ts +4 -4
- package/packages/server/src/routes/system-routes.ts +38 -1
- package/packages/server/src/server.ts +85 -66
- package/packages/server/src/session-api.ts +54 -3
- package/packages/server/src/session-bootstrap.ts +27 -12
- package/packages/server/src/session-discovery.ts +11 -4
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/session-scanner.ts +4 -2
- package/packages/server/src/spawn-failure-log.ts +130 -0
- package/packages/server/src/spawn-preflight.ts +82 -0
- package/packages/server/src/spawn-register-watchdog.ts +291 -0
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/terminal-manager.ts +12 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -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 +6 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config.test.ts +48 -0
- package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
- package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
- package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
- package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
- package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
- package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
- package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
- package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
- package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +197 -3
- package/packages/shared/src/browser-protocol.ts +155 -1
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +15 -0
- package/packages/shared/src/dashboard-starter.ts +33 -0
- package/packages/shared/src/doctor-core.ts +821 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/installable-list.ts +152 -0
- package/packages/shared/src/launch-source-flag.ts +14 -0
- package/packages/shared/src/launch-source-types.ts +18 -0
- package/packages/shared/src/openspec-activity-detector.ts +25 -7
- package/packages/shared/src/platform/detached-spawn.ts +13 -2
- package/packages/shared/src/platform/managed-node-path.ts +77 -0
- package/packages/shared/src/platform/node-spawn.ts +29 -21
- package/packages/shared/src/protocol.ts +54 -2
- package/packages/shared/src/resolve-jiti.ts +62 -9
- package/packages/shared/src/rest-api.ts +4 -0
- package/packages/shared/src/skill-block-parser.ts +115 -0
- package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
- package/packages/shared/src/tool-registry/definitions.ts +33 -8
- package/packages/shared/src/tool-registry/strategies.ts +42 -0
- package/packages/shared/src/types.ts +64 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
buildJitiRegisterUrl,
|
|
4
|
+
resolveJitiImport,
|
|
5
|
+
pickJitiRegisterUrl,
|
|
6
|
+
pickJitiFromAnchor,
|
|
7
|
+
JITI_PACKAGES,
|
|
8
|
+
} from "../resolve-jiti.js";
|
|
3
9
|
|
|
4
10
|
describe("buildJitiRegisterUrl", () => {
|
|
5
11
|
// Pure function: given a jiti package.json path, return the file:// URL of
|
|
@@ -38,16 +44,141 @@ describe("buildJitiRegisterUrl", () => {
|
|
|
38
44
|
});
|
|
39
45
|
|
|
40
46
|
describe("resolveJitiImport", () => {
|
|
41
|
-
// Integration-lite:
|
|
42
|
-
// process.argv[1]
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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.
|
|
45
54
|
|
|
46
|
-
it("
|
|
47
|
-
|
|
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();
|
|
48
172
|
});
|
|
49
173
|
|
|
50
|
-
it("
|
|
51
|
-
expect(
|
|
174
|
+
it("returns null when nothing resolves", () => {
|
|
175
|
+
expect(
|
|
176
|
+
pickJitiFromAnchor(
|
|
177
|
+
() => {
|
|
178
|
+
throw new Error("nope");
|
|
179
|
+
},
|
|
180
|
+
() => true,
|
|
181
|
+
),
|
|
182
|
+
).toBeNull();
|
|
52
183
|
});
|
|
53
184
|
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseSkillBlock, buildSkillBlock } from "../skill-block-parser.js";
|
|
3
|
+
|
|
4
|
+
describe("parseSkillBlock", () => {
|
|
5
|
+
it("matches a well-formed wrapper with args", () => {
|
|
6
|
+
const text =
|
|
7
|
+
`<skill name="foo" location="/x/SKILL.md">\nReferences are relative to /x.\n\nbody\n</skill>\n\nargs here`;
|
|
8
|
+
const block = parseSkillBlock(text);
|
|
9
|
+
expect(block).not.toBeNull();
|
|
10
|
+
expect(block!.name).toBe("foo");
|
|
11
|
+
expect(block!.location).toBe("/x/SKILL.md");
|
|
12
|
+
expect(block!.body).toBe("body");
|
|
13
|
+
expect(block!.args).toBe("args here");
|
|
14
|
+
expect(block!.condensed).toBe("/skill:foo args here");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("strips the 'References are relative to <baseDir>.\\n\\n' preamble from body", () => {
|
|
18
|
+
const text =
|
|
19
|
+
`<skill name="foo" location="/x/SKILL.md">\nReferences are relative to /x.\n\nfirst body line\nsecond body line\n</skill>`;
|
|
20
|
+
const block = parseSkillBlock(text);
|
|
21
|
+
expect(block!.body).toBe("first body line\nsecond body line");
|
|
22
|
+
expect(block!.body.startsWith("References are relative")).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("falls back to verbatim content when preamble is absent", () => {
|
|
26
|
+
// Hand-crafted wrapper without the standard preamble (defensive: pi may evolve).
|
|
27
|
+
const text = `<skill name="foo" location="/x">\nbody only\n</skill>`;
|
|
28
|
+
expect(parseSkillBlock(text)!.body).toBe("body only");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("matches a wrapper without args (no preamble)", () => {
|
|
32
|
+
const text = `<skill name="foo" location="/x">\nbody\n</skill>`;
|
|
33
|
+
const block = parseSkillBlock(text);
|
|
34
|
+
expect(block).not.toBeNull();
|
|
35
|
+
expect(block!.args).toBeUndefined();
|
|
36
|
+
expect(block!.condensed).toBe("/skill:foo");
|
|
37
|
+
expect(block!.body).toBe("body");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("preserves multi-line user args verbatim", () => {
|
|
41
|
+
const text =
|
|
42
|
+
`<skill name="foo" location="/x">\nbody\n</skill>\n\nline1\nline2\nline3`;
|
|
43
|
+
const block = parseSkillBlock(text);
|
|
44
|
+
expect(block!.args).toBe("line1\nline2\nline3");
|
|
45
|
+
expect(block!.condensed).toBe("/skill:foo line1\nline2\nline3");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns null for plain text", () => {
|
|
49
|
+
expect(parseSkillBlock("Hello, this is just text.")).toBeNull();
|
|
50
|
+
expect(parseSkillBlock("")).toBeNull();
|
|
51
|
+
expect(parseSkillBlock("/skill:foo args (unexpanded)")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns null for mid-document <skill> (anchor enforcement)", () => {
|
|
55
|
+
const text = `prefix\n<skill name="foo" location="/x">\nbody\n</skill>`;
|
|
56
|
+
expect(parseSkillBlock(text)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns null for trailing whitespace after </skill>", () => {
|
|
60
|
+
// Anchor end-of-string: a stray newline after </skill> with no args fails the optional-args group.
|
|
61
|
+
const text = `<skill name="foo" location="/x">\nbody\n</skill>\n`;
|
|
62
|
+
expect(parseSkillBlock(text)).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("does not terminate prematurely on body containing literal <skill> text", () => {
|
|
66
|
+
const text =
|
|
67
|
+
`<skill name="real" location="/x">\nReferences are relative to /x.\n\nDocumented like: <skill name="example">…</skill>\nThat ended.\n</skill>`;
|
|
68
|
+
const block = parseSkillBlock(text);
|
|
69
|
+
expect(block).not.toBeNull();
|
|
70
|
+
expect(block!.name).toBe("real");
|
|
71
|
+
expect(block!.body).toContain('Documented like: <skill name="example">…</skill>');
|
|
72
|
+
expect(block!.body).toContain("That ended.");
|
|
73
|
+
// preamble was stripped — body starts with the user-visible content
|
|
74
|
+
expect(block!.body.startsWith("Documented")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("handles an empty body (no preamble)", () => {
|
|
78
|
+
const text = `<skill name="empty" location="/x">\n\n</skill>`;
|
|
79
|
+
const block = parseSkillBlock(text);
|
|
80
|
+
expect(block).not.toBeNull();
|
|
81
|
+
expect(block!.body).toBe("");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("condensed form has a single space between name and args", () => {
|
|
85
|
+
const text = `<skill name="x" location="/p">\nb\n</skill>\n\nfoo`;
|
|
86
|
+
expect(parseSkillBlock(text)!.condensed).toBe("/skill:x foo");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("condensed form has no trailing space when args is absent", () => {
|
|
90
|
+
const text = `<skill name="x" location="/p">\nb\n</skill>`;
|
|
91
|
+
expect(parseSkillBlock(text)!.condensed).toBe("/skill:x");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("buildSkillBlock", () => {
|
|
96
|
+
it("emits pi's exact wrapper format with args", () => {
|
|
97
|
+
const out = buildSkillBlock({
|
|
98
|
+
name: "openspec-explore",
|
|
99
|
+
filePath: "/x/openspec-explore/SKILL.md",
|
|
100
|
+
baseDir: "/x/openspec-explore",
|
|
101
|
+
body: "Enter explore mode.",
|
|
102
|
+
userArgs: "continue with X",
|
|
103
|
+
});
|
|
104
|
+
expect(out).toBe(
|
|
105
|
+
`<skill name="openspec-explore" location="/x/openspec-explore/SKILL.md">\n` +
|
|
106
|
+
`References are relative to /x/openspec-explore.\n\n` +
|
|
107
|
+
`Enter explore mode.\n` +
|
|
108
|
+
`</skill>\n\n` +
|
|
109
|
+
`continue with X`,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("emits wrapper without trailing args block when userArgs is absent", () => {
|
|
114
|
+
const out = buildSkillBlock({
|
|
115
|
+
name: "foo",
|
|
116
|
+
filePath: "/x/foo/SKILL.md",
|
|
117
|
+
baseDir: "/x/foo",
|
|
118
|
+
body: "body line",
|
|
119
|
+
});
|
|
120
|
+
expect(out.endsWith("</skill>")).toBe(true);
|
|
121
|
+
expect(out).not.toContain("</skill>\n\n");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("buildSkillBlock + parseSkillBlock round-trip", () => {
|
|
126
|
+
it("round-trips name / body / args (with args)", () => {
|
|
127
|
+
const built = buildSkillBlock({
|
|
128
|
+
name: "round",
|
|
129
|
+
filePath: "/p/SKILL.md",
|
|
130
|
+
baseDir: "/p",
|
|
131
|
+
body: "Some body\nwith multiple lines",
|
|
132
|
+
userArgs: "the args here",
|
|
133
|
+
});
|
|
134
|
+
const parsed = parseSkillBlock(built);
|
|
135
|
+
expect(parsed).not.toBeNull();
|
|
136
|
+
expect(parsed!.name).toBe("round");
|
|
137
|
+
expect(parsed!.location).toBe("/p/SKILL.md");
|
|
138
|
+
expect(parsed!.body).toBe("Some body\nwith multiple lines");
|
|
139
|
+
expect(parsed!.args).toBe("the args here");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("round-trips with no args", () => {
|
|
143
|
+
const built = buildSkillBlock({
|
|
144
|
+
name: "noargs",
|
|
145
|
+
filePath: "/p/SKILL.md",
|
|
146
|
+
baseDir: "/p",
|
|
147
|
+
body: "Body only",
|
|
148
|
+
});
|
|
149
|
+
const parsed = parseSkillBlock(built);
|
|
150
|
+
expect(parsed!.args).toBeUndefined();
|
|
151
|
+
expect(parsed!.condensed).toBe("/skill:noargs");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -104,7 +104,7 @@ describe("pi binary definition", () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
describe("pi-coding-agent module definition", () => {
|
|
107
|
-
it("probes both @
|
|
107
|
+
it("probes both @earendil-works (preferred) and @mariozechner (legacy fallback) alias names", () => {
|
|
108
108
|
const r = freshRegistry({ exists: () => false });
|
|
109
109
|
const res = r.resolve("pi-coding-agent");
|
|
110
110
|
const names = res.tried.map((t) => t.strategy);
|
|
@@ -12,8 +12,15 @@
|
|
|
12
12
|
*
|
|
13
13
|
* See change: unified-bootstrap-install.
|
|
14
14
|
*/
|
|
15
|
-
import { spawn as cpSpawn } from "./platform/exec.js";
|
|
16
|
-
import {
|
|
15
|
+
import { spawn as cpSpawn, spawnSync as cpSpawnSync } from "./platform/exec.js";
|
|
16
|
+
import {
|
|
17
|
+
cpSync,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
17
24
|
import path from "node:path";
|
|
18
25
|
import { getManagedDir } from "./managed-paths.js";
|
|
19
26
|
import { getDefaultRegistry, type ToolRegistry } from "./tool-registry/index.js";
|
|
@@ -206,7 +213,194 @@ export async function bootstrapInstallDefaults(
|
|
|
206
213
|
progress?: ProgressCallback,
|
|
207
214
|
): Promise<BootstrapInstallResult> {
|
|
208
215
|
return bootstrapInstall({
|
|
209
|
-
packages: ["@
|
|
216
|
+
packages: ["@earendil-works/pi-coding-agent", "@fission-ai/openspec", "tsx"],
|
|
210
217
|
progress,
|
|
211
218
|
});
|
|
212
219
|
}
|
|
220
|
+
|
|
221
|
+
// ── Managed Node runtime install ───────────────────────────────────────
|
|
222
|
+
//
|
|
223
|
+
// See change: embed-managed-node-runtime (spec: managed-node-runtime).
|
|
224
|
+
//
|
|
225
|
+
// `installManagedNode` copies a bundled Node distribution into
|
|
226
|
+
// `<managedDir>/node/` and writes a `<managedDir>/node/.version` marker.
|
|
227
|
+
// Idempotent: skip when marker matches the bundled version, replace on
|
|
228
|
+
// mismatch, no-op when the bundled source isn't available (standalone
|
|
229
|
+
// CLI install with no Electron resources).
|
|
230
|
+
|
|
231
|
+
export interface InstallManagedNodeOptions {
|
|
232
|
+
/**
|
|
233
|
+
* Source directory containing the bundled Node distribution.
|
|
234
|
+
* Layout matches the upstream Node zip/tar:
|
|
235
|
+
* Windows: `<dir>/node.exe`, `<dir>/npm.cmd`, `<dir>/npx.cmd`
|
|
236
|
+
* Unix: `<dir>/bin/node`, `<dir>/bin/npm`, `<dir>/bin/npx`
|
|
237
|
+
*
|
|
238
|
+
* Caller resolves this (Electron uses `path.dirname(getBundledNodePath())`
|
|
239
|
+
* after stripping the platform-specific suffix). Pass `null` /
|
|
240
|
+
* `undefined` for the standalone CLI install case — the function
|
|
241
|
+
* no-ops without error.
|
|
242
|
+
*/
|
|
243
|
+
bundledNodeDir?: string | null;
|
|
244
|
+
/** Root of the managed install. Defaults to `getManagedDir()`. */
|
|
245
|
+
managedDir?: string;
|
|
246
|
+
/** Called on every progress tick. Mirrors `bootstrapInstall` shape. */
|
|
247
|
+
progress?: ProgressCallback;
|
|
248
|
+
/**
|
|
249
|
+
* Test seam: override how the bundled Node version is read. Default
|
|
250
|
+
* spawns `<bundledNodeDir>/bin/node --version` (or `node.exe` on
|
|
251
|
+
* Windows) and trims stdout. Tests inject a fake to avoid real spawns.
|
|
252
|
+
*/
|
|
253
|
+
_readVersion?: (sourceBinary: string) => string | null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface InstallManagedNodeResult {
|
|
257
|
+
/** True iff the operation succeeded (including the no-op cases). */
|
|
258
|
+
ok: boolean;
|
|
259
|
+
/** Did we actually copy files? false when no-op or skipped. */
|
|
260
|
+
copied: boolean;
|
|
261
|
+
/** Resolved managed Node directory (`<managedDir>/node`). */
|
|
262
|
+
managedNodeDir: string;
|
|
263
|
+
/** Bundled Node version (e.g. `v22.12.0`) when known. */
|
|
264
|
+
version?: string;
|
|
265
|
+
/** Reason for skip / failure. Always set when `copied === false`. */
|
|
266
|
+
reason?: string;
|
|
267
|
+
/** Error message when `ok === false`. */
|
|
268
|
+
error?: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Path to the source `node` / `node.exe` binary inside `bundledNodeDir`. */
|
|
272
|
+
function sourceNodeBinary(bundledNodeDir: string): string {
|
|
273
|
+
return process.platform === "win32" // platform-branch-ok: Node distribution layout differs Windows vs Unix
|
|
274
|
+
? path.join(bundledNodeDir, "node.exe")
|
|
275
|
+
: path.join(bundledNodeDir, "bin", "node");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Spawn `<nodeBinary> --version` and return the trimmed stdout (e.g.
|
|
280
|
+
* `v22.12.0`). Returns null on failure or when the binary is missing.
|
|
281
|
+
* Synchronous because bootstrap is naturally serial.
|
|
282
|
+
*/
|
|
283
|
+
function readNodeVersion(nodeBinary: string): string | null {
|
|
284
|
+
if (!existsSync(nodeBinary)) return null;
|
|
285
|
+
try {
|
|
286
|
+
const r = cpSpawnSync(nodeBinary, ["--version"], {
|
|
287
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
288
|
+
timeout: 5_000,
|
|
289
|
+
encoding: "utf-8",
|
|
290
|
+
});
|
|
291
|
+
if (r.status !== 0) return null;
|
|
292
|
+
const out = (r.stdout ?? "").toString().trim();
|
|
293
|
+
return out || null;
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Read the `<managedNodeDir>/.version` marker if present, else null. */
|
|
300
|
+
function readManagedMarker(managedNodeDir: string): string | null {
|
|
301
|
+
const markerPath = path.join(managedNodeDir, ".version");
|
|
302
|
+
if (!existsSync(markerPath)) return null;
|
|
303
|
+
try {
|
|
304
|
+
return readFileSync(markerPath, "utf-8").trim() || null;
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Idempotently copy the bundled Node distribution into `<managedDir>/node/`.
|
|
312
|
+
*
|
|
313
|
+
* - First-run: full recursive copy + write `.version` marker.
|
|
314
|
+
* - Re-run with matching marker: no-op.
|
|
315
|
+
* - Mismatched marker (or missing marker with dir present): replace +
|
|
316
|
+
* rewrite marker.
|
|
317
|
+
* - Bundled source absent or `bundledNodeDir == null`: no-op.
|
|
318
|
+
* - Failed copy mid-flight: marker NOT written, so next call retries.
|
|
319
|
+
*/
|
|
320
|
+
export async function installManagedNode(
|
|
321
|
+
opts: InstallManagedNodeOptions = {},
|
|
322
|
+
): Promise<InstallManagedNodeResult> {
|
|
323
|
+
const managedDir = opts.managedDir ?? getManagedDir();
|
|
324
|
+
const managedNodeDir = path.join(managedDir, "node");
|
|
325
|
+
const step = "node-runtime";
|
|
326
|
+
|
|
327
|
+
const bundledDir = opts.bundledNodeDir ?? null;
|
|
328
|
+
if (!bundledDir) {
|
|
329
|
+
return {
|
|
330
|
+
ok: true,
|
|
331
|
+
copied: false,
|
|
332
|
+
managedNodeDir,
|
|
333
|
+
reason: "no bundled source",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const sourceBinary = sourceNodeBinary(bundledDir);
|
|
338
|
+
const sourceVersion = (opts._readVersion ?? readNodeVersion)(sourceBinary);
|
|
339
|
+
if (!sourceVersion) {
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
copied: false,
|
|
343
|
+
managedNodeDir,
|
|
344
|
+
reason: `bundled node binary missing or unreadable: ${sourceBinary}`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const existingMarker = readManagedMarker(managedNodeDir);
|
|
349
|
+
if (existingMarker === sourceVersion) {
|
|
350
|
+
return {
|
|
351
|
+
ok: true,
|
|
352
|
+
copied: false,
|
|
353
|
+
managedNodeDir,
|
|
354
|
+
version: sourceVersion,
|
|
355
|
+
reason: "version matches bundled — no copy needed",
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
opts.progress?.({
|
|
360
|
+
step,
|
|
361
|
+
status: "running",
|
|
362
|
+
output: `Installing Node ${sourceVersion} runtime`,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
// Replace any existing dir (handles the mismatch + missing-marker
|
|
367
|
+
// cases) so the copy is from a clean slate.
|
|
368
|
+
if (existsSync(managedNodeDir)) {
|
|
369
|
+
rmSync(managedNodeDir, { recursive: true, force: true });
|
|
370
|
+
}
|
|
371
|
+
mkdirSync(path.dirname(managedNodeDir), { recursive: true });
|
|
372
|
+
|
|
373
|
+
cpSync(bundledDir, managedNodeDir, {
|
|
374
|
+
recursive: true,
|
|
375
|
+
force: true,
|
|
376
|
+
// dereference: false keeps symlinks-as-symlinks (Unix npm shim).
|
|
377
|
+
// verbatimSymlinks would also work in newer Node; default is fine.
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Marker last — partial copy on failure leaves no marker, so the
|
|
381
|
+
// next invocation treats the dir as missing and retries.
|
|
382
|
+
writeFileSync(
|
|
383
|
+
path.join(managedNodeDir, ".version"),
|
|
384
|
+
sourceVersion + "\n",
|
|
385
|
+
"utf-8",
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
opts.progress?.({ step, status: "done", output: `Node ${sourceVersion}` });
|
|
389
|
+
return {
|
|
390
|
+
ok: true,
|
|
391
|
+
copied: true,
|
|
392
|
+
managedNodeDir,
|
|
393
|
+
version: sourceVersion,
|
|
394
|
+
};
|
|
395
|
+
} catch (err) {
|
|
396
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
397
|
+
opts.progress?.({ step, status: "error", error: message });
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
copied: false,
|
|
401
|
+
managedNodeDir,
|
|
402
|
+
version: sourceVersion,
|
|
403
|
+
error: message,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|