@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
|
@@ -128,6 +128,102 @@ describe("publish.yml — electron job dependency-graph contract", () => {
|
|
|
128
128
|
// single source of truth is the `prepare` job's computed `is_prerelease`
|
|
129
129
|
// output. See change: eliminate-bash-on-windows-runners (D6).
|
|
130
130
|
|
|
131
|
+
// ── Lockfile-regen contract ──────────────────────────────────────────────
|
|
132
|
+
// The `prepare` job MUST regenerate package-lock.json with the bumped
|
|
133
|
+
// versions (between sync-versions.js and the git commit) so consumers'
|
|
134
|
+
// `npm ci` doesn't fall back to stale registry tarballs via strict
|
|
135
|
+
// prerelease semver. See change: fix-release-lockfile-drift.
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse the `steps:` block of a single job into an array of `{ run }`
|
|
139
|
+
* entries. We only care about the `run:` field for this contract; the
|
|
140
|
+
* step delimiter is any ` - ` line (6-space indent + dash + space).
|
|
141
|
+
* Multi-line `run: |` blocks fold into a single `run` string.
|
|
142
|
+
*/
|
|
143
|
+
function parseJobSteps(jobBlock: string): Array<{ run: string }> {
|
|
144
|
+
const lines = jobBlock.split("\n");
|
|
145
|
+
const steps: Array<{ run: string }> = [];
|
|
146
|
+
let i = 0;
|
|
147
|
+
// Find the ` steps:` line.
|
|
148
|
+
while (i < lines.length && !/^ steps:\s*$/.test(lines[i])) i++;
|
|
149
|
+
i++;
|
|
150
|
+
let current: { run: string } | null = null;
|
|
151
|
+
let inRunBlock = false;
|
|
152
|
+
let runBlockIndent = 0;
|
|
153
|
+
while (i < lines.length) {
|
|
154
|
+
const line = lines[i];
|
|
155
|
+
// New step delimiter: ` - ` at 6-space indent.
|
|
156
|
+
if (/^ - /.test(line)) {
|
|
157
|
+
if (current) steps.push(current);
|
|
158
|
+
current = { run: "" };
|
|
159
|
+
inRunBlock = false;
|
|
160
|
+
// Inline `- run: foo` form.
|
|
161
|
+
const inlineRun = line.match(/^ -\s+run:\s+(.*)$/);
|
|
162
|
+
if (inlineRun) current.run = inlineRun[1];
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (current) {
|
|
167
|
+
// Block scalar ` run: |`.
|
|
168
|
+
const blockStart = line.match(/^ run:\s*\|?\s*$/);
|
|
169
|
+
const inlineKey = line.match(/^ run:\s+(.+)$/);
|
|
170
|
+
if (blockStart) {
|
|
171
|
+
inRunBlock = true;
|
|
172
|
+
runBlockIndent = 10; // body lines start at ≥ 10-space indent
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (inlineKey) {
|
|
177
|
+
current.run += (current.run ? "\n" : "") + inlineKey[1];
|
|
178
|
+
i++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (inRunBlock) {
|
|
182
|
+
// Body line of a `run: |` block. Stop when we hit a less-indented
|
|
183
|
+
// line (next key at 8-space indent, or the next step at 6-space).
|
|
184
|
+
if (line.length === 0) {
|
|
185
|
+
current.run += "\n";
|
|
186
|
+
i++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const indent = line.length - line.trimStart().length;
|
|
190
|
+
if (indent < runBlockIndent) {
|
|
191
|
+
inRunBlock = false;
|
|
192
|
+
continue; // re-process this line as a key
|
|
193
|
+
}
|
|
194
|
+
current.run += (current.run ? "\n" : "") + line.slice(runBlockIndent);
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
if (current) steps.push(current);
|
|
202
|
+
return steps;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
describe("publish.yml — prepare job lockfile-regen contract", () => {
|
|
206
|
+
const yaml = fs.readFileSync(WORKFLOW_PATH, "utf8");
|
|
207
|
+
const prepareBlock = extractJobBlock(yaml, "prepare");
|
|
208
|
+
const prepareSteps = parseJobSteps(prepareBlock);
|
|
209
|
+
|
|
210
|
+
it("prepare job regenerates lockfile after version bump (fix-release-lockfile-drift)", () => {
|
|
211
|
+
const syncIdx = prepareSteps.findIndex((s) => /sync-versions\.js/.test(s.run || ""));
|
|
212
|
+
const regenIdx = prepareSteps.findIndex((s) =>
|
|
213
|
+
/npm install --package-lock-only/.test(s.run || ""),
|
|
214
|
+
);
|
|
215
|
+
const commitIdx = prepareSteps.findIndex((s) =>
|
|
216
|
+
/git commit -m "chore\(release\)/.test(s.run || ""),
|
|
217
|
+
);
|
|
218
|
+
expect(syncIdx, "sync-versions.js step missing").toBeGreaterThanOrEqual(0);
|
|
219
|
+
expect(
|
|
220
|
+
regenIdx,
|
|
221
|
+
"lockfile regen step missing — see change fix-release-lockfile-drift",
|
|
222
|
+
).toBeGreaterThan(syncIdx);
|
|
223
|
+
expect(commitIdx, "git commit step missing").toBeGreaterThan(regenIdx);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
131
227
|
describe("publish.yml — prerelease safety contract", () => {
|
|
132
228
|
const yaml = fs.readFileSync(WORKFLOW_PATH, "utf8");
|
|
133
229
|
const prepareBlock = extractJobBlock(yaml, "prepare");
|
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
} from "../recommended-extensions.js";
|
|
9
9
|
|
|
10
10
|
describe("RECOMMENDED_EXTENSIONS manifest", () => {
|
|
11
|
-
it("contains exactly the
|
|
11
|
+
it("contains exactly the six expected entries", () => {
|
|
12
12
|
const ids = RECOMMENDED_EXTENSIONS.map((e) => e.id).sort();
|
|
13
13
|
expect(ids).toEqual(
|
|
14
14
|
[
|
|
15
15
|
"pi-anthropic-messages",
|
|
16
16
|
"pi-agent-browser",
|
|
17
17
|
"pi-flows",
|
|
18
|
+
"pi-memory-honcho",
|
|
18
19
|
"pi-web-access",
|
|
19
20
|
"tintinweb-pi-subagents",
|
|
20
21
|
].sort(),
|
|
@@ -62,7 +63,12 @@ describe("RECOMMENDED_EXTENSIONS manifest", () => {
|
|
|
62
63
|
it("npm-sourced entries use the npm: prefix", () => {
|
|
63
64
|
const npmEntries = RECOMMENDED_EXTENSIONS.filter((e) => e.source.startsWith("npm:"));
|
|
64
65
|
expect(npmEntries.map((e) => e.id).sort()).toEqual(
|
|
65
|
-
[
|
|
66
|
+
[
|
|
67
|
+
"pi-agent-browser",
|
|
68
|
+
"pi-memory-honcho",
|
|
69
|
+
"pi-web-access",
|
|
70
|
+
"tintinweb-pi-subagents",
|
|
71
|
+
].sort(),
|
|
66
72
|
);
|
|
67
73
|
});
|
|
68
74
|
|
|
@@ -105,7 +111,9 @@ describe("getRecommendedByStatus", () => {
|
|
|
105
111
|
|
|
106
112
|
it("filters by optional", () => {
|
|
107
113
|
const optional = getRecommendedByStatus("optional");
|
|
108
|
-
expect(optional.map((e) => e.id)).toEqual(
|
|
114
|
+
expect(optional.map((e) => e.id).sort()).toEqual(
|
|
115
|
+
["pi-agent-browser", "pi-memory-honcho"].sort(),
|
|
116
|
+
);
|
|
109
117
|
});
|
|
110
118
|
});
|
|
111
119
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `launchDashboardServer`.
|
|
3
|
+
*
|
|
4
|
+
* Mocks the test seams (`_resolveJiti`, `_spawnNodeScript`,
|
|
5
|
+
* `_isDashboardRunning`, `_fs`, `_sleep`, `_now`) so the launcher's
|
|
6
|
+
* orchestration logic is exercised without spawning a real child or
|
|
7
|
+
* touching the filesystem.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi } from "vitest";
|
|
10
|
+
import { EventEmitter } from "node:events";
|
|
11
|
+
import {
|
|
12
|
+
launchDashboardServer,
|
|
13
|
+
JitiNotFoundError,
|
|
14
|
+
PortConflictError,
|
|
15
|
+
EarlyExitError,
|
|
16
|
+
} from "../server-launcher.js";
|
|
17
|
+
import type { ChildProcess } from "node:child_process";
|
|
18
|
+
import type { spawnNodeScript } from "../platform/node-spawn.js";
|
|
19
|
+
import type { isDashboardRunning } from "../server-identity.js";
|
|
20
|
+
|
|
21
|
+
const spawnSpy = (impl: () => ChildProcess) =>
|
|
22
|
+
vi.fn<typeof spawnNodeScript>(impl as unknown as typeof spawnNodeScript);
|
|
23
|
+
const probeSpy = <T>(impl: () => Promise<T>) =>
|
|
24
|
+
vi.fn<typeof isDashboardRunning>(impl as unknown as typeof isDashboardRunning);
|
|
25
|
+
|
|
26
|
+
interface FakeChildOpts {
|
|
27
|
+
pid?: number | null;
|
|
28
|
+
exitCode?: number | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeFakeChild(opts: FakeChildOpts = {}): ChildProcess {
|
|
32
|
+
const ee = new EventEmitter() as unknown as ChildProcess & {
|
|
33
|
+
pid: number | undefined;
|
|
34
|
+
exitCode: number | null;
|
|
35
|
+
signalCode: NodeJS.Signals | null;
|
|
36
|
+
unref: () => void;
|
|
37
|
+
};
|
|
38
|
+
ee.pid = (opts.pid ?? 12345) as number | undefined;
|
|
39
|
+
ee.exitCode = opts.exitCode ?? null;
|
|
40
|
+
ee.signalCode = null;
|
|
41
|
+
ee.unref = vi.fn();
|
|
42
|
+
return ee;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function baseOpts(overrides: Partial<Parameters<typeof launchDashboardServer>[0]> = {}) {
|
|
46
|
+
return {
|
|
47
|
+
cliPath: "/srv/cli.ts",
|
|
48
|
+
stdio: "ignore" as const,
|
|
49
|
+
healthTimeoutMs: 5000,
|
|
50
|
+
port: 8000,
|
|
51
|
+
_resolveJiti: () => "file:///loader/jiti-register.mjs",
|
|
52
|
+
_spawnNodeScript: spawnSpy(() => makeFakeChild()),
|
|
53
|
+
_isDashboardRunning: probeSpy(async () => ({ running: true, pid: 99 })),
|
|
54
|
+
_sleep: () => Promise.resolve(),
|
|
55
|
+
_pollIntervalMs: 1,
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("launchDashboardServer — happy path", () => {
|
|
61
|
+
it("returns childPid + reportedPid + healthOk on first health-ok poll", async () => {
|
|
62
|
+
const result = await launchDashboardServer(baseOpts());
|
|
63
|
+
expect(result.childPid).toBe(12345);
|
|
64
|
+
expect(result.reportedPid).toBe(99);
|
|
65
|
+
expect(result.healthOk).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("delegates argv to spawnNodeScript with loader + entry + args", async () => {
|
|
69
|
+
const spy = spawnSpy(() => makeFakeChild());
|
|
70
|
+
await launchDashboardServer(baseOpts({
|
|
71
|
+
_spawnNodeScript: spy,
|
|
72
|
+
extraArgs: ["--port", "8000", "--pi-port", "9999"],
|
|
73
|
+
}));
|
|
74
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
75
|
+
const call = spy.mock.calls[0]![0]!;
|
|
76
|
+
expect(call.loader).toBe("file:///loader/jiti-register.mjs");
|
|
77
|
+
expect(call.entry).toBe("/srv/cli.ts");
|
|
78
|
+
expect(call.args).toEqual(["--port", "8000", "--pi-port", "9999"]);
|
|
79
|
+
expect(call.spawnOptions?.detached).toBe(true);
|
|
80
|
+
expect(call.spawnOptions?.windowsHide).toBe(true);
|
|
81
|
+
expect(call.spawnOptions?.stdio).toBe("ignore");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("launchDashboardServer — jiti resolution", () => {
|
|
86
|
+
it("throws JitiNotFoundError when resolveJiti returns null (no spawn)", async () => {
|
|
87
|
+
const spawn = spawnSpy(() => makeFakeChild());
|
|
88
|
+
await expect(launchDashboardServer(baseOpts({
|
|
89
|
+
_resolveJiti: () => null,
|
|
90
|
+
_spawnNodeScript: spawn,
|
|
91
|
+
}))).rejects.toBeInstanceOf(JitiNotFoundError);
|
|
92
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("launchDashboardServer — readiness termination", () => {
|
|
97
|
+
it("throws PortConflictError when probe reports portConflict", async () => {
|
|
98
|
+
await expect(launchDashboardServer(baseOpts({
|
|
99
|
+
_isDashboardRunning: async () => ({ running: false, portConflict: true }),
|
|
100
|
+
}))).rejects.toBeInstanceOf(PortConflictError);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("throws EarlyExitError when child exits during poll", async () => {
|
|
104
|
+
const child = makeFakeChild();
|
|
105
|
+
let calls = 0;
|
|
106
|
+
const spawnFn = spawnSpy(() => child);
|
|
107
|
+
const probe = probeSpy(async () => {
|
|
108
|
+
calls++;
|
|
109
|
+
if (calls === 1) {
|
|
110
|
+
// Mid-poll, child crashes.
|
|
111
|
+
(child as unknown as { exitCode: number }).exitCode = 7;
|
|
112
|
+
}
|
|
113
|
+
return { running: false };
|
|
114
|
+
});
|
|
115
|
+
await expect(launchDashboardServer(baseOpts({
|
|
116
|
+
_spawnNodeScript: spawnFn,
|
|
117
|
+
_isDashboardRunning: probe,
|
|
118
|
+
}))).rejects.toBeInstanceOf(EarlyExitError);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("throws readiness-timeout Error after healthTimeoutMs elapses", async () => {
|
|
122
|
+
let now = 1000;
|
|
123
|
+
await expect(launchDashboardServer(baseOpts({
|
|
124
|
+
healthTimeoutMs: 100,
|
|
125
|
+
_now: () => { now += 60; return now; }, // each poll advances 60ms — 2 polls past deadline
|
|
126
|
+
_isDashboardRunning: async () => ({ running: false }),
|
|
127
|
+
}))).rejects.toThrow(/readiness timeout/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("port-conflict beats timeout (probe order respected)", async () => {
|
|
131
|
+
let now = 1000;
|
|
132
|
+
await expect(launchDashboardServer(baseOpts({
|
|
133
|
+
healthTimeoutMs: 100,
|
|
134
|
+
_now: () => { now += 200; return now; },
|
|
135
|
+
_isDashboardRunning: async () => ({ running: false, portConflict: true }),
|
|
136
|
+
}))).rejects.toBeInstanceOf(PortConflictError);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("launchDashboardServer — log-file stdio", () => {
|
|
141
|
+
it("mkdirs parent, opens append fd, writes header, passes fd, closes parent's copy", async () => {
|
|
142
|
+
const calls: string[] = [];
|
|
143
|
+
const fsStub = {
|
|
144
|
+
mkdirSync: vi.fn((p: any) => { calls.push(`mkdir:${p}`); }),
|
|
145
|
+
openSync: vi.fn((p: any, mode: any) => { calls.push(`open:${p}:${mode}`); return 42; }),
|
|
146
|
+
writeSync: vi.fn((fd: number, s: any) => { calls.push(`write:${fd}:${String(s).slice(0, 20)}…`); return s.length; }),
|
|
147
|
+
closeSync: vi.fn((fd: number) => { calls.push(`close:${fd}`); }),
|
|
148
|
+
};
|
|
149
|
+
const spawn = spawnSpy(() => makeFakeChild());
|
|
150
|
+
await launchDashboardServer(baseOpts({
|
|
151
|
+
stdio: { logFile: "/var/log/dashboard/server.log" },
|
|
152
|
+
starter: "Standalone",
|
|
153
|
+
_fs: fsStub as any,
|
|
154
|
+
_spawnNodeScript: spawn,
|
|
155
|
+
}));
|
|
156
|
+
expect(fsStub.mkdirSync).toHaveBeenCalledWith("/var/log/dashboard", { recursive: true });
|
|
157
|
+
expect(fsStub.openSync).toHaveBeenCalledWith("/var/log/dashboard/server.log", "a");
|
|
158
|
+
expect(fsStub.writeSync).toHaveBeenCalledOnce();
|
|
159
|
+
expect(String(fsStub.writeSync.mock.calls[0]![1])).toContain("Standalone launch");
|
|
160
|
+
// Spawn received [ignore, fd, fd]:
|
|
161
|
+
const stdio = spawn.mock.calls[0]![0]!.spawnOptions!.stdio as Array<unknown>;
|
|
162
|
+
expect(stdio).toEqual(["ignore", 42, 42]);
|
|
163
|
+
// Parent fd closed AFTER spawn:
|
|
164
|
+
expect(fsStub.closeSync).toHaveBeenCalledWith(42);
|
|
165
|
+
const closeIdx = calls.findIndex((c) => c.startsWith("close:42"));
|
|
166
|
+
const writeIdx = calls.findIndex((c) => c.startsWith("write:42"));
|
|
167
|
+
expect(closeIdx).toBeGreaterThan(writeIdx);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("launchDashboardServer — env merge", () => {
|
|
172
|
+
it("caller env keys override buildSpawnEnv defaults", async () => {
|
|
173
|
+
const spawn = spawnSpy(() => makeFakeChild());
|
|
174
|
+
await launchDashboardServer(baseOpts({
|
|
175
|
+
_spawnNodeScript: spawn,
|
|
176
|
+
env: { DASHBOARD_STARTER: "Bridge", CUSTOM_KEY: "x" },
|
|
177
|
+
}));
|
|
178
|
+
const env = spawn.mock.calls[0]![0]!.spawnOptions!.env as Record<string, string>;
|
|
179
|
+
expect(env.DASHBOARD_STARTER).toBe("Bridge");
|
|
180
|
+
expect(env.CUSTOM_KEY).toBe("x");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("starter option becomes DASHBOARD_STARTER when env does not supply it", async () => {
|
|
184
|
+
const spawn = spawnSpy(() => makeFakeChild());
|
|
185
|
+
await launchDashboardServer(baseOpts({
|
|
186
|
+
_spawnNodeScript: spawn,
|
|
187
|
+
starter: "Electron",
|
|
188
|
+
}));
|
|
189
|
+
const env = spawn.mock.calls[0]![0]!.spawnOptions!.env as Record<string, string>;
|
|
190
|
+
expect(env.DASHBOARD_STARTER).toBe("Electron");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("explicit env.DASHBOARD_STARTER wins over starter option", async () => {
|
|
194
|
+
const spawn = spawnSpy(() => makeFakeChild());
|
|
195
|
+
await launchDashboardServer(baseOpts({
|
|
196
|
+
_spawnNodeScript: spawn,
|
|
197
|
+
starter: "Electron",
|
|
198
|
+
env: { DASHBOARD_STARTER: "Bridge" },
|
|
199
|
+
}));
|
|
200
|
+
const env = spawn.mock.calls[0]![0]!.spawnOptions!.env as Record<string, string>;
|
|
201
|
+
expect(env.DASHBOARD_STARTER).toBe("Bridge");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("launchDashboardServer — entry URL-wrapping", () => {
|
|
206
|
+
// The launcher delegates to spawnNodeScript, which uses
|
|
207
|
+
// `shouldUrlWrapEntry(loader, platform)`. We verify the launcher
|
|
208
|
+
// simply forwards the raw entry; the URL-wrap behaviour itself is
|
|
209
|
+
// pinned by node-spawn-jiti-contract.test.ts.
|
|
210
|
+
it("forwards `cliPath` verbatim to spawnNodeScript (URL-wrapping owned downstream)", async () => {
|
|
211
|
+
const spawn = spawnSpy(() => makeFakeChild());
|
|
212
|
+
await launchDashboardServer(baseOpts({
|
|
213
|
+
_spawnNodeScript: spawn,
|
|
214
|
+
cliPath: "/posix/cli.ts",
|
|
215
|
+
}));
|
|
216
|
+
expect(spawn.mock.calls[0]![0]!.entry).toBe("/posix/cli.ts");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("forwards Windows-style `cliPath` verbatim too", async () => {
|
|
220
|
+
const spawn = spawnSpy(() => makeFakeChild());
|
|
221
|
+
await launchDashboardServer(baseOpts({
|
|
222
|
+
_spawnNodeScript: spawn,
|
|
223
|
+
cliPath: "C:\\srv\\cli.ts",
|
|
224
|
+
}));
|
|
225
|
+
expect(spawn.mock.calls[0]![0]!.entry).toBe("C:\\srv\\cli.ts");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -213,7 +213,7 @@ export async function bootstrapInstallDefaults(
|
|
|
213
213
|
progress?: ProgressCallback,
|
|
214
214
|
): Promise<BootstrapInstallResult> {
|
|
215
215
|
return bootstrapInstall({
|
|
216
|
-
packages: ["@earendil-works/pi-coding-agent", "@fission-ai/openspec"
|
|
216
|
+
packages: ["@earendil-works/pi-coding-agent", "@fission-ai/openspec"],
|
|
217
217
|
progress,
|
|
218
218
|
});
|
|
219
219
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ImageContent,
|
|
10
10
|
FileEntry,
|
|
11
11
|
OpenSpecData,
|
|
12
|
+
OpenSpecGroup,
|
|
12
13
|
ModelInfo,
|
|
13
14
|
PiSessionInfo,
|
|
14
15
|
ExtensionUiModule,
|
|
@@ -96,6 +97,19 @@ export interface BrowserOpenSpecUpdateMessage {
|
|
|
96
97
|
data: OpenSpecData;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Per-repo OpenSpec change-grouping update. Broadcast after every successful
|
|
102
|
+
* write to `<cwd>/openspec/groups/groups.json`, debounced 100 ms per cwd.
|
|
103
|
+
* Full payload (no incremental delta) so client logic stays simple.
|
|
104
|
+
* See change: add-openspec-change-grouping.
|
|
105
|
+
*/
|
|
106
|
+
export interface BrowserOpenSpecGroupsUpdateMessage {
|
|
107
|
+
type: "openspec_groups_update";
|
|
108
|
+
cwd: string;
|
|
109
|
+
groups: OpenSpecGroup[];
|
|
110
|
+
assignments: Record<string, string>;
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
export interface BrowserModelsListMessage {
|
|
100
114
|
type: "models_list";
|
|
101
115
|
sessionId: string;
|
|
@@ -386,6 +400,18 @@ export interface BootstrapStateSnapshot {
|
|
|
386
400
|
upgradeDashboard?: boolean;
|
|
387
401
|
};
|
|
388
402
|
bridgeRegistrationError?: string;
|
|
403
|
+
/**
|
|
404
|
+
* Legacy `@mariozechner/pi-coding-agent` installs detected on disk.
|
|
405
|
+
* Surfaced by the client as a one-click cleanup banner. Empty array
|
|
406
|
+
* means no legacy installs found. Pi was renamed to
|
|
407
|
+
* `@earendil-works/pi-coding-agent` at v0.74 — the legacy scope can
|
|
408
|
+
* collide with the new scope's `bin/pi` symlink.
|
|
409
|
+
*/
|
|
410
|
+
legacyPiInstalls?: Array<{
|
|
411
|
+
scope: "npm-global" | "npx-cache" | "managed";
|
|
412
|
+
path: string;
|
|
413
|
+
version: string | null;
|
|
414
|
+
}>;
|
|
389
415
|
}
|
|
390
416
|
|
|
391
417
|
/**
|
|
@@ -514,6 +540,7 @@ export type ServerToBrowserMessage =
|
|
|
514
540
|
| BrowserUiDismissMessage
|
|
515
541
|
| BrowserFilesListMessage
|
|
516
542
|
| BrowserOpenSpecUpdateMessage
|
|
543
|
+
| BrowserOpenSpecGroupsUpdateMessage
|
|
517
544
|
| BrowserModelsListMessage
|
|
518
545
|
| SessionsListBrowserMessage
|
|
519
546
|
| ResumeResultBrowserMessage
|