@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -0,0 +1,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
|
+
});
|
|
@@ -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);
|
|
@@ -213,7 +213,7 @@ export async function bootstrapInstallDefaults(
|
|
|
213
213
|
progress?: ProgressCallback,
|
|
214
214
|
): Promise<BootstrapInstallResult> {
|
|
215
215
|
return bootstrapInstall({
|
|
216
|
-
packages: ["@
|
|
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,
|
|
@@ -22,6 +23,14 @@ import type { EditorInstanceStatus } from "./editor-types.js";
|
|
|
22
23
|
export interface SessionAddedMessage {
|
|
23
24
|
type: "session_added";
|
|
24
25
|
session: DashboardSession;
|
|
26
|
+
/**
|
|
27
|
+
* Echoed `requestId` from the originating browser `spawn_session` /
|
|
28
|
+
* `resume_session` (when known). Lets the client auto-select / dismiss
|
|
29
|
+
* placeholder by exact correlation, replacing the cwd-FIFO heuristic.
|
|
30
|
+
* Absent for server-initiated spawns (auto-resume, headless reload).
|
|
31
|
+
* See change: spawn-correlation-token.
|
|
32
|
+
*/
|
|
33
|
+
spawnRequestId?: string;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
export interface SessionUpdatedMessage {
|
|
@@ -88,6 +97,19 @@ export interface BrowserOpenSpecUpdateMessage {
|
|
|
88
97
|
data: OpenSpecData;
|
|
89
98
|
}
|
|
90
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
|
+
|
|
91
113
|
export interface BrowserModelsListMessage {
|
|
92
114
|
type: "models_list";
|
|
93
115
|
sessionId: string;
|
|
@@ -118,6 +140,24 @@ export interface ResumeResultBrowserMessage {
|
|
|
118
140
|
sessionId: string;
|
|
119
141
|
success: boolean;
|
|
120
142
|
message: string;
|
|
143
|
+
/** Echoed from input `resume_session.requestId` when provided. */
|
|
144
|
+
requestId?: string;
|
|
145
|
+
/**
|
|
146
|
+
* For `mode: "fork"` only — populated once the new fork's bridge has
|
|
147
|
+
* registered and been correlated. Absent for `mode: "continue"` (the
|
|
148
|
+
* sessionId is unchanged across the respawn).
|
|
149
|
+
* See change: spawn-correlation-token.
|
|
150
|
+
*/
|
|
151
|
+
newSessionId?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Optional structured failure classifier. Known values:
|
|
154
|
+
* - `"FORK_EMPTY_SESSION"`: fork attempted on a session whose
|
|
155
|
+
* `sessionFile` does not exist on disk yet (e.g., freshly spawned,
|
|
156
|
+
* no messages persisted).
|
|
157
|
+
* Old clients that don't read this field still get the human-readable
|
|
158
|
+
* `message`. See change: fix-fork-empty-session-silent-timeout.
|
|
159
|
+
*/
|
|
160
|
+
code?: string;
|
|
121
161
|
}
|
|
122
162
|
|
|
123
163
|
export interface SpawnResultBrowserMessage {
|
|
@@ -125,6 +165,10 @@ export interface SpawnResultBrowserMessage {
|
|
|
125
165
|
cwd: string;
|
|
126
166
|
success: boolean;
|
|
127
167
|
message: string;
|
|
168
|
+
/** Echoed from input `spawn_session.requestId` when provided. */
|
|
169
|
+
requestId?: string;
|
|
170
|
+
/** Spawned process PID when known (headless strategies); informational. */
|
|
171
|
+
pid?: number;
|
|
128
172
|
}
|
|
129
173
|
|
|
130
174
|
/**
|
|
@@ -356,6 +400,18 @@ export interface BootstrapStateSnapshot {
|
|
|
356
400
|
upgradeDashboard?: boolean;
|
|
357
401
|
};
|
|
358
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
|
+
}>;
|
|
359
415
|
}
|
|
360
416
|
|
|
361
417
|
/**
|
|
@@ -484,6 +540,7 @@ export type ServerToBrowserMessage =
|
|
|
484
540
|
| BrowserUiDismissMessage
|
|
485
541
|
| BrowserFilesListMessage
|
|
486
542
|
| BrowserOpenSpecUpdateMessage
|
|
543
|
+
| BrowserOpenSpecGroupsUpdateMessage
|
|
487
544
|
| BrowserModelsListMessage
|
|
488
545
|
| SessionsListBrowserMessage
|
|
489
546
|
| ResumeResultBrowserMessage
|
|
@@ -665,6 +722,12 @@ export interface ResumeSessionBrowserMessage {
|
|
|
665
722
|
mode: "continue" | "fork";
|
|
666
723
|
/** When forking, optionally fork from a specific session entry instead of the latest */
|
|
667
724
|
entryId?: string;
|
|
725
|
+
/**
|
|
726
|
+
* Client-minted UUIDv4 used to correlate `resume_result` and (for fork mode)
|
|
727
|
+
* the eventual `session_added` for the new session. Optional for back-compat.
|
|
728
|
+
* See change: spawn-correlation-token.
|
|
729
|
+
*/
|
|
730
|
+
requestId?: string;
|
|
668
731
|
/**
|
|
669
732
|
* Placement intent for the resumed session in the cwd's sessionOrder:
|
|
670
733
|
* - "front" (default): move to top of alive tier (Resume button, REST,
|
|
@@ -699,6 +762,13 @@ export interface SpawnSessionBrowserMessage {
|
|
|
699
762
|
* add-folder-task-checker-and-spawn-attach.
|
|
700
763
|
*/
|
|
701
764
|
attachProposal?: string;
|
|
765
|
+
/**
|
|
766
|
+
* Client-minted UUIDv4 used to correlate `spawn_result` and the eventual
|
|
767
|
+
* `session_added` (which echoes it as `spawnRequestId`). Optional for
|
|
768
|
+
* back-compat with older clients.
|
|
769
|
+
* See change: spawn-correlation-token.
|
|
770
|
+
*/
|
|
771
|
+
requestId?: string;
|
|
702
772
|
}
|
|
703
773
|
|
|
704
774
|
export interface AttachProposalBrowserMessage {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the pi changelog display feature.
|
|
3
|
+
*
|
|
4
|
+
* Server parses `CHANGELOG.md` files installed alongside core packages
|
|
5
|
+
* and the client renders the result via `WhatsNewDialog`. These types
|
|
6
|
+
* are the wire contract between the two halves.
|
|
7
|
+
*
|
|
8
|
+
* See change: pi-update-whats-new-panel.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One bullet point under a release section. Preserves the original
|
|
13
|
+
* markdown prose verbatim so issue/PR links and inline code formatting
|
|
14
|
+
* survive intact for the client renderer.
|
|
15
|
+
*/
|
|
16
|
+
export interface ChangelogBullet {
|
|
17
|
+
/**
|
|
18
|
+
* Original markdown text of the bullet, with the leading `- `
|
|
19
|
+
* removed and any continuation lines joined with `\n`. Inline
|
|
20
|
+
* markdown (links, code spans, emphasis) is preserved exactly as
|
|
21
|
+
* written so the client can hand it to a markdown renderer.
|
|
22
|
+
*/
|
|
23
|
+
text: string;
|
|
24
|
+
/**
|
|
25
|
+
* Issue / PR references mined from the prose via the canonical
|
|
26
|
+
* `([#NNN](URL))` pattern pi uses at end-of-bullet. May be empty
|
|
27
|
+
* when no such pattern is found. Order matches occurrence order.
|
|
28
|
+
* The `text` field still contains the link in its original form.
|
|
29
|
+
*/
|
|
30
|
+
issues: { num: number; url: string }[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One release entry parsed from a Keep-a-Changelog-style markdown
|
|
35
|
+
* document. Typed sub-section arrays are populated only when the
|
|
36
|
+
* corresponding H3 heading is present in the source.
|
|
37
|
+
*/
|
|
38
|
+
export interface ChangelogRelease {
|
|
39
|
+
/**
|
|
40
|
+
* Version string lifted from the `## [<version>] - <date>` H2
|
|
41
|
+
* heading. The bracket contents are taken verbatim so versions
|
|
42
|
+
* like `0.4.3-rc.1` survive.
|
|
43
|
+
*/
|
|
44
|
+
version: string;
|
|
45
|
+
/**
|
|
46
|
+
* Date string lifted from the H2 heading. Set to `null` when the
|
|
47
|
+
* date token is missing or fails to parse as a YYYY-MM-DD-ish form.
|
|
48
|
+
* Parser tolerance is intentional — pi has shipped rare entries
|
|
49
|
+
* with date ranges or missing dates.
|
|
50
|
+
*/
|
|
51
|
+
date: string | null;
|
|
52
|
+
/** Bullets under `### Breaking Changes`, in source order. */
|
|
53
|
+
breaking: ChangelogBullet[];
|
|
54
|
+
/**
|
|
55
|
+
* Union of bullets under `### New Features` and `### Added`. Pi
|
|
56
|
+
* uses both labels at different times for the same concept, so
|
|
57
|
+
* we merge them. Source order is preserved within each sub-section
|
|
58
|
+
* and the two sub-sections are concatenated in source order.
|
|
59
|
+
*/
|
|
60
|
+
features: ChangelogBullet[];
|
|
61
|
+
/** Bullets under `### Changed`. */
|
|
62
|
+
changed: ChangelogBullet[];
|
|
63
|
+
/** Bullets under `### Fixed`. */
|
|
64
|
+
fixed: ChangelogBullet[];
|
|
65
|
+
/**
|
|
66
|
+
* Full markdown text from the release's H2 line up to (but not
|
|
67
|
+
* including) the next H2 line. Retained as a fallback render path
|
|
68
|
+
* when the typed arrays don't capture the content (e.g. an
|
|
69
|
+
* unrecognized H3 heading).
|
|
70
|
+
*/
|
|
71
|
+
raw: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Response shape for `GET /api/pi-core/changelog`. Always wraps the
|
|
76
|
+
* filtered release list in this envelope so the client gets the
|
|
77
|
+
* derived `hasBreaking` flag and the GitHub link in one round-trip.
|
|
78
|
+
*/
|
|
79
|
+
export interface ChangelogResponse {
|
|
80
|
+
/** Package the changelog was parsed for. */
|
|
81
|
+
pkg: string;
|
|
82
|
+
/**
|
|
83
|
+
* Lower bound of the version range, EXCLUSIVE. Echoes the request
|
|
84
|
+
* query param. Releases at or below this version are filtered out.
|
|
85
|
+
*/
|
|
86
|
+
from: string;
|
|
87
|
+
/**
|
|
88
|
+
* Upper bound of the version range, INCLUSIVE. Echoes the request
|
|
89
|
+
* query param. Releases above this version are filtered out.
|
|
90
|
+
*/
|
|
91
|
+
to: string;
|
|
92
|
+
/**
|
|
93
|
+
* Filtered release list, ordered with the latest version FIRST.
|
|
94
|
+
* Empty when no releases exist in `(from, to]` or when the
|
|
95
|
+
* CHANGELOG could not be located.
|
|
96
|
+
*/
|
|
97
|
+
releases: ChangelogRelease[];
|
|
98
|
+
/**
|
|
99
|
+
* Convenience flag derived from `releases.some(r => r.breaking.length > 0)`.
|
|
100
|
+
* Lets the client render the warning icon without re-walking the array.
|
|
101
|
+
*/
|
|
102
|
+
hasBreaking: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Public URL to the full CHANGELOG on GitHub, derived from the
|
|
105
|
+
* package's `repository` field. `null` when the repository is not
|
|
106
|
+
* GitHub-hosted or is unparseable.
|
|
107
|
+
*/
|
|
108
|
+
changelogUrl: string | null;
|
|
109
|
+
/** ISO timestamp at which the parser produced this response. */
|
|
110
|
+
parsedAt: string;
|
|
111
|
+
}
|