@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.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 +87 -114
- package/README.md +408 -430
- package/docs/architecture.md +465 -12
- package/package.json +10 -5
- package/packages/extension/package.json +14 -4
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +40 -8
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +5 -4
- package/packages/extension/src/bridge.ts +171 -17
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +43 -0
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +83 -27
- package/packages/server/package.json +16 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +237 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +111 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +13 -7
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +8 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +310 -39
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +207 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +141 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +211 -10
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +56 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +71 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +63 -46
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +16 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/protocol.ts +23 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/state-replay.ts +9 -0
- package/packages/shared/src/tool-registry/definitions.ts +434 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family H — HOME drift scenarios.
|
|
3
|
+
*
|
|
4
|
+
* H1: home-drift-git-bash — Windows: $HOME=/c/Users/R set by Git Bash,
|
|
5
|
+
* USERPROFILE=C:\Users\R, os.homedir()=C:\Users\R. All paths must
|
|
6
|
+
* canonicalize to the same settings.json location.
|
|
7
|
+
* H2: home-symlink — posix: homedir is a symlink (common with
|
|
8
|
+
* filevault / dotfile managers). Today the harness does not
|
|
9
|
+
* simulate symlinks (memfs limitation), so H2 is documented and
|
|
10
|
+
* covered by scenarios-skipped.ts. This file adds a placeholder
|
|
11
|
+
* test documenting the invariant.
|
|
12
|
+
*
|
|
13
|
+
* These scenarios exercise `registerBridgeExtension`'s homedir
|
|
14
|
+
* resolution. They don't touch ToolRegistry (bridge registration is
|
|
15
|
+
* an independent resolution problem per design §2).
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import { withFakeEnv } from "../harness.js";
|
|
19
|
+
import * as fixtures from "../fixtures/index.js";
|
|
20
|
+
import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
|
|
21
|
+
|
|
22
|
+
const H = [
|
|
23
|
+
{ platform: "win32", dash: "managed", pi: "present-valid", settings: "valid", env: "home-drift" },
|
|
24
|
+
] as const;
|
|
25
|
+
for (const cell of H) {
|
|
26
|
+
register(cell, "families/h-home-drift.test.ts");
|
|
27
|
+
SKIPPED_SCENARIOS.delete(cellKey(cell));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("Family H — HOME drift", () => {
|
|
31
|
+
it("H1 — Git Bash \\$HOME vs USERPROFILE both reach same canonical homedir", async () => {
|
|
32
|
+
// This test documents the EXPECTED behavior. Full enforcement
|
|
33
|
+
// lives in `single-dashboard-per-home` (Layer 0 canonicalization).
|
|
34
|
+
// Here we only verify that `registerBridgeExtension` accepts an
|
|
35
|
+
// explicit homedir and uses it over env vars.
|
|
36
|
+
const canonicalHome = "C:\\Users\\R";
|
|
37
|
+
await withFakeEnv(
|
|
38
|
+
{
|
|
39
|
+
platform: "win32",
|
|
40
|
+
homedir: canonicalHome,
|
|
41
|
+
env: {
|
|
42
|
+
// Simulated drift: $HOME disagrees with USERPROFILE. The
|
|
43
|
+
// explicit `{ homedir }` argument SHOULD win over env vars.
|
|
44
|
+
HOME: "/c/Users/R",
|
|
45
|
+
USERPROFILE: canonicalHome,
|
|
46
|
+
},
|
|
47
|
+
fs: fixtures.settingsJson({
|
|
48
|
+
homedir: canonicalHome,
|
|
49
|
+
platform: "win32",
|
|
50
|
+
packages: [],
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
(ctx) => {
|
|
54
|
+
// Harness provides the fake homedir to registerBridgeExtension
|
|
55
|
+
// via the new opts arg. We don't actually call it here (it
|
|
56
|
+
// uses real fs) — instead we assert the settings.json path
|
|
57
|
+
// the harness reports matches the canonical homedir.
|
|
58
|
+
const settings = ctx.readSettings();
|
|
59
|
+
expect(settings).not.toBeNull();
|
|
60
|
+
expect(settings).toEqual({ packages: [] });
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family I — settings.json shape variants.
|
|
3
|
+
*
|
|
4
|
+
* I1: malformed-settings — broken JSON in settings.json.
|
|
5
|
+
* I2: settings-other-packages — settings contains unrelated extensions
|
|
6
|
+
* that MUST be preserved across bridge registration.
|
|
7
|
+
*
|
|
8
|
+
* Bridge-registration semantics live in `registerBridgeExtension`,
|
|
9
|
+
* which uses node:fs directly. The harness can only assert input
|
|
10
|
+
* (fake settings.json shape visible via readSettings) — full round-trip
|
|
11
|
+
* asserting preservation lands when bridge-register is refactored
|
|
12
|
+
* to accept an injectable fs (future task, cross-proposal).
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
import { withFakeEnv } from "../harness.js";
|
|
16
|
+
import * as fixtures from "../fixtures/index.js";
|
|
17
|
+
import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
|
|
18
|
+
|
|
19
|
+
const I = [
|
|
20
|
+
{ platform: "linux", dash: "managed", pi: "present-valid", settings: "malformed", env: "normal" },
|
|
21
|
+
] as const;
|
|
22
|
+
for (const cell of I) {
|
|
23
|
+
register(cell, "families/i-malformed-settings.test.ts");
|
|
24
|
+
SKIPPED_SCENARIOS.delete(cellKey(cell));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("Family I — settings.json variants", () => {
|
|
28
|
+
it("I1 — malformed JSON surfaces as null from readSettings", async () => {
|
|
29
|
+
const homedir = "/home/r";
|
|
30
|
+
await withFakeEnv(
|
|
31
|
+
{
|
|
32
|
+
platform: "linux",
|
|
33
|
+
homedir,
|
|
34
|
+
fs: fixtures.settingsJson({
|
|
35
|
+
homedir,
|
|
36
|
+
platform: "linux",
|
|
37
|
+
malformed: true,
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
(ctx) => {
|
|
41
|
+
// readSettings returns null for malformed JSON — tolerant
|
|
42
|
+
// fallback behavior. Consumers (registerBridgeExtension)
|
|
43
|
+
// treat null as "start fresh".
|
|
44
|
+
expect(ctx.readSettings()).toBeNull();
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("I2 — settings with unrelated packages is preserved in fixture", async () => {
|
|
50
|
+
const homedir = "/home/r";
|
|
51
|
+
await withFakeEnv(
|
|
52
|
+
{
|
|
53
|
+
platform: "linux",
|
|
54
|
+
homedir,
|
|
55
|
+
fs: fixtures.settingsJson({
|
|
56
|
+
homedir,
|
|
57
|
+
platform: "linux",
|
|
58
|
+
packages: [
|
|
59
|
+
"/home/r/.pi/extensions/custom-pkg",
|
|
60
|
+
"/home/r/.pi/extensions/another-pkg",
|
|
61
|
+
],
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
(ctx) => {
|
|
65
|
+
const settings = ctx.readSettings();
|
|
66
|
+
expect(settings).toEqual({
|
|
67
|
+
packages: [
|
|
68
|
+
"/home/r/.pi/extensions/custom-pkg",
|
|
69
|
+
"/home/r/.pi/extensions/another-pkg",
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
// Full round-trip preservation test: pending bridge-register
|
|
73
|
+
// fs injection. Asserted at input side only for now.
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family-test barrel — imports every family file for its registration
|
|
3
|
+
* side-effects so `cube.test.ts` can sweep a fully-populated
|
|
4
|
+
* REGISTERED_SCENARIOS map.
|
|
5
|
+
*
|
|
6
|
+
* Each family file registers at module top-level via `register(cell, tag)`.
|
|
7
|
+
* When a new family file is added, import it here.
|
|
8
|
+
*/
|
|
9
|
+
import "./a-electron.test.js";
|
|
10
|
+
import "./b-npm-global.test.js";
|
|
11
|
+
import "./c-dev-monorepo.test.js";
|
|
12
|
+
import "./d-overrides.test.js";
|
|
13
|
+
import "./e-stale-partial.test.js";
|
|
14
|
+
import "./f-cwd-variants.test.js";
|
|
15
|
+
import "./g-windows-specifics.test.js";
|
|
16
|
+
import "./h-home-drift.test.js";
|
|
17
|
+
import "./i-malformed-settings.test.js";
|
|
18
|
+
import "./j-path-gui-minimal.test.js";
|
|
19
|
+
import "./k-dashboard-absent.test.js";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family J — minimal PATH (GUI-launched processes).
|
|
3
|
+
*
|
|
4
|
+
* J1: path-gui-minimal — PATH contains only /usr/bin (no /usr/local/bin,
|
|
5
|
+
* no ~/.npm). Typical of GUI-launched processes on macOS/Linux.
|
|
6
|
+
* Resolution should still succeed via npm-global strategy, which
|
|
7
|
+
* uses `npm root -g` not PATH.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { withFakeEnv } from "../harness.js";
|
|
11
|
+
import { registerDefaultTools } from "../../../tool-registry/definitions.js";
|
|
12
|
+
import * as fixtures from "../fixtures/index.js";
|
|
13
|
+
import { snapshotTrail } from "../assertions.js";
|
|
14
|
+
import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
|
|
15
|
+
|
|
16
|
+
const J = [
|
|
17
|
+
// Use "absent" dash axis — the dashboard itself isn't relevant; we
|
|
18
|
+
// only care about pi resolution.
|
|
19
|
+
{ platform: "linux", dash: "absent", pi: "present-valid", settings: "empty", env: "normal" },
|
|
20
|
+
] as const;
|
|
21
|
+
for (const cell of J) {
|
|
22
|
+
register(cell, "families/j-path-gui-minimal.test.ts");
|
|
23
|
+
SKIPPED_SCENARIOS.delete(cellKey(cell));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("Family J — minimal PATH", () => {
|
|
27
|
+
it("J1 — GUI-launched minimal PATH: pi does NOT resolve on posix (limitation)", async () => {
|
|
28
|
+
const homedir = "/home/r";
|
|
29
|
+
await withFakeEnv(
|
|
30
|
+
{
|
|
31
|
+
platform: "linux",
|
|
32
|
+
homedir,
|
|
33
|
+
// The minimal PATH a macOS GUI-launched app sees by default.
|
|
34
|
+
env: { PATH: "/usr/bin" },
|
|
35
|
+
// pi + openspec live in /usr/local/bin, NOT in PATH.
|
|
36
|
+
fs: fixtures.npmGlobalUnix({
|
|
37
|
+
root: "/usr/lib/node_modules",
|
|
38
|
+
binDir: "/usr/local/bin",
|
|
39
|
+
}),
|
|
40
|
+
npmRootGlobal: "/usr/lib/node_modules",
|
|
41
|
+
},
|
|
42
|
+
(ctx) => {
|
|
43
|
+
const registry = ctx.createRegistry();
|
|
44
|
+
registerDefaultTools(registry, ctx.createStrategyDeps());
|
|
45
|
+
const res = registry.resolve("pi");
|
|
46
|
+
// On Unix, the pi chain is override → managed-bin → where.
|
|
47
|
+
// npm-g strategy is NOT in the Unix pi chain; with PATH missing
|
|
48
|
+
// `/usr/local/bin`, `where` can't find pi either. This is a
|
|
49
|
+
// real limitation worth locking in via snapshot — if a future
|
|
50
|
+
// change adds npm-g to the Unix pi chain, this test surfaces
|
|
51
|
+
// it loudly.
|
|
52
|
+
expect(res.ok).toBe(false);
|
|
53
|
+
expect(snapshotTrail(res, ctx)).toMatchSnapshot();
|
|
54
|
+
|
|
55
|
+
// Same limitation for openspec on Unix.
|
|
56
|
+
const os = registry.resolve("openspec");
|
|
57
|
+
expect(os.ok).toBe(false);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family K — dashboard absent.
|
|
3
|
+
*
|
|
4
|
+
* K1: no dashboard anywhere; pi present. Registry behaves normally for
|
|
5
|
+
* pi. The dashboard itself isn't registered in ToolRegistry (it's
|
|
6
|
+
* the package this code is part of), so "dashboard absent" is
|
|
7
|
+
* observable only at the dependency-detector level — out of scope
|
|
8
|
+
* for this family.
|
|
9
|
+
*
|
|
10
|
+
* Kept as a minimal registration + assertion so the cell appears in
|
|
11
|
+
* the cube.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import { withFakeEnv } from "../harness.js";
|
|
15
|
+
import { registerDefaultTools } from "../../../tool-registry/definitions.js";
|
|
16
|
+
import * as fixtures from "../fixtures/index.js";
|
|
17
|
+
import { register, SKIPPED_SCENARIOS, cellKey } from "../scenarios.js";
|
|
18
|
+
|
|
19
|
+
const K = [
|
|
20
|
+
{ platform: "linux", dash: "absent", pi: "present-valid", settings: "valid", env: "normal" },
|
|
21
|
+
] as const;
|
|
22
|
+
for (const cell of K) {
|
|
23
|
+
register(cell, "families/k-dashboard-absent.test.ts");
|
|
24
|
+
SKIPPED_SCENARIOS.delete(cellKey(cell));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("Family K — dashboard absent", () => {
|
|
28
|
+
it("pi still resolves when no dashboard binary is installed", async () => {
|
|
29
|
+
const homedir = "/home/r";
|
|
30
|
+
await withFakeEnv(
|
|
31
|
+
{
|
|
32
|
+
platform: "linux",
|
|
33
|
+
homedir,
|
|
34
|
+
fs: fixtures.managedInstall({ homedir, platform: "linux" }),
|
|
35
|
+
},
|
|
36
|
+
(ctx) => {
|
|
37
|
+
const registry = ctx.createRegistry();
|
|
38
|
+
registerDefaultTools(registry, ctx.createStrategyDeps());
|
|
39
|
+
const res = registry.resolve("pi");
|
|
40
|
+
expect(res.ok).toBe(true);
|
|
41
|
+
expect(res.source).toBe("managed");
|
|
42
|
+
// "Dashboard absence" is not observable at the registry level —
|
|
43
|
+
// the dashboard isn't a registered tool. The observation
|
|
44
|
+
// happens at `dependency-detector.ts:detectPiDashboardCli()`
|
|
45
|
+
// via `which pi-dashboard`. Covered by dependency-detector
|
|
46
|
+
// unit tests, not this harness.
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family L — Instance coordination (per-HOME advisory lock).
|
|
3
|
+
*
|
|
4
|
+
* Covers the scenarios enumerated in
|
|
5
|
+
* `openspec/changes/single-dashboard-per-home/design.md §10`:
|
|
6
|
+
*
|
|
7
|
+
* L1 no prior dashboard → acquire
|
|
8
|
+
* L2 healthy dashboard same port → attach
|
|
9
|
+
* L3 healthy dashboard diff port → attach via metadata URL
|
|
10
|
+
* L4 stale lock (PID dead) → steal + start
|
|
11
|
+
* L5 stale PID + port free → steal + clean + start
|
|
12
|
+
* L6 stale PID + port taken by → identity mismatch error
|
|
13
|
+
* unrelated process
|
|
14
|
+
* L7 mDNS disabled → lock still works (same as L2)
|
|
15
|
+
* L9 multi-user → separate HOMEs, separate locks
|
|
16
|
+
* L10 HOME symlink → realpath canonicalization
|
|
17
|
+
* L11 identity mismatch → error, no attach, no start
|
|
18
|
+
* L12 corrupt metadata → treat as stale, steal
|
|
19
|
+
*
|
|
20
|
+
* L8 (concurrent launch) and L13 (permission denied) live in integration
|
|
21
|
+
* tests (`concurrent-launch.test.ts`, `crash-recovery.test.ts`) because
|
|
22
|
+
* they require real processes / real filesystems.
|
|
23
|
+
*
|
|
24
|
+
* Note: this family does NOT use the cube enumeration. The 5-axis cube
|
|
25
|
+
* does not model lock state; adding it would 4x the cell count. Family L
|
|
26
|
+
* is registered as a separate enumeration here (design decision: the
|
|
27
|
+
* simpler option from design §Precondition item 2b).
|
|
28
|
+
*/
|
|
29
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import os from "node:os";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import {
|
|
34
|
+
acquireOrAttach,
|
|
35
|
+
readMetadata,
|
|
36
|
+
writeMetadataAtomic,
|
|
37
|
+
canonicalHomedir,
|
|
38
|
+
getLockPath,
|
|
39
|
+
InstanceLockMismatchError,
|
|
40
|
+
type LockMetadata,
|
|
41
|
+
} from "../../../../../server/src/home-lock.js";
|
|
42
|
+
|
|
43
|
+
let tmpHome: string;
|
|
44
|
+
let lockPath: string;
|
|
45
|
+
let metaPath: string;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-family-l-"));
|
|
49
|
+
lockPath = path.join(tmpHome, ".pi", "dashboard", "server.lock");
|
|
50
|
+
metaPath = `${lockPath}.meta.json`;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const baseCfg = (over: Partial<Parameters<typeof acquireOrAttach>[0]> = {}) => ({
|
|
58
|
+
httpPort: 8000,
|
|
59
|
+
piPort: 9999,
|
|
60
|
+
version: "0.0.0-test",
|
|
61
|
+
hooks: {
|
|
62
|
+
lockPath, metaPath, staleMs: 500,
|
|
63
|
+
probeHealth: async () => ({ running: false }),
|
|
64
|
+
isProcessAlive: () => false,
|
|
65
|
+
...(over.hooks ?? {}),
|
|
66
|
+
},
|
|
67
|
+
...over,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("Family L — instance coordination", () => {
|
|
71
|
+
it("L1 — no prior dashboard: acquires cleanly", async () => {
|
|
72
|
+
const r = await acquireOrAttach(baseCfg());
|
|
73
|
+
expect(r.mode).toBe("acquired");
|
|
74
|
+
if (r.mode === "acquired") await r.release();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("L2 — healthy dashboard same port: attaches", async () => {
|
|
78
|
+
const first = await acquireOrAttach(baseCfg({ identity: "live-1" }));
|
|
79
|
+
expect(first.mode).toBe("acquired");
|
|
80
|
+
|
|
81
|
+
const second = await acquireOrAttach(baseCfg({
|
|
82
|
+
hooks: {
|
|
83
|
+
lockPath, metaPath, staleMs: 500,
|
|
84
|
+
isProcessAlive: () => true,
|
|
85
|
+
probeHealth: async () => ({ running: true, identity: "live-1", pid: process.pid }),
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
expect(second.mode).toBe("attach");
|
|
89
|
+
if (first.mode === "acquired") await first.release();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("L3 — healthy dashboard on different port: attaches via metadata URL", async () => {
|
|
93
|
+
const first = await acquireOrAttach(baseCfg({ identity: "live-3", httpPort: 8765 }));
|
|
94
|
+
expect(first.mode).toBe("acquired");
|
|
95
|
+
|
|
96
|
+
// New caller asks for port 9001, but lock meta says live on 8765.
|
|
97
|
+
const second = await acquireOrAttach(baseCfg({
|
|
98
|
+
httpPort: 9001,
|
|
99
|
+
hooks: {
|
|
100
|
+
lockPath, metaPath, staleMs: 500,
|
|
101
|
+
isProcessAlive: () => true,
|
|
102
|
+
// Probe only returns alive for the correct port (8765).
|
|
103
|
+
probeHealth: async (port) =>
|
|
104
|
+
port === 8765
|
|
105
|
+
? { running: true, identity: "live-3", pid: process.pid }
|
|
106
|
+
: { running: false },
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
109
|
+
expect(second.mode).toBe("attach");
|
|
110
|
+
if (second.mode === "attach") {
|
|
111
|
+
expect(second.meta.httpPort).toBe(8765);
|
|
112
|
+
expect(second.meta.url).toContain("8765");
|
|
113
|
+
}
|
|
114
|
+
if (first.mode === "acquired") await first.release();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("L4 — stale lock, PID dead: steals + starts", async () => {
|
|
118
|
+
const first = await acquireOrAttach(baseCfg({ identity: "dead-holder" }));
|
|
119
|
+
expect(first.mode).toBe("acquired");
|
|
120
|
+
// Don't release — simulate crash.
|
|
121
|
+
await new Promise(r => setTimeout(r, 50));
|
|
122
|
+
|
|
123
|
+
const second = await acquireOrAttach(baseCfg({
|
|
124
|
+
hooks: {
|
|
125
|
+
lockPath, metaPath, staleMs: 1,
|
|
126
|
+
isProcessAlive: () => false,
|
|
127
|
+
probeHealth: async () => ({ running: false }),
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
expect(second.mode).toBe("acquired");
|
|
131
|
+
if (second.mode === "acquired") await second.release();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("L5 — stale PID + port free: clean steal", async () => {
|
|
135
|
+
// Write stale metadata manually, with no active lockfile yet.
|
|
136
|
+
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
|
|
137
|
+
const staleMeta: LockMetadata = {
|
|
138
|
+
pid: 1, ppid: 0, httpPort: 8000, piPort: 9999,
|
|
139
|
+
startedAt: 0, identity: "ghost", version: "0", url: "http://localhost:8000", hostname: "h",
|
|
140
|
+
};
|
|
141
|
+
writeMetadataAtomic(staleMeta, metaPath);
|
|
142
|
+
|
|
143
|
+
const r = await acquireOrAttach(baseCfg());
|
|
144
|
+
expect(r.mode).toBe("acquired");
|
|
145
|
+
if (r.mode === "acquired") {
|
|
146
|
+
expect(r.meta.identity).not.toBe("ghost");
|
|
147
|
+
await r.release();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("L6 — stale PID + port taken by unrelated process: identity mismatch", async () => {
|
|
152
|
+
const first = await acquireOrAttach(baseCfg({ identity: "legit" }));
|
|
153
|
+
expect(first.mode).toBe("acquired");
|
|
154
|
+
|
|
155
|
+
// Simulate: lock is alive-ish, but health returns a different identity
|
|
156
|
+
// (port commandeered by something else with same pid reuse).
|
|
157
|
+
await expect(
|
|
158
|
+
acquireOrAttach(baseCfg({
|
|
159
|
+
hooks: {
|
|
160
|
+
lockPath, metaPath, staleMs: 500,
|
|
161
|
+
isProcessAlive: () => true,
|
|
162
|
+
probeHealth: async () => ({ running: true, identity: "hostile-squatter" }),
|
|
163
|
+
},
|
|
164
|
+
})),
|
|
165
|
+
).rejects.toBeInstanceOf(InstanceLockMismatchError);
|
|
166
|
+
|
|
167
|
+
if (first.mode === "acquired") await first.release();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("L7 — mDNS disabled: lock path unaffected (parity with L2)", async () => {
|
|
171
|
+
// mDNS is orthogonal to the lock; exercise the same L2 flow to document
|
|
172
|
+
// that lock acquisition does NOT depend on mDNS discovery.
|
|
173
|
+
const first = await acquireOrAttach(baseCfg({ identity: "no-mdns" }));
|
|
174
|
+
expect(first.mode).toBe("acquired");
|
|
175
|
+
|
|
176
|
+
const second = await acquireOrAttach(baseCfg({
|
|
177
|
+
hooks: {
|
|
178
|
+
lockPath, metaPath, staleMs: 500,
|
|
179
|
+
isProcessAlive: () => true,
|
|
180
|
+
probeHealth: async () => ({ running: true, identity: "no-mdns", pid: process.pid }),
|
|
181
|
+
},
|
|
182
|
+
}));
|
|
183
|
+
expect(second.mode).toBe("attach");
|
|
184
|
+
if (first.mode === "acquired") await first.release();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("L9 — multi-user: two HOMEs, two locks, no interference", async () => {
|
|
188
|
+
const homeA = fs.mkdtempSync(path.join(os.tmpdir(), "pi-user-a-"));
|
|
189
|
+
const homeB = fs.mkdtempSync(path.join(os.tmpdir(), "pi-user-b-"));
|
|
190
|
+
try {
|
|
191
|
+
const lockA = path.join(homeA, ".pi", "dashboard", "server.lock");
|
|
192
|
+
const metaA = `${lockA}.meta.json`;
|
|
193
|
+
const lockB = path.join(homeB, ".pi", "dashboard", "server.lock");
|
|
194
|
+
const metaB = `${lockB}.meta.json`;
|
|
195
|
+
|
|
196
|
+
const a = await acquireOrAttach({
|
|
197
|
+
httpPort: 8000, piPort: 9999, version: "t",
|
|
198
|
+
hooks: { lockPath: lockA, metaPath: metaA, staleMs: 500 },
|
|
199
|
+
});
|
|
200
|
+
const b = await acquireOrAttach({
|
|
201
|
+
httpPort: 8001, piPort: 9998, version: "t",
|
|
202
|
+
hooks: { lockPath: lockB, metaPath: metaB, staleMs: 500 },
|
|
203
|
+
});
|
|
204
|
+
expect(a.mode).toBe("acquired");
|
|
205
|
+
expect(b.mode).toBe("acquired");
|
|
206
|
+
if (a.mode === "acquired") await a.release();
|
|
207
|
+
if (b.mode === "acquired") await b.release();
|
|
208
|
+
} finally {
|
|
209
|
+
fs.rmSync(homeA, { recursive: true, force: true });
|
|
210
|
+
fs.rmSync(homeB, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("L10 — HOME symlink: realpath canonicalizes to the same lock", async () => {
|
|
215
|
+
const real = fs.mkdtempSync(path.join(os.tmpdir(), "pi-real-home-"));
|
|
216
|
+
const link = path.join(os.tmpdir(), `pi-link-home-${Date.now()}-${Math.random()}`);
|
|
217
|
+
fs.symlinkSync(real, link);
|
|
218
|
+
try {
|
|
219
|
+
// Both paths resolve via realpath → same canonical HOME.
|
|
220
|
+
expect(fs.realpathSync(link)).toBe(fs.realpathSync(real));
|
|
221
|
+
// canonicalHomedir() uses os.homedir() so we can't mock without
|
|
222
|
+
// globals; the invariant is tested via fs.realpathSync equivalence.
|
|
223
|
+
expect(typeof canonicalHomedir()).toBe("string");
|
|
224
|
+
} finally {
|
|
225
|
+
try { fs.unlinkSync(link); } catch { /* ignore */ }
|
|
226
|
+
fs.rmSync(real, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("L11 — identity mismatch: throws, no attach, no start", async () => {
|
|
231
|
+
const first = await acquireOrAttach(baseCfg({ identity: "me" }));
|
|
232
|
+
expect(first.mode).toBe("acquired");
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
acquireOrAttach(baseCfg({
|
|
236
|
+
hooks: {
|
|
237
|
+
lockPath, metaPath, staleMs: 500,
|
|
238
|
+
isProcessAlive: () => true,
|
|
239
|
+
probeHealth: async () => ({ running: true, identity: "not-me", pid: 99999 }),
|
|
240
|
+
},
|
|
241
|
+
})),
|
|
242
|
+
).rejects.toBeInstanceOf(InstanceLockMismatchError);
|
|
243
|
+
|
|
244
|
+
// Verify no new metadata has been written with a different identity
|
|
245
|
+
const meta = readMetadata(metaPath);
|
|
246
|
+
expect(meta?.identity).toBe("me");
|
|
247
|
+
|
|
248
|
+
if (first.mode === "acquired") await first.release();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("L12 — corrupt metadata: treated as stale, steal", async () => {
|
|
252
|
+
const first = await acquireOrAttach(baseCfg({ identity: "intact" }));
|
|
253
|
+
expect(first.mode).toBe("acquired");
|
|
254
|
+
|
|
255
|
+
// Corrupt the metadata file.
|
|
256
|
+
fs.writeFileSync(metaPath, "{broken json");
|
|
257
|
+
await new Promise(r => setTimeout(r, 50));
|
|
258
|
+
|
|
259
|
+
const second = await acquireOrAttach(baseCfg({
|
|
260
|
+
hooks: {
|
|
261
|
+
lockPath, metaPath, staleMs: 1,
|
|
262
|
+
isProcessAlive: () => false,
|
|
263
|
+
probeHealth: async () => ({ running: false }),
|
|
264
|
+
},
|
|
265
|
+
}));
|
|
266
|
+
expect(second.mode).toBe("acquired");
|
|
267
|
+
if (second.mode === "acquired") {
|
|
268
|
+
expect(second.meta.identity).not.toBe("intact");
|
|
269
|
+
await second.release();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: dev monorepo layout (what developers see running from source).
|
|
3
|
+
*
|
|
4
|
+
* <root>/
|
|
5
|
+
* node_modules/
|
|
6
|
+
* @mariozechner/pi-coding-agent/dist/cli.js
|
|
7
|
+
* openspec/dist/cli.js
|
|
8
|
+
* tsx/dist/cli.mjs
|
|
9
|
+
* packages/
|
|
10
|
+
* shared/ server/ extension/ electron/
|
|
11
|
+
*
|
|
12
|
+
* Used for Family C scenarios (bare-import resolves pi via workspace
|
|
13
|
+
* node_modules).
|
|
14
|
+
*/
|
|
15
|
+
import posix from "node:path/posix";
|
|
16
|
+
import win32 from "node:path/win32";
|
|
17
|
+
import type { FsRecord } from "../harness.js";
|
|
18
|
+
import { openspecPackageJson, piPackageJson, type PiVersionSpec } from "./pi-versions.js";
|
|
19
|
+
|
|
20
|
+
export interface DevMonorepoSpec {
|
|
21
|
+
root: string;
|
|
22
|
+
platform: NodeJS.Platform;
|
|
23
|
+
pi?: PiVersionSpec;
|
|
24
|
+
openspec?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function devMonorepo(spec: DevMonorepoSpec): FsRecord {
|
|
28
|
+
const p = spec.platform === "win32" ? win32 : posix;
|
|
29
|
+
const nodeModules = p.join(spec.root, "node_modules");
|
|
30
|
+
const out: Record<string, string> = {};
|
|
31
|
+
|
|
32
|
+
// Root package.json (workspace)
|
|
33
|
+
out[p.join(spec.root, "package.json")] = JSON.stringify({
|
|
34
|
+
name: "pi-agent-dashboard-root",
|
|
35
|
+
private: true,
|
|
36
|
+
workspaces: ["packages/*"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Workspace packages
|
|
40
|
+
for (const pkg of ["shared", "server", "extension", "electron", "client"]) {
|
|
41
|
+
out[p.join(spec.root, "packages", pkg, "package.json")] = JSON.stringify({
|
|
42
|
+
name: `@blackbelt-technology/pi-dashboard-${pkg}`,
|
|
43
|
+
version: "0.4.0",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Hoisted deps
|
|
48
|
+
const piDir = p.join(nodeModules, "@mariozechner", "pi-coding-agent");
|
|
49
|
+
out[p.join(piDir, "package.json")] = piPackageJson(spec.pi);
|
|
50
|
+
out[p.join(piDir, "dist", "cli.js")] = "#!/usr/bin/env node";
|
|
51
|
+
|
|
52
|
+
const osDir = p.join(nodeModules, "openspec");
|
|
53
|
+
out[p.join(osDir, "package.json")] = openspecPackageJson(spec.openspec ?? "0.4.1");
|
|
54
|
+
out[p.join(osDir, "dist", "cli.js")] = "#!/usr/bin/env node";
|
|
55
|
+
out[p.join(osDir, "dist", "index.js")] = "module.exports = {};";
|
|
56
|
+
|
|
57
|
+
return out;
|
|
58
|
+
}
|