@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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-spawn mechanism selection.
|
|
3
|
+
*
|
|
4
|
+
* The user expresses preference via a two-valued config type
|
|
5
|
+
* (`SpawnStrategy` = "tmux" | "headless"). The dashboard internally
|
|
6
|
+
* decides WHICH actual mechanism to use given the OS and what's
|
|
7
|
+
* available on this host. This module is the single source of truth
|
|
8
|
+
* for that decision.
|
|
9
|
+
*
|
|
10
|
+
* Mechanisms:
|
|
11
|
+
* • "tmux" — Unix terminal multiplexer (Linux, macOS)
|
|
12
|
+
* • "wt" — Windows Terminal new-tab (Win10/11)
|
|
13
|
+
* • "wsl-tmux" — WSL-hosted tmux (Windows, niche)
|
|
14
|
+
* • "headless" — RPC-mode pi, no TTY, bridge over WebSocket
|
|
15
|
+
*
|
|
16
|
+
* `selectMechanism` is pure: no I/O, no subprocess calls. Availability
|
|
17
|
+
* is determined by the caller (typically via `ToolRegistry.resolve`)
|
|
18
|
+
* and passed in. This keeps the decision trivially testable.
|
|
19
|
+
*
|
|
20
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type SpawnMechanism = "tmux" | "wt" | "wsl-tmux" | "headless";
|
|
24
|
+
|
|
25
|
+
/** User-visible config value (from `SpawnStrategy` in shared/config.ts). */
|
|
26
|
+
export type UserSpawnStrategy = "tmux" | "headless";
|
|
27
|
+
|
|
28
|
+
export interface SpawnMechanismContext {
|
|
29
|
+
platform: NodeJS.Platform;
|
|
30
|
+
userStrategy: UserSpawnStrategy;
|
|
31
|
+
electronMode: boolean;
|
|
32
|
+
available: {
|
|
33
|
+
tmux: boolean;
|
|
34
|
+
wt: boolean;
|
|
35
|
+
wslTmux: boolean;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Select one spawn mechanism for this platform given the user's
|
|
41
|
+
* preference, the electron-mode flag, and tool availability.
|
|
42
|
+
*
|
|
43
|
+
* Rules (in order):
|
|
44
|
+
* 1. electronMode forces "headless".
|
|
45
|
+
* 2. userStrategy "headless" forces "headless".
|
|
46
|
+
* 3. Unix (linux/darwin): tmux if available, else headless.
|
|
47
|
+
* 4. Windows: wt > wsl-tmux > headless.
|
|
48
|
+
* 5. Any other platform falls back to headless.
|
|
49
|
+
*/
|
|
50
|
+
export function selectMechanism(ctx: SpawnMechanismContext): SpawnMechanism {
|
|
51
|
+
if (ctx.electronMode) return "headless";
|
|
52
|
+
if (ctx.userStrategy === "headless") return "headless";
|
|
53
|
+
|
|
54
|
+
if (ctx.platform === "linux" || ctx.platform === "darwin") {
|
|
55
|
+
return ctx.available.tmux ? "tmux" : "headless";
|
|
56
|
+
}
|
|
57
|
+
if (ctx.platform === "win32") {
|
|
58
|
+
if (ctx.available.wt) return "wt";
|
|
59
|
+
if (ctx.available.wslTmux) return "wsl-tmux";
|
|
60
|
+
return "headless";
|
|
61
|
+
}
|
|
62
|
+
return "headless";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Windows Terminal argv builder ───────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export interface WtArgsOptions {
|
|
68
|
+
/** Absolute cwd for the new tab. Spaces / parens / quotes are safe in argv form. */
|
|
69
|
+
cwd: string;
|
|
70
|
+
/** Tab title, typically the basename of cwd. */
|
|
71
|
+
title: string;
|
|
72
|
+
/**
|
|
73
|
+
* Pre-resolved pi argv: typically [node.exe, cli.js, --mode?, rpc?, --fork?, file?].
|
|
74
|
+
* Interactive wt sessions OMIT --mode rpc so pi runs its TUI.
|
|
75
|
+
*/
|
|
76
|
+
piArgv: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build argv (NOT a shell string) to invoke Windows Terminal so it opens
|
|
81
|
+
* a new tab in the existing WT window and runs `piArgv` there.
|
|
82
|
+
*
|
|
83
|
+
* Design notes:
|
|
84
|
+
* • argv form — passed to spawn with shell:false, so wt re-parses it
|
|
85
|
+
* internally. No need to escape spaces, semicolons, or quotes in cwd.
|
|
86
|
+
* • `-w 0` reuses the most-recently-used WT window; new tab, not new
|
|
87
|
+
* window. Matches tmux `new-window` semantics.
|
|
88
|
+
* • No `-p <profile>` — respect the user's default WT profile
|
|
89
|
+
* (cmd / pwsh / WSL).
|
|
90
|
+
* • `--` sentinel before piArgv so any `-` or `/` prefix in piArgv
|
|
91
|
+
* can't be misparsed as a wt option.
|
|
92
|
+
*/
|
|
93
|
+
export function buildWtArgs(opts: WtArgsOptions): string[] {
|
|
94
|
+
return [
|
|
95
|
+
"-w", "0",
|
|
96
|
+
"new-tab",
|
|
97
|
+
"-d", opts.cwd,
|
|
98
|
+
"--title", opts.title,
|
|
99
|
+
"--",
|
|
100
|
+
...opts.piArgv,
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Shared helper: append session/fork flags uniformly ─────────────────────
|
|
105
|
+
|
|
106
|
+
export interface SessionFlags {
|
|
107
|
+
sessionFile?: string;
|
|
108
|
+
mode?: "continue" | "fork";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Return `["--session", file]` or `["--fork", file]` or `[]`.
|
|
113
|
+
* Every mechanism MUST use this to append flags; dropping them silently
|
|
114
|
+
* is the exact bug that motivated this change (B1, B2).
|
|
115
|
+
*/
|
|
116
|
+
export function sessionFlagsToArgv(flags: SessionFlags): string[] {
|
|
117
|
+
if (flags.sessionFile && flags.mode === "continue") {
|
|
118
|
+
return ["--session", flags.sessionFile];
|
|
119
|
+
}
|
|
120
|
+
if (flags.sessionFile && flags.mode === "fork") {
|
|
121
|
+
return ["--fork", flags.sessionFile];
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess adapter — strategy pattern for OS-aware subprocess invocation.
|
|
3
|
+
*
|
|
4
|
+
* The adapter is the single point of entry for spawning any subprocess
|
|
5
|
+
* from dashboard code or from libraries we wrap. It dispatches to a
|
|
6
|
+
* platform-specific implementation:
|
|
7
|
+
*
|
|
8
|
+
* - Windows: `.cmd`/`.bat` shims go through explicit `cmd.exe /d /s /c`
|
|
9
|
+
* invocation with `windowsHide: true` and `shell: false` (the only
|
|
10
|
+
* reliable way to avoid Node issue #21825's flashing console).
|
|
11
|
+
* Native `.exe`s spawn directly.
|
|
12
|
+
* - Unix: direct spawn, no shell, no special cases.
|
|
13
|
+
*
|
|
14
|
+
* Why an adapter instead of a global monkey-patch?
|
|
15
|
+
*
|
|
16
|
+
* - Explicit dependency injection. Callers (and tests) know exactly
|
|
17
|
+
* which spawn implementation they get.
|
|
18
|
+
* - Isolated — third-party code that needs this behaviour gets it via
|
|
19
|
+
* a thin subclass that consumes the adapter (see
|
|
20
|
+
* `createSafePackageManagerClass` in
|
|
21
|
+
* `packages/server/src/package-manager-wrapper.ts`). No cross-
|
|
22
|
+
* cutting global state.
|
|
23
|
+
* - Testable: fake adapter => assert argv without spawning real
|
|
24
|
+
* subprocesses.
|
|
25
|
+
*
|
|
26
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
27
|
+
*/
|
|
28
|
+
import type {
|
|
29
|
+
ChildProcess,
|
|
30
|
+
SpawnOptions,
|
|
31
|
+
SpawnSyncOptions,
|
|
32
|
+
SpawnSyncReturns,
|
|
33
|
+
} from "node:child_process";
|
|
34
|
+
import {
|
|
35
|
+
spawn as safeSpawn,
|
|
36
|
+
spawnSync as safeSpawnSync,
|
|
37
|
+
buildSafeArgv,
|
|
38
|
+
} from "./exec.js";
|
|
39
|
+
|
|
40
|
+
// ── Interface ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cross-platform subprocess adapter. Implementations guarantee:
|
|
44
|
+
* - `windowsHide: true` on Windows, always.
|
|
45
|
+
* - No `shell: true` ever — `.cmd` shims are invoked via explicit
|
|
46
|
+
* `cmd.exe /d /s /c` argv.
|
|
47
|
+
* - Arg arrays are passed verbatim, no shell-escaping surprises.
|
|
48
|
+
*/
|
|
49
|
+
export interface SubprocessAdapter {
|
|
50
|
+
/** Async spawn. Returns the live ChildProcess. */
|
|
51
|
+
spawn(command: string, args?: readonly string[], options?: SpawnOptions): ChildProcess;
|
|
52
|
+
|
|
53
|
+
/** Synchronous spawn. Blocks until completion. */
|
|
54
|
+
spawnSync<T extends string | Buffer = Buffer>(
|
|
55
|
+
command: string,
|
|
56
|
+
args?: readonly string[],
|
|
57
|
+
options?: SpawnSyncOptions,
|
|
58
|
+
): SpawnSyncReturns<T>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Windows implementation ─────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
class WindowsSubprocessAdapter implements SubprocessAdapter {
|
|
64
|
+
spawn(command: string, args: readonly string[] = [], options?: SpawnOptions): ChildProcess {
|
|
65
|
+
const { argv, spawnOptions } = buildSafeArgv(command, args, "win32");
|
|
66
|
+
return safeSpawn(argv[0], argv.slice(1), { ...(options ?? {}), ...spawnOptions });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
spawnSync<T extends string | Buffer = Buffer>(
|
|
70
|
+
command: string,
|
|
71
|
+
args: readonly string[] = [],
|
|
72
|
+
options?: SpawnSyncOptions,
|
|
73
|
+
): SpawnSyncReturns<T> {
|
|
74
|
+
const { argv, spawnOptions } = buildSafeArgv(command, args, "win32");
|
|
75
|
+
return safeSpawnSync<T>(argv[0], argv.slice(1), { ...(options ?? {}), ...spawnOptions });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Unix implementation ────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
class UnixSubprocessAdapter implements SubprocessAdapter {
|
|
82
|
+
spawn(command: string, args: readonly string[] = [], options?: SpawnOptions): ChildProcess {
|
|
83
|
+
return safeSpawn(command, args, { ...(options ?? {}), shell: false });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
spawnSync<T extends string | Buffer = Buffer>(
|
|
87
|
+
command: string,
|
|
88
|
+
args: readonly string[] = [],
|
|
89
|
+
options?: SpawnSyncOptions,
|
|
90
|
+
): SpawnSyncReturns<T> {
|
|
91
|
+
return safeSpawnSync<T>(command, args, { ...(options ?? {}), shell: false });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Factory ────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Return the appropriate adapter for the given platform. Default:
|
|
99
|
+
* `process.platform`. Tests pass explicit values without mutating the
|
|
100
|
+
* global.
|
|
101
|
+
*/
|
|
102
|
+
export function createSubprocessAdapter(
|
|
103
|
+
platform: NodeJS.Platform = process.platform,
|
|
104
|
+
): SubprocessAdapter {
|
|
105
|
+
if (platform === "win32") return new WindowsSubprocessAdapter();
|
|
106
|
+
return new UnixSubprocessAdapter();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Process-wide default adapter. Constructed lazily on first access.
|
|
111
|
+
* Callers that want a different strategy (e.g. tests injecting a fake)
|
|
112
|
+
* pass the adapter explicitly to their constructor instead of using
|
|
113
|
+
* this singleton.
|
|
114
|
+
*/
|
|
115
|
+
let defaultAdapter: SubprocessAdapter | null = null;
|
|
116
|
+
export function getDefaultSubprocessAdapter(): SubprocessAdapter {
|
|
117
|
+
if (!defaultAdapter) defaultAdapter = createSubprocessAdapter();
|
|
118
|
+
return defaultAdapter;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Test-only: drop the cached default adapter. */
|
|
122
|
+
export function _resetDefaultSubprocessAdapter(): void {
|
|
123
|
+
defaultAdapter = null;
|
|
124
|
+
}
|
|
@@ -57,6 +57,29 @@ export interface EventForwardMessage {
|
|
|
57
57
|
event: DashboardEvent;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Conventions on `event_forward` payloads relevant to per-message fork:
|
|
62
|
+
*
|
|
63
|
+
* - `message_start` and `message_end` events MAY carry an optional
|
|
64
|
+
* `data.nonce: string` stamped by the bridge. The reducer carries it
|
|
65
|
+
* onto the resulting ChatMessage so a later `entry_persisted` event
|
|
66
|
+
* can back-fill the entry id.
|
|
67
|
+
* - `entry_persisted` events have shape:
|
|
68
|
+
* {
|
|
69
|
+
* eventType: "entry_persisted",
|
|
70
|
+
* timestamp,
|
|
71
|
+
* data: { type: "entry_persisted", entryId: string, nonce: string }
|
|
72
|
+
* }
|
|
73
|
+
* They are emitted by the bridge after pi calls
|
|
74
|
+
* `sessionManager.appendMessage` and the entry id has been generated.
|
|
75
|
+
* See change: fix-per-message-fork.
|
|
76
|
+
*/
|
|
77
|
+
export interface EntryPersistedEventData {
|
|
78
|
+
type: "entry_persisted";
|
|
79
|
+
entryId: string;
|
|
80
|
+
nonce: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
export interface CommandsListMessage {
|
|
61
84
|
type: "commands_list";
|
|
62
85
|
sessionId: string;
|
|
@@ -82,7 +82,7 @@ export interface EnrichedRecommendedExtension extends RecommendedExtension {
|
|
|
82
82
|
export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
|
|
83
83
|
{
|
|
84
84
|
id: "pi-anthropic-messages",
|
|
85
|
-
source: "
|
|
85
|
+
source: "https://github.com/BlackBeltTechnology/pi-anthropic-messages.git",
|
|
86
86
|
displayName: "pi-anthropic-messages",
|
|
87
87
|
fallbackDescription:
|
|
88
88
|
"Protocol bridge that makes pi's custom tools work with any " +
|
|
@@ -114,7 +114,7 @@ export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
|
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
id: "pi-flows",
|
|
117
|
-
source: "
|
|
117
|
+
source: "https://github.com/BlackBeltTechnology/pi-flows.git",
|
|
118
118
|
displayName: "pi-flows",
|
|
119
119
|
fallbackDescription:
|
|
120
120
|
"Flow engine, dashboard, and orchestration extensions for pi. " +
|
|
@@ -167,6 +167,22 @@ export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
|
|
|
167
167
|
},
|
|
168
168
|
];
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Ids of recommended extensions that ship inside the Electron installer
|
|
172
|
+
* as a pre-bundled source tree. See
|
|
173
|
+
* `packages/electron/scripts/bundle-recommended-extensions.sh` and
|
|
174
|
+
* `installBundledExtensions()` in `dependency-installer.ts`. Every id
|
|
175
|
+
* MUST also appear in `RECOMMENDED_EXTENSIONS` and MUST have a git-based
|
|
176
|
+
* `source` (enforced by a test).
|
|
177
|
+
*
|
|
178
|
+
* Kept deliberately short — only first-party, source-only, native-dep-free
|
|
179
|
+
* extensions belong here.
|
|
180
|
+
*/
|
|
181
|
+
export const BUNDLED_EXTENSION_IDS: readonly string[] = [
|
|
182
|
+
"pi-anthropic-messages",
|
|
183
|
+
"pi-flows",
|
|
184
|
+
];
|
|
185
|
+
|
|
170
186
|
/** Retrieve a recommended entry by id, or `undefined`. */
|
|
171
187
|
export function getRecommendedExtension(id: string): RecommendedExtension | undefined {
|
|
172
188
|
return RECOMMENDED_EXTENSIONS.find((e) => e.id === id);
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
|
-
import { realpathSync } from "node:fs";
|
|
11
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
13
14
|
|
|
14
15
|
const JITI_PACKAGES = [
|
|
15
16
|
"@mariozechner/jiti",
|
|
@@ -17,8 +18,38 @@ const JITI_PACKAGES = [
|
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
+
* Pure helper: given a jiti package.json path, return the file:// URL of
|
|
22
|
+
* its register hook. Exported for testing — no I/O.
|
|
23
|
+
*
|
|
24
|
+
* Returns a file:// URL (not a raw path) because Node >= 20 on Windows
|
|
25
|
+
* rejects raw absolute paths with a drive letter for --import (parses
|
|
26
|
+
* "C:" / "B:" as a URL scheme → ERR_UNSUPPORTED_ESM_URL_SCHEME). file://
|
|
27
|
+
* URLs are accepted on every OS.
|
|
28
|
+
* See change: fix-windows-server-parity.
|
|
29
|
+
*/
|
|
30
|
+
export function buildJitiRegisterUrl(pkgJsonPath: string): string {
|
|
31
|
+
// Detect Windows-style input (drive letter + backslash) regardless of
|
|
32
|
+
// host OS, so unit tests can exercise the Windows path contract on macOS/Linux.
|
|
33
|
+
// Production behaviour is unchanged because the host-OS `path`/`pathToFileURL`
|
|
34
|
+
// match the input style automatically.
|
|
35
|
+
const isWindowsStyle = /^[A-Za-z]:[\\/]/.test(pkgJsonPath);
|
|
36
|
+
if (isWindowsStyle) {
|
|
37
|
+
// Manually build file:///C:/path/lib/jiti-register.mjs — pathToFileURL on
|
|
38
|
+
// POSIX hosts URL-encodes backslashes rather than treating them as
|
|
39
|
+
// separators. Do the join with path.win32 and format the URL ourselves.
|
|
40
|
+
const registerPath = path.win32.join(path.win32.dirname(pkgJsonPath), "lib", "jiti-register.mjs");
|
|
41
|
+
return `file:///${registerPath.replace(/\\/g, "/")}`;
|
|
42
|
+
}
|
|
43
|
+
const registerPath = path.join(path.dirname(pkgJsonPath), "lib", "jiti-register.mjs");
|
|
44
|
+
return pathToFileURL(registerPath).href;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns jiti's register hook as a file:// URL suitable for `node --import`.
|
|
21
49
|
* Uses process.argv[1] (pi's entry point) to anchor module resolution.
|
|
50
|
+
*
|
|
51
|
+
* The return value is ALWAYS a file:// URL (never a raw path). See
|
|
52
|
+
* buildJitiRegisterUrl for the URL contract rationale.
|
|
22
53
|
*/
|
|
23
54
|
export function resolveJitiImport(): string {
|
|
24
55
|
const anchor = process.argv[1];
|
|
@@ -30,7 +61,7 @@ export function resolveJitiImport(): string {
|
|
|
30
61
|
for (const jiti of JITI_PACKAGES) {
|
|
31
62
|
try {
|
|
32
63
|
const pkgJson = req.resolve(`${jiti}/package.json`);
|
|
33
|
-
return
|
|
64
|
+
return buildJitiRegisterUrl(pkgJson);
|
|
34
65
|
} catch { /* next */ }
|
|
35
66
|
}
|
|
36
67
|
} catch { /* fall through */ }
|
|
@@ -41,3 +72,31 @@ export function resolveJitiImport(): string {
|
|
|
41
72
|
"Is @mariozechner/pi-coding-agent or @oh-my-pi/pi-coding-agent installed?"
|
|
42
73
|
);
|
|
43
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve jiti's register hook from an arbitrary anchor path (e.g. a
|
|
78
|
+
* pi-coding-agent package.json in a managed install, or a pi binary on
|
|
79
|
+
* the system PATH). Returns a file:// URL or null if jiti cannot be
|
|
80
|
+
* resolved from the anchor.
|
|
81
|
+
*
|
|
82
|
+
* This is the Electron/managed-install variant of `resolveJitiImport`
|
|
83
|
+
* — the difference is the caller supplies the anchor explicitly
|
|
84
|
+
* instead of using `process.argv[1]`. Consolidates what used to be a
|
|
85
|
+
* duplicate `resolveJitiFromAnchor` in
|
|
86
|
+
* `packages/electron/src/lib/server-lifecycle.ts`.
|
|
87
|
+
* See change: consolidate-platform-handlers.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveJitiFromAnchor(anchorPath: string): string | null {
|
|
90
|
+
if (!existsSync(anchorPath)) return null;
|
|
91
|
+
try {
|
|
92
|
+
const req = createRequire(anchorPath);
|
|
93
|
+
for (const jiti of JITI_PACKAGES) {
|
|
94
|
+
try {
|
|
95
|
+
const pkgJson = req.resolve(`${jiti}/package.json`);
|
|
96
|
+
const registerPath = path.join(path.dirname(pkgJson), "lib", "jiti-register.mjs");
|
|
97
|
+
if (existsSync(registerPath)) return pathToFileURL(registerPath).href;
|
|
98
|
+
} catch { /* next */ }
|
|
99
|
+
}
|
|
100
|
+
} catch { /* ignore */ }
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
@@ -80,6 +80,16 @@ export interface BrowseResult {
|
|
|
80
80
|
entries: BrowseEntry[];
|
|
81
81
|
parent: string | null;
|
|
82
82
|
current: string;
|
|
83
|
+
/**
|
|
84
|
+
* The server's `process.platform` — lets the client use OS-correct path
|
|
85
|
+
* handling (separator, case-sensitivity, drive-letter rules) without
|
|
86
|
+
* having to sniff `navigator.userAgent`. Optional for backward
|
|
87
|
+
* compatibility; consumers fall back to inferring from the `current`
|
|
88
|
+
* path shape when absent.
|
|
89
|
+
*
|
|
90
|
+
* See change: platform-path-normalization.
|
|
91
|
+
*/
|
|
92
|
+
platform?: NodeJS.Platform;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
export type BrowseResponse = ApiResponse<BrowseResult>;
|
|
@@ -352,3 +362,19 @@ export interface NetworkInterface {
|
|
|
352
362
|
export type ListRecommendedExtensionsResponse = ApiResponse<{
|
|
353
363
|
recommended: EnrichedRecommendedExtension[];
|
|
354
364
|
}>;
|
|
365
|
+
|
|
366
|
+
// ── Tool registry ────────────────────
|
|
367
|
+
|
|
368
|
+
import type { Resolution } from "./tool-registry/types.js";
|
|
369
|
+
export type { Resolution, Source, TriedEntry } from "./tool-registry/types.js";
|
|
370
|
+
|
|
371
|
+
export type ListToolsResponse = ApiResponse<{ tools: Resolution[] }>;
|
|
372
|
+
export type GetToolResponse = ApiResponse<Resolution>;
|
|
373
|
+
|
|
374
|
+
export interface RescanToolsRequest {
|
|
375
|
+
name?: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export interface SetToolOverrideRequest {
|
|
379
|
+
path: string;
|
|
380
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny FIFO semaphore for throttling concurrent async operations.
|
|
3
|
+
*
|
|
4
|
+
* Used by the server's openspec polling scheduler to cap how many
|
|
5
|
+
* `openspec` CLI spawns may be running at once. Rolled in-repo instead
|
|
6
|
+
* of pulling `p-limit` because we need `setMax()` for live reconfig
|
|
7
|
+
* (when the user edits `openspec.maxConcurrentSpawns` in settings).
|
|
8
|
+
*
|
|
9
|
+
* Contract:
|
|
10
|
+
* - `run(fn)` runs `fn` through the gate. At most `max` tasks are
|
|
11
|
+
* in-flight; excess tasks queue FIFO.
|
|
12
|
+
* - `setMax(n)` resizes. Growing drains the queue up to the new cap
|
|
13
|
+
* on the next microtask. Shrinking does not interrupt in-flight
|
|
14
|
+
* tasks; it only affects newly queued ones.
|
|
15
|
+
* - `size()` = active + queued.
|
|
16
|
+
* - If the task throws/rejects, the slot is released and queued
|
|
17
|
+
* tasks proceed.
|
|
18
|
+
*/
|
|
19
|
+
export interface Semaphore {
|
|
20
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
21
|
+
setMax(n: number): void;
|
|
22
|
+
size(): number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createSemaphore(max: number): Semaphore {
|
|
26
|
+
if (!Number.isFinite(max) || max < 1) {
|
|
27
|
+
throw new Error(`Semaphore max must be a positive integer, got ${max}`);
|
|
28
|
+
}
|
|
29
|
+
let limit = Math.floor(max);
|
|
30
|
+
let active = 0;
|
|
31
|
+
const queue: Array<() => void> = [];
|
|
32
|
+
|
|
33
|
+
function drain() {
|
|
34
|
+
while (active < limit && queue.length > 0) {
|
|
35
|
+
const next = queue.shift()!;
|
|
36
|
+
active++;
|
|
37
|
+
next();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function release() {
|
|
42
|
+
active--;
|
|
43
|
+
// Schedule drain on microtask so `run()` callers see a stable state first.
|
|
44
|
+
queueMicrotask(drain);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
run<T>(fn: () => Promise<T>): Promise<T> {
|
|
49
|
+
return new Promise<T>((resolve, reject) => {
|
|
50
|
+
const start = () => {
|
|
51
|
+
let settled = false;
|
|
52
|
+
try {
|
|
53
|
+
Promise.resolve()
|
|
54
|
+
.then(fn)
|
|
55
|
+
.then(
|
|
56
|
+
(value) => { if (!settled) { settled = true; release(); resolve(value); } },
|
|
57
|
+
(err) => { if (!settled) { settled = true; release(); reject(err); } },
|
|
58
|
+
);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (!settled) { settled = true; release(); reject(err); }
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
if (active < limit) {
|
|
64
|
+
active++;
|
|
65
|
+
start();
|
|
66
|
+
} else {
|
|
67
|
+
queue.push(start);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
setMax(n: number): void {
|
|
72
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
73
|
+
throw new Error(`Semaphore max must be a positive integer, got ${n}`);
|
|
74
|
+
}
|
|
75
|
+
limit = Math.floor(n);
|
|
76
|
+
// Drain synchronously so callers that do `setMax(n); await tick` see queued tasks started.
|
|
77
|
+
drain();
|
|
78
|
+
},
|
|
79
|
+
size(): number {
|
|
80
|
+
return active + queue.length;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -13,6 +13,15 @@ import type { EventForwardMessage } from "./protocol.js";
|
|
|
13
13
|
* - message_update + message_end for assistant messages
|
|
14
14
|
* - tool_execution_start / tool_execution_end for tool calls
|
|
15
15
|
* - model_select for model changes
|
|
16
|
+
*
|
|
17
|
+
* NOTE on entryId (per change: fix-per-message-fork):
|
|
18
|
+
* Replay reads from the persisted JSONL, so each entry already has a
|
|
19
|
+
* stable `id`. We attach it directly as `entryId` on both `message_start`
|
|
20
|
+
* (user) and `message_end` (assistant) events. Replay therefore does NOT
|
|
21
|
+
* need to emit an `entry_persisted` follow-up — the back-fill protocol
|
|
22
|
+
* exists to bridge a timing gap that only happens for LIVE pi events on
|
|
23
|
+
* pi 0.69+, where the bridge sees `message_start` before pi has assigned
|
|
24
|
+
* the entry id. Replay has no such gap.
|
|
16
25
|
*/
|
|
17
26
|
export function replayEntriesAsEvents(
|
|
18
27
|
sessionId: string,
|