@blackbelt-technology/pi-agent-dashboard 0.2.9 → 0.4.0
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 +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- 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 +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/prompt-expander.ts +25 -4
- 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 +71 -27
- package/packages/server/package.json +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -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 +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -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__/cors.test.ts +34 -2
- 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-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -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__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- 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__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -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-auth-routes.test.ts +13 -3
- 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__/recommended-routes.test.ts +389 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -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__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- 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 +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- 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 +108 -9
- package/packages/server/src/browser-gateway.ts +16 -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 +256 -32
- 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 +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -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/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -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 +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- 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 +363 -26
- 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 +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- 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 +59 -3
- 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__/openspec-poller.test.ts +44 -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 +156 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -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 +93 -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 +71 -49
- 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 +15 -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/recommended-extensions.ts +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -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
- package/packages/shared/src/types.ts +7 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: Electron packaged resources layout.
|
|
3
|
+
*
|
|
4
|
+
* macOS: /Applications/PI Dashboard.app/Contents/Resources/server/...
|
|
5
|
+
* Linux: /usr/lib/pi-dashboard/resources/server/...
|
|
6
|
+
* Windows: C:\Program Files\PI Dashboard\resources\server\...
|
|
7
|
+
* AppImage: /tmp/.mount_PIxxxx/resources/server/... (temp, unstable)
|
|
8
|
+
*
|
|
9
|
+
* The bundled extension lives alongside at
|
|
10
|
+
* `<resourcesPath>/server/packages/extension/`.
|
|
11
|
+
*/
|
|
12
|
+
import posix from "node:path/posix";
|
|
13
|
+
import win32 from "node:path/win32";
|
|
14
|
+
import type { FsRecord } from "../harness.js";
|
|
15
|
+
|
|
16
|
+
export interface ElectronLayoutSpec {
|
|
17
|
+
platform: NodeJS.Platform;
|
|
18
|
+
/** If true, simulate AppImage temp-mount path. */
|
|
19
|
+
appimage?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resourcesRoot(spec: ElectronLayoutSpec): string {
|
|
23
|
+
if (spec.appimage) {
|
|
24
|
+
return "/tmp/.mount_PIxxxx/resources";
|
|
25
|
+
}
|
|
26
|
+
switch (spec.platform) {
|
|
27
|
+
case "darwin":
|
|
28
|
+
return "/Applications/PI Dashboard.app/Contents/Resources";
|
|
29
|
+
case "win32":
|
|
30
|
+
return "C:\\Program Files\\PI Dashboard\\resources";
|
|
31
|
+
default:
|
|
32
|
+
return "/usr/lib/pi-dashboard/resources";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function electronPackaged(spec: ElectronLayoutSpec): FsRecord {
|
|
37
|
+
const p = spec.platform === "win32" ? win32 : posix;
|
|
38
|
+
const resources = resourcesRoot(spec);
|
|
39
|
+
const serverDir = p.join(resources, "server");
|
|
40
|
+
const extensionDir = p.join(serverDir, "packages", "extension");
|
|
41
|
+
const out: Record<string, string> = {};
|
|
42
|
+
|
|
43
|
+
// Bundled server package.json
|
|
44
|
+
out[p.join(serverDir, "package.json")] = JSON.stringify({
|
|
45
|
+
name: "@blackbelt-technology/pi-agent-dashboard-root",
|
|
46
|
+
version: "0.4.0",
|
|
47
|
+
private: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Bundled server CLI source
|
|
51
|
+
out[p.join(serverDir, "packages", "server", "package.json")] = JSON.stringify({
|
|
52
|
+
name: "@blackbelt-technology/pi-dashboard-server",
|
|
53
|
+
version: "0.4.0",
|
|
54
|
+
});
|
|
55
|
+
out[p.join(serverDir, "packages", "server", "src", "cli.ts")] = "// cli";
|
|
56
|
+
|
|
57
|
+
// Bundled bridge extension
|
|
58
|
+
out[p.join(extensionDir, "package.json")] = JSON.stringify({
|
|
59
|
+
name: "@blackbelt-technology/pi-dashboard-extension",
|
|
60
|
+
version: "0.4.0",
|
|
61
|
+
});
|
|
62
|
+
out[p.join(extensionDir, "src", "bridge.ts")] = "// bridge";
|
|
63
|
+
|
|
64
|
+
// Bundled Node.js (minimal)
|
|
65
|
+
const nodeBin = spec.platform === "win32"
|
|
66
|
+
? p.join(resources, "node", "bin", "node.exe")
|
|
67
|
+
: p.join(resources, "node", "bin", "node");
|
|
68
|
+
out[nodeBin] = "\x7fELF"; // binary-ish marker
|
|
69
|
+
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Returns the resolved `resourcesPath` + extension path for assertions. */
|
|
74
|
+
export function electronPaths(spec: ElectronLayoutSpec): {
|
|
75
|
+
resources: string;
|
|
76
|
+
serverDir: string;
|
|
77
|
+
extensionDir: string;
|
|
78
|
+
} {
|
|
79
|
+
const p = spec.platform === "win32" ? win32 : posix;
|
|
80
|
+
const resources = resourcesRoot(spec);
|
|
81
|
+
const serverDir = p.join(resources, "server");
|
|
82
|
+
const extensionDir = p.join(serverDir, "packages", "extension");
|
|
83
|
+
return { resources, serverDir, extensionDir };
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel export for bootstrap fixtures. Import as `import * as fixtures`.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./pi-versions.js";
|
|
5
|
+
export * from "./managed-install.js";
|
|
6
|
+
export * from "./npm-global-layout.js";
|
|
7
|
+
export * from "./electron-layout.js";
|
|
8
|
+
export * from "./dev-monorepo.js";
|
|
9
|
+
export * from "./settings-json.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: managed install at `<homedir>/.pi-dashboard/`.
|
|
3
|
+
*
|
|
4
|
+
* Produces the fs layout a user gets after running the Electron wizard
|
|
5
|
+
* or proposal-2's CLI first-run bootstrap: node_modules/@mariozechner/
|
|
6
|
+
* pi-coding-agent + node_modules/.bin shims.
|
|
7
|
+
*/
|
|
8
|
+
import posix from "node:path/posix";
|
|
9
|
+
import win32 from "node:path/win32";
|
|
10
|
+
import type { FsRecord } from "../harness.js";
|
|
11
|
+
import { openspecPackageJson, piPackageJson, type PiVersionSpec } from "./pi-versions.js";
|
|
12
|
+
|
|
13
|
+
export interface ManagedInstallSpec {
|
|
14
|
+
homedir: string;
|
|
15
|
+
platform: NodeJS.Platform;
|
|
16
|
+
pi?: PiVersionSpec | false;
|
|
17
|
+
openspec?: string | false;
|
|
18
|
+
tsx?: string | false;
|
|
19
|
+
/**
|
|
20
|
+
* If `true`, write just the package.json for pi — no `dist/cli.js`.
|
|
21
|
+
* Simulates an install that was interrupted mid-extract (scenario E2).
|
|
22
|
+
*/
|
|
23
|
+
piPartial?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function managedInstall(spec: ManagedInstallSpec): FsRecord {
|
|
27
|
+
const p = spec.platform === "win32" ? win32 : posix;
|
|
28
|
+
const out: Record<string, string> = {};
|
|
29
|
+
const managedDir = p.join(spec.homedir, ".pi-dashboard");
|
|
30
|
+
const nodeModules = p.join(managedDir, "node_modules");
|
|
31
|
+
const binDir = p.join(nodeModules, ".bin");
|
|
32
|
+
|
|
33
|
+
out[p.join(managedDir, "package.json")] = JSON.stringify({
|
|
34
|
+
name: "pi-dashboard-managed",
|
|
35
|
+
private: true,
|
|
36
|
+
type: "module",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (spec.pi !== false) {
|
|
40
|
+
const piSpec = spec.pi ?? {};
|
|
41
|
+
const piDir = p.join(nodeModules, "@mariozechner", "pi-coding-agent");
|
|
42
|
+
out[p.join(piDir, "package.json")] = piPackageJson(piSpec);
|
|
43
|
+
if (!spec.piPartial) {
|
|
44
|
+
out[p.join(piDir, "dist", "cli.js")] = "#!/usr/bin/env node\n// pi cli stub";
|
|
45
|
+
// bin shim
|
|
46
|
+
if (spec.platform === "win32") {
|
|
47
|
+
out[p.join(binDir, "pi.cmd")] = "@node %~dp0\\..\\@mariozechner\\pi-coding-agent\\dist\\cli.js %*";
|
|
48
|
+
} else {
|
|
49
|
+
out[p.join(binDir, "pi")] = "#!/bin/sh\nexec node ../@mariozechner/pi-coding-agent/dist/cli.js \"$@\"";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (spec.openspec !== false) {
|
|
55
|
+
const v = spec.openspec ?? "0.4.1";
|
|
56
|
+
const dir = p.join(nodeModules, "openspec");
|
|
57
|
+
out[p.join(dir, "package.json")] = openspecPackageJson(v);
|
|
58
|
+
out[p.join(dir, "dist", "cli.js")] = "#!/usr/bin/env node";
|
|
59
|
+
out[p.join(dir, "dist", "index.js")] = "module.exports = {};";
|
|
60
|
+
if (spec.platform === "win32") {
|
|
61
|
+
out[p.join(binDir, "openspec.cmd")] = "@node %~dp0\\..\\openspec\\dist\\cli.js %*";
|
|
62
|
+
} else {
|
|
63
|
+
out[p.join(binDir, "openspec")] = "#!/bin/sh\nexec node ../openspec/dist/cli.js \"$@\"";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (spec.tsx !== false) {
|
|
68
|
+
const v = spec.tsx ?? "4.20.0";
|
|
69
|
+
const dir = p.join(nodeModules, "tsx");
|
|
70
|
+
out[p.join(dir, "package.json")] = JSON.stringify({
|
|
71
|
+
name: "tsx",
|
|
72
|
+
version: v,
|
|
73
|
+
main: "dist/cli.mjs",
|
|
74
|
+
bin: { tsx: "dist/cli.mjs" },
|
|
75
|
+
});
|
|
76
|
+
out[p.join(dir, "dist", "cli.mjs")] = "#!/usr/bin/env node";
|
|
77
|
+
if (spec.platform === "win32") {
|
|
78
|
+
out[p.join(binDir, "tsx.cmd")] = "@node %~dp0\\..\\tsx\\dist\\cli.mjs %*";
|
|
79
|
+
} else {
|
|
80
|
+
out[p.join(binDir, "tsx")] = "#!/bin/sh\nexec node ../tsx/dist/cli.mjs \"$@\"";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: global npm install layout.
|
|
3
|
+
*
|
|
4
|
+
* Posix: /usr/lib/node_modules/<pkg>/...
|
|
5
|
+
* Windows: %APPDATA%\Roaming\npm\node_modules\<pkg>\...
|
|
6
|
+
* with pi.cmd / openspec.cmd shims in %APPDATA%\Roaming\npm\
|
|
7
|
+
*
|
|
8
|
+
* For the "%ProgramFiles%\nodejs" variant on Windows, see
|
|
9
|
+
* `npmGlobalWindowsProgramFiles` — npm is installed there when users pick
|
|
10
|
+
* "Add to PATH" during Node.js installer.
|
|
11
|
+
*/
|
|
12
|
+
import posix from "node:path/posix";
|
|
13
|
+
import win32 from "node:path/win32";
|
|
14
|
+
import type { FsRecord } from "../harness.js";
|
|
15
|
+
import { openspecPackageJson, piPackageJson, type PiVersionSpec } from "./pi-versions.js";
|
|
16
|
+
|
|
17
|
+
interface PosixSpec {
|
|
18
|
+
pi?: PiVersionSpec | false;
|
|
19
|
+
openspec?: string | false;
|
|
20
|
+
/** Install root; defaults to `/usr/lib/node_modules`. */
|
|
21
|
+
root?: string;
|
|
22
|
+
binDir?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WindowsSpec {
|
|
26
|
+
pi?: PiVersionSpec | false;
|
|
27
|
+
openspec?: string | false;
|
|
28
|
+
dashboard?: boolean;
|
|
29
|
+
/** Where %APPDATA%\Roaming\npm lives. */
|
|
30
|
+
npmDir?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Unix global npm install. Root defaults to `/usr/lib/node_modules`,
|
|
35
|
+
* binaries at `/usr/local/bin`.
|
|
36
|
+
*/
|
|
37
|
+
export function npmGlobalUnix(spec: PosixSpec = {}): FsRecord {
|
|
38
|
+
const p = posix;
|
|
39
|
+
const root = spec.root ?? "/usr/lib/node_modules";
|
|
40
|
+
const binDir = spec.binDir ?? "/usr/local/bin";
|
|
41
|
+
const out: Record<string, string> = {};
|
|
42
|
+
|
|
43
|
+
if (spec.pi !== false) {
|
|
44
|
+
const piSpec = spec.pi ?? {};
|
|
45
|
+
const piDir = p.join(root, "@mariozechner", "pi-coding-agent");
|
|
46
|
+
out[p.join(piDir, "package.json")] = piPackageJson(piSpec);
|
|
47
|
+
out[p.join(piDir, "dist", "cli.js")] = "#!/usr/bin/env node";
|
|
48
|
+
out[p.join(binDir, "pi")] = "#!/bin/sh\nexec node ...";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (spec.openspec !== false) {
|
|
52
|
+
// openspec tool definition uses package `@fission-ai/openspec`
|
|
53
|
+
// with entry `bin/openspec.js` (see definitions.ts).
|
|
54
|
+
const dir = p.join(root, "@fission-ai", "openspec");
|
|
55
|
+
out[p.join(dir, "package.json")] = JSON.stringify({
|
|
56
|
+
name: "@fission-ai/openspec",
|
|
57
|
+
version: spec.openspec ?? "0.4.1",
|
|
58
|
+
bin: { openspec: "bin/openspec.js" },
|
|
59
|
+
});
|
|
60
|
+
out[p.join(dir, "bin", "openspec.js")] = "#!/usr/bin/env node";
|
|
61
|
+
out[p.join(binDir, "openspec")] = "#!/bin/sh\nexec node ...";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Windows AppData\Roaming\npm layout — the default for MSI installs of
|
|
69
|
+
* Node.js. `npmDir` overrides the location.
|
|
70
|
+
*/
|
|
71
|
+
export function npmGlobalWindowsAppData(
|
|
72
|
+
homedir: string,
|
|
73
|
+
spec: WindowsSpec = {},
|
|
74
|
+
): FsRecord {
|
|
75
|
+
const p = win32;
|
|
76
|
+
const npmDir = spec.npmDir ?? p.join(homedir, "AppData", "Roaming", "npm");
|
|
77
|
+
const nodeModules = p.join(npmDir, "node_modules");
|
|
78
|
+
const out: Record<string, string> = {};
|
|
79
|
+
|
|
80
|
+
if (spec.pi !== false) {
|
|
81
|
+
const piSpec = spec.pi ?? {};
|
|
82
|
+
const piDir = p.join(nodeModules, "@mariozechner", "pi-coding-agent");
|
|
83
|
+
out[p.join(piDir, "package.json")] = piPackageJson(piSpec);
|
|
84
|
+
out[p.join(piDir, "dist", "cli.js")] = "#!/usr/bin/env node";
|
|
85
|
+
out[p.join(npmDir, "pi.cmd")] = "@node %~dp0\\node_modules\\@mariozechner\\pi-coding-agent\\dist\\cli.js %*";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (spec.openspec !== false) {
|
|
89
|
+
const dir = p.join(nodeModules, "@fission-ai", "openspec");
|
|
90
|
+
out[p.join(dir, "package.json")] = JSON.stringify({
|
|
91
|
+
name: "@fission-ai/openspec",
|
|
92
|
+
version: spec.openspec ?? "0.4.1",
|
|
93
|
+
bin: { openspec: "bin/openspec.js" },
|
|
94
|
+
});
|
|
95
|
+
out[p.join(dir, "bin", "openspec.js")] = "#!/usr/bin/env node";
|
|
96
|
+
out[p.join(npmDir, "openspec.cmd")] = "@node %~dp0\\node_modules\\@fission-ai\\openspec\\bin\\openspec.js %*";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (spec.dashboard) {
|
|
100
|
+
const dir = p.join(nodeModules, "@blackbelt-technology", "pi-agent-dashboard");
|
|
101
|
+
out[p.join(dir, "package.json")] = JSON.stringify({
|
|
102
|
+
name: "@blackbelt-technology/pi-agent-dashboard",
|
|
103
|
+
version: "0.4.0",
|
|
104
|
+
bin: { "pi-dashboard": "packages/server/dist/cli.js" },
|
|
105
|
+
});
|
|
106
|
+
out[p.join(dir, "packages", "server", "dist", "cli.js")] = "#!/usr/bin/env node";
|
|
107
|
+
out[p.join(npmDir, "pi-dashboard.cmd")] = "@node %~dp0\\node_modules\\@blackbelt-technology\\pi-agent-dashboard\\packages\\server\\dist\\cli.js %*";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Windows %ProgramFiles%\nodejs\node_modules layout — picked when Node
|
|
115
|
+
* installer chose "install as system tool."
|
|
116
|
+
*/
|
|
117
|
+
export function npmGlobalWindowsProgramFiles(spec: WindowsSpec = {}): FsRecord {
|
|
118
|
+
return npmGlobalWindowsAppData("C:\\Program Files\\nodejs_", {
|
|
119
|
+
...spec,
|
|
120
|
+
npmDir: "C:\\Program Files\\nodejs",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: stamp a specific version into a pi-coding-agent package.json.
|
|
3
|
+
* Returns the package.json JSON string ready for FsRecord insertion.
|
|
4
|
+
*/
|
|
5
|
+
export interface PiVersionSpec {
|
|
6
|
+
name?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
bin?: Record<string, string>;
|
|
9
|
+
main?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function piPackageJson(spec: PiVersionSpec = {}): string {
|
|
13
|
+
return JSON.stringify(
|
|
14
|
+
{
|
|
15
|
+
name: spec.name ?? "@mariozechner/pi-coding-agent",
|
|
16
|
+
version: spec.version ?? "0.6.3",
|
|
17
|
+
main: spec.main ?? "dist/cli.js",
|
|
18
|
+
bin: spec.bin ?? { pi: "dist/cli.js" },
|
|
19
|
+
},
|
|
20
|
+
null,
|
|
21
|
+
2,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function openspecPackageJson(version = "0.4.1"): string {
|
|
26
|
+
return JSON.stringify(
|
|
27
|
+
{
|
|
28
|
+
name: "openspec",
|
|
29
|
+
version,
|
|
30
|
+
main: "dist/index.js",
|
|
31
|
+
bin: { openspec: "dist/cli.js" },
|
|
32
|
+
},
|
|
33
|
+
null,
|
|
34
|
+
2,
|
|
35
|
+
);
|
|
36
|
+
}
|