@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
|
@@ -1,310 +1,491 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Process manager for spawning pi sessions
|
|
2
|
+
* Process manager for spawning pi sessions.
|
|
3
|
+
*
|
|
4
|
+
* Dispatch is owned by `platform/spawn-mechanism.ts`'s `selectMechanism`.
|
|
5
|
+
* Per-mechanism spawn is owned by `platform/detached-spawn.ts`. This
|
|
6
|
+
* module's job is: resolve pi + tool availability, build per-mechanism
|
|
7
|
+
* command, delegate.
|
|
8
|
+
*
|
|
9
|
+
* Invariants:
|
|
10
|
+
* - No direct `process.platform === "..."` branches in this file.
|
|
11
|
+
* All platform-aware behaviour lives in `platform/**`.
|
|
12
|
+
* - Every mechanism branch builds pi argv uniformly from
|
|
13
|
+
* `buildHeadlessArgs` or its wt/tmux counterpart; `sessionFile`
|
|
14
|
+
* and `mode` are never dropped by any branch.
|
|
15
|
+
*
|
|
16
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
3
17
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
18
|
+
import { existsSync, mkdirSync, openSync, closeSync } from "node:fs";
|
|
6
19
|
import path from "node:path";
|
|
7
20
|
import os from "node:os";
|
|
21
|
+
import type { ChildProcess } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
8
22
|
import type { SpawnStrategy } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
9
23
|
import { MANAGED_BIN } from "@blackbelt-technology/pi-dashboard-shared/managed-paths.js";
|
|
10
|
-
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/
|
|
24
|
+
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js";
|
|
25
|
+
import { execSync, spawnSync, buildSafeArgv } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
26
|
+
import {
|
|
27
|
+
spawnDetached,
|
|
28
|
+
waitForNoCrash,
|
|
29
|
+
} from "@blackbelt-technology/pi-dashboard-shared/platform/detached-spawn.js";
|
|
30
|
+
import {
|
|
31
|
+
selectMechanism,
|
|
32
|
+
buildWtArgs,
|
|
33
|
+
sessionFlagsToArgv,
|
|
34
|
+
type SpawnMechanism,
|
|
35
|
+
type UserSpawnStrategy,
|
|
36
|
+
} from "@blackbelt-technology/pi-dashboard-shared/platform/spawn-mechanism.js";
|
|
37
|
+
|
|
38
|
+
// ── Resolver seam (injectable for tests) ────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
let resolver: ToolResolver = new ToolResolver({ processExecPath: process.execPath });
|
|
41
|
+
|
|
42
|
+
/** Inject a resolver — used by tests. Production code never calls this. */
|
|
43
|
+
export function setResolver(r: ToolResolver): void {
|
|
44
|
+
resolver = r;
|
|
45
|
+
}
|
|
11
46
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
47
|
+
/** Reset to default — used by tests to clean up. */
|
|
48
|
+
export function resetResolver(): void {
|
|
49
|
+
resolver = new ToolResolver({ processExecPath: process.execPath });
|
|
50
|
+
}
|
|
14
51
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
52
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export interface SessionOptions {
|
|
55
|
+
sessionFile?: string;
|
|
56
|
+
mode?: "continue" | "fork";
|
|
57
|
+
strategy?: SpawnStrategy;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SpawnResult {
|
|
61
|
+
success: boolean;
|
|
62
|
+
message: string;
|
|
63
|
+
pid?: number;
|
|
64
|
+
process?: ChildProcess;
|
|
65
|
+
/** True when spawned from the dashboard (for writing session meta) */
|
|
66
|
+
dashboardSpawned?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Build env with managed install bin + current node binary dir prepended to PATH. */
|
|
18
70
|
export function buildSpawnEnv(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
19
71
|
return resolver.buildSpawnEnv(baseEnv);
|
|
20
72
|
}
|
|
21
73
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Escape a string for safe use inside a POSIX shell command.
|
|
76
|
+
* Used by buildTmuxCommand for tmux/wsl-tmux argv construction.
|
|
77
|
+
*/
|
|
78
|
+
export function shellEscape(s: string): string {
|
|
79
|
+
if (/^[a-zA-Z0-9_./:=@-]+$/.test(s)) return s;
|
|
80
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
25
81
|
}
|
|
26
82
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
if (p === "win32") {
|
|
34
|
-
return { strategy: "wsl", platform: p };
|
|
35
|
-
}
|
|
36
|
-
return { strategy: "tmux", platform: p };
|
|
83
|
+
/**
|
|
84
|
+
* Build the argv tail for a headless pi invocation: `--mode rpc` plus
|
|
85
|
+
* `--session <file>` or `--fork <file>` when options provide them.
|
|
86
|
+
*/
|
|
87
|
+
export function buildHeadlessArgs(options?: SessionOptions): string[] {
|
|
88
|
+
return ["--mode", "rpc", ...sessionFlagsToArgv(options ?? {})];
|
|
37
89
|
}
|
|
38
90
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Build the argv tail for an INTERACTIVE pi invocation (wt, tmux, wsl-tmux):
|
|
93
|
+
* no `--mode rpc`; just session/fork flags when provided.
|
|
94
|
+
*/
|
|
95
|
+
export function buildInteractivePiArgs(options?: SessionOptions): string[] {
|
|
96
|
+
return sessionFlagsToArgv(options ?? {});
|
|
43
97
|
}
|
|
44
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Build a tmux shell command string to run pi in a new tmux window/session.
|
|
101
|
+
* Kept as a string (not argv) because tmux is invoked via `execSync(cmd)`.
|
|
102
|
+
*/
|
|
45
103
|
export function buildTmuxCommand(cwd: string, sessionExists: boolean, options?: SessionOptions): string {
|
|
46
104
|
const safeCwd = shellEscape(cwd);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} else if (options?.sessionFile && options?.mode === "fork") {
|
|
52
|
-
piCmd = `cd ${safeCwd} && pi --fork ${shellEscape(options.sessionFile)}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
105
|
+
const flags = sessionFlagsToArgv(options ?? {})
|
|
106
|
+
.map(shellEscape)
|
|
107
|
+
.join(" ");
|
|
108
|
+
const piCmd = flags ? `cd ${safeCwd} && pi ${flags}` : `cd ${safeCwd} && pi`;
|
|
55
109
|
if (sessionExists) {
|
|
56
110
|
return `tmux new-window -t pi-dashboard -c ${safeCwd} "${piCmd}"`;
|
|
57
111
|
}
|
|
58
112
|
return `tmux new-session -d -s pi-dashboard -c ${safeCwd} "${piCmd}"`;
|
|
59
113
|
}
|
|
60
114
|
|
|
115
|
+
// ── Availability probes (isolated, one place) ───────────────────────────────
|
|
116
|
+
|
|
61
117
|
function isTmuxAvailable(): boolean {
|
|
62
118
|
try {
|
|
63
|
-
|
|
64
|
-
return
|
|
119
|
+
// `which` / `where` already baked into ToolResolver.
|
|
120
|
+
return resolver.which("tmux") !== null;
|
|
65
121
|
} catch {
|
|
66
122
|
return false;
|
|
67
123
|
}
|
|
68
124
|
}
|
|
69
125
|
|
|
70
|
-
function
|
|
126
|
+
function isWtAvailable(): boolean {
|
|
71
127
|
try {
|
|
72
|
-
|
|
73
|
-
return true;
|
|
128
|
+
return resolver.which("wt") !== null;
|
|
74
129
|
} catch {
|
|
75
130
|
return false;
|
|
76
131
|
}
|
|
77
132
|
}
|
|
78
133
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
134
|
+
// Cache the WSL-tmux probe for the server lifetime. On machines with a broken
|
|
135
|
+
// WSL install (e.g. Docker Desktop WSL mount failure) this single probe can
|
|
136
|
+
// cost 30+ seconds — we MUST NOT pay it on every + Session click. The result
|
|
137
|
+
// can only change if the user installs/uninstalls WSL or tmux, which requires
|
|
138
|
+
// a server restart anyway.
|
|
139
|
+
let _wslTmuxAvailabilityCache: boolean | null = null;
|
|
140
|
+
let _wslFallbackLogged = false;
|
|
141
|
+
|
|
142
|
+
/** Test-only: reset the cache so tests can exercise both branches. */
|
|
143
|
+
export function _resetWslTmuxCacheForTests(): void {
|
|
144
|
+
_wslTmuxAvailabilityCache = null;
|
|
145
|
+
_wslFallbackLogged = false;
|
|
86
146
|
}
|
|
87
147
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
148
|
+
function isWslTmuxAvailable(): boolean {
|
|
149
|
+
// WSL tmux probe. Route through `buildSafeArgv` so there is NO
|
|
150
|
+
// cmd.exe-as-shell in the path — `spawnSync("wsl", ["which", "tmux"])`
|
|
151
|
+
// with windowsHide:true + shell:false keeps the console invisible.
|
|
152
|
+
// `wsl.exe` itself still spins up WSL briefly, but that's background
|
|
153
|
+
// (no visible window). Only invoked after `wt` is known absent.
|
|
154
|
+
//
|
|
155
|
+
// Cached for the server lifetime (see comment on _wslTmuxAvailabilityCache).
|
|
156
|
+
if (_wslTmuxAvailabilityCache !== null) return _wslTmuxAvailabilityCache;
|
|
157
|
+
try {
|
|
158
|
+
const { argv, spawnOptions } = buildSafeArgv("wsl", ["which", "tmux"]);
|
|
159
|
+
const r = spawnSync(argv[0], argv.slice(1), {
|
|
160
|
+
stdio: "ignore",
|
|
161
|
+
timeout: 1500,
|
|
162
|
+
...spawnOptions,
|
|
163
|
+
});
|
|
164
|
+
_wslTmuxAvailabilityCache = r.status === 0;
|
|
165
|
+
} catch {
|
|
166
|
+
_wslTmuxAvailabilityCache = false;
|
|
95
167
|
}
|
|
168
|
+
if (!_wslTmuxAvailabilityCache && !_wslFallbackLogged) {
|
|
169
|
+
_wslFallbackLogged = true;
|
|
170
|
+
console.error(
|
|
171
|
+
"[spawn] Windows Terminal (wt.exe) not on PATH and WSL tmux unavailable \u2014 " +
|
|
172
|
+
"falling back to headless session spawn. Install Windows Terminal for a " +
|
|
173
|
+
"nicer UX: https://aka.ms/terminal",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return _wslTmuxAvailabilityCache;
|
|
177
|
+
}
|
|
96
178
|
|
|
97
|
-
|
|
179
|
+
function dashboardSessionExists(): boolean {
|
|
180
|
+
try {
|
|
181
|
+
execSync("tmux has-session -t pi-dashboard 2>/dev/null", { stdio: "ignore" });
|
|
182
|
+
return true;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
98
186
|
}
|
|
99
187
|
|
|
100
|
-
/** Resolve
|
|
101
|
-
* Delegates to ToolResolver.resolvePi().
|
|
102
|
-
*/
|
|
188
|
+
/** Resolve pi as argv. Prefers node.exe + cli.js on Windows (avoids .cmd). */
|
|
103
189
|
function resolvePiCommand(): string[] | null {
|
|
104
190
|
return resolver.resolvePi();
|
|
105
191
|
}
|
|
106
192
|
|
|
107
|
-
|
|
108
|
-
|
|
193
|
+
// ── Mechanism dispatch ─────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Select the spawn mechanism for this invocation using lazy tool
|
|
197
|
+
* availability probing. Each probe runs a subprocess, so we short-
|
|
198
|
+
* circuit as soon as a mechanism is decided — crucially, the WSL
|
|
199
|
+
* probe (`wsl which tmux`) spins up the WSL VM on Windows and is
|
|
200
|
+
* the most expensive, so we only run it when wt is ALREADY known
|
|
201
|
+
* absent and the user hasn't asked for headless.
|
|
202
|
+
*
|
|
203
|
+
* Ordering mirrors `selectMechanism`'s decision rules:
|
|
204
|
+
* 1. electronMode or userStrategy=headless → no probes at all
|
|
205
|
+
* 2. Unix → probe tmux only
|
|
206
|
+
* 3. Windows → probe wt first; probe wsl-tmux only if wt is absent
|
|
109
207
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
args: string[],
|
|
114
|
-
env: NodeJS.ProcessEnv,
|
|
115
|
-
): Promise<SpawnResult> {
|
|
116
|
-
const [bin, ...prefixArgs] = piCmd;
|
|
117
|
-
const needsShell = bin.endsWith(".cmd");
|
|
118
|
-
const spawnBin = needsShell ? `"${bin}"` : bin;
|
|
119
|
-
const spawnArgs = needsShell
|
|
120
|
-
? [...prefixArgs, ...args].map(a => `"${a}"`)
|
|
121
|
-
: [...prefixArgs, ...args];
|
|
208
|
+
function chooseMechanism(options?: SessionOptions, electronMode = false): SpawnMechanism {
|
|
209
|
+
const userStrategy: UserSpawnStrategy = options?.strategy === "headless" ? "headless" : "tmux";
|
|
210
|
+
const platform = process.platform;
|
|
122
211
|
|
|
123
|
-
|
|
124
|
-
|
|
212
|
+
// Short-circuit #1: headless requires no probes.
|
|
213
|
+
if (electronMode || userStrategy === "headless") {
|
|
214
|
+
return "headless";
|
|
215
|
+
}
|
|
125
216
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
217
|
+
// Unix: tmux or headless.
|
|
218
|
+
if (platform === "linux" || platform === "darwin") {
|
|
219
|
+
return selectMechanism({
|
|
220
|
+
platform,
|
|
221
|
+
userStrategy,
|
|
222
|
+
electronMode,
|
|
223
|
+
available: { tmux: isTmuxAvailable(), wt: false, wslTmux: false },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
135
226
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
227
|
+
// Windows: wt first (cheap `where wt`). Only probe WSL when wt is
|
|
228
|
+
// absent — `wsl which tmux` starts the WSL VM and is slow + flashy.
|
|
229
|
+
if (platform === "win32") {
|
|
230
|
+
const wt = isWtAvailable();
|
|
231
|
+
if (wt) {
|
|
232
|
+
return selectMechanism({
|
|
233
|
+
platform,
|
|
234
|
+
userStrategy,
|
|
235
|
+
electronMode,
|
|
236
|
+
available: { tmux: false, wt: true, wslTmux: false },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const wslTmux = isWslTmuxAvailable();
|
|
240
|
+
return selectMechanism({
|
|
241
|
+
platform,
|
|
242
|
+
userStrategy,
|
|
243
|
+
electronMode,
|
|
244
|
+
available: { tmux: false, wt: false, wslTmux },
|
|
245
|
+
});
|
|
246
|
+
}
|
|
143
247
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
spawnError = err.message;
|
|
148
|
-
console.error(`[spawn] Windows spawn error: ${err.message}`);
|
|
149
|
-
});
|
|
248
|
+
// Unknown platform → headless.
|
|
249
|
+
return "headless";
|
|
250
|
+
}
|
|
150
251
|
|
|
151
|
-
|
|
152
|
-
(child.stdin as any)?.unref();
|
|
153
|
-
child.stderr?.unref();
|
|
252
|
+
// ── Main entry point ───────────────────────────────────────────────────────
|
|
154
253
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
message: `Failed to spawn pi: ${spawnError || "unknown error (no PID)"}. Command: ${cmdForLog}`,
|
|
162
|
-
};
|
|
254
|
+
export async function spawnPiSession(
|
|
255
|
+
cwd: string,
|
|
256
|
+
options?: SessionOptions & { electronMode?: boolean },
|
|
257
|
+
): Promise<SpawnResult> {
|
|
258
|
+
if (!existsSync(cwd)) {
|
|
259
|
+
return { success: false, message: `Directory does not exist: ${cwd}` };
|
|
163
260
|
}
|
|
164
261
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
262
|
+
const mechanism = chooseMechanism(options, options?.electronMode ?? false);
|
|
263
|
+
|
|
264
|
+
switch (mechanism) {
|
|
265
|
+
case "tmux": return spawnTmux(cwd, options);
|
|
266
|
+
case "wt": return spawnWt(cwd, options);
|
|
267
|
+
case "wsl-tmux": return spawnWslTmux(cwd, options);
|
|
268
|
+
case "headless": return spawnHeadless(cwd, options);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Per-mechanism spawn ────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
function spawnTmux(cwd: string, options?: SessionOptions): SpawnResult {
|
|
275
|
+
const exists = dashboardSessionExists();
|
|
276
|
+
const cmd = buildTmuxCommand(cwd, exists, options);
|
|
277
|
+
try {
|
|
278
|
+
execSync(cmd, { stdio: "ignore" });
|
|
176
279
|
return {
|
|
177
|
-
success:
|
|
178
|
-
|
|
280
|
+
success: true,
|
|
281
|
+
dashboardSpawned: true,
|
|
282
|
+
message: `Pi session spawned in tmux (${exists ? "new window" : "new session"})`,
|
|
179
283
|
};
|
|
284
|
+
} catch (err: any) {
|
|
285
|
+
return { success: false, message: `Failed to spawn session: ${err.message}` };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function spawnWslTmux(cwd: string, options?: SessionOptions): SpawnResult {
|
|
290
|
+
try {
|
|
291
|
+
const cmd = `wsl ${buildTmuxCommand(cwd, false, options)}`;
|
|
292
|
+
execSync(cmd, { stdio: "ignore" });
|
|
293
|
+
return { success: true, dashboardSpawned: true, message: "Pi session spawned via WSL tmux" };
|
|
294
|
+
} catch (err: any) {
|
|
295
|
+
return { success: false, message: `Failed to spawn via WSL tmux: ${err.message}` };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function spawnWt(cwd: string, options?: SessionOptions): Promise<SpawnResult> {
|
|
300
|
+
const wt = resolver.which("wt");
|
|
301
|
+
if (!wt) {
|
|
302
|
+
return { success: false, message: "Windows Terminal (wt.exe) not found" };
|
|
303
|
+
}
|
|
304
|
+
const piCmd = resolvePiCommand();
|
|
305
|
+
if (!piCmd) {
|
|
306
|
+
return { success: false, message: `pi binary not found. Checked: ${MANAGED_BIN} and system PATH.` };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const piArgv = [...piCmd, ...buildInteractivePiArgs(options)];
|
|
310
|
+
const args = buildWtArgs({ cwd, title: path.basename(cwd) || "pi", piArgv });
|
|
311
|
+
|
|
312
|
+
const r = await spawnDetached({
|
|
313
|
+
cmd: wt,
|
|
314
|
+
args,
|
|
315
|
+
cwd,
|
|
316
|
+
env: buildSpawnEnv(),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!r.ok) {
|
|
320
|
+
return { success: false, message: `Failed to launch Windows Terminal: ${r.error}` };
|
|
180
321
|
}
|
|
181
322
|
|
|
182
323
|
return {
|
|
183
324
|
success: true,
|
|
184
325
|
dashboardSpawned: true,
|
|
185
|
-
message:
|
|
186
|
-
pid:
|
|
187
|
-
process:
|
|
326
|
+
message: "Pi session spawned in Windows Terminal",
|
|
327
|
+
pid: r.pid,
|
|
328
|
+
process: r.process,
|
|
188
329
|
};
|
|
189
330
|
}
|
|
190
331
|
|
|
191
332
|
async function spawnHeadless(cwd: string, options?: SessionOptions): Promise<SpawnResult> {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
success: false,
|
|
201
|
-
message: `pi binary not found. Checked: ${MANAGED_BIN}/pi and system PATH.`,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (process.platform === "win32") {
|
|
206
|
-
return await spawnHeadlessWindows(cwd, piCmd_, args, env);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Unix (macOS / Linux / WSL): wrap with "tail -f /dev/null | pi" so stdin
|
|
210
|
-
// is an internal pipe that survives GC and server restarts.
|
|
211
|
-
// detached: true creates a new process group; we kill via -pid later.
|
|
212
|
-
const piBin = piCmd_[0];
|
|
213
|
-
const piCmd = [shellEscape(piBin), ...args.map(shellEscape)].join(" ");
|
|
214
|
-
// Use "tail -f /dev/null" to keep stdin pipe open for pi.
|
|
215
|
-
// Unlike "sleep N", tail -f /dev/null works correctly even when
|
|
216
|
-
// the outer shell's stdin is /dev/null (stdio:"ignore"),
|
|
217
|
-
// which breaks "sleep | pi" on some Linux systems.
|
|
218
|
-
const child = spawn("sh", ["-c", `tail -f /dev/null | ${piCmd}`], {
|
|
219
|
-
cwd,
|
|
220
|
-
detached: true,
|
|
221
|
-
stdio: "ignore",
|
|
222
|
-
env,
|
|
223
|
-
});
|
|
224
|
-
child.unref();
|
|
333
|
+
const args = buildHeadlessArgs(options);
|
|
334
|
+
const env = buildSpawnEnv();
|
|
335
|
+
const piCmd = resolvePiCommand();
|
|
336
|
+
if (!piCmd) {
|
|
337
|
+
return { success: false, message: `pi binary not found. Checked: ${MANAGED_BIN} and system PATH.` };
|
|
338
|
+
}
|
|
339
|
+
const [bin, ...prefixArgs] = piCmd;
|
|
225
340
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
message: `Pi session spawned headless (pid ${child.pid})`,
|
|
230
|
-
pid: child.pid,
|
|
231
|
-
process: child,
|
|
232
|
-
};
|
|
233
|
-
} catch (err: any) {
|
|
234
|
-
return {
|
|
235
|
-
success: false,
|
|
236
|
-
message: `Failed to spawn headless session: ${err.message}`,
|
|
237
|
-
};
|
|
341
|
+
const platform = process.platform;
|
|
342
|
+
if (platform === "win32") {
|
|
343
|
+
return spawnHeadlessDetached(cwd, bin, prefixArgs, args, env);
|
|
238
344
|
}
|
|
239
|
-
}
|
|
240
345
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
346
|
+
// Unix: use the sh -c "tail -f /dev/null | pi" wrapper so pi's stdin is
|
|
347
|
+
// an internal pipe that survives GC. Pass through the detached-spawn
|
|
348
|
+
// primitive so all the libuv defaults (detached, stdio, windowsHide) are
|
|
349
|
+
// uniform. The wrapper is a domain-specific stdin-survival trick — it
|
|
350
|
+
// belongs here (process-manager), not inside the primitive.
|
|
351
|
+
const piLine = [shellEscape(bin), ...[...prefixArgs, ...args].map(shellEscape)].join(" ");
|
|
352
|
+
const r = await spawnDetached({
|
|
353
|
+
cmd: "sh",
|
|
354
|
+
args: ["-c", `tail -f /dev/null | ${piLine}`],
|
|
355
|
+
cwd,
|
|
356
|
+
env,
|
|
357
|
+
});
|
|
358
|
+
if (!r.ok) {
|
|
359
|
+
return { success: false, message: `Failed to spawn headless (Unix): ${r.error}` };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
success: true,
|
|
363
|
+
dashboardSpawned: true,
|
|
364
|
+
message: `Pi session spawned headless (pid ${r.pid})`,
|
|
365
|
+
pid: r.pid,
|
|
366
|
+
process: r.process,
|
|
367
|
+
};
|
|
245
368
|
}
|
|
246
369
|
|
|
247
|
-
|
|
248
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Windows headless spawn using the detached-spawn primitive.
|
|
372
|
+
*
|
|
373
|
+
* Key correctness fixes vs. the previous spawnHeadlessWindows:
|
|
374
|
+
* • detached: true (via primitive) — excludes from libuv's
|
|
375
|
+
* kill-on-close job; sessions survive
|
|
376
|
+
* server restart.
|
|
377
|
+
* • shell: false (via primitive) — sidesteps Node issue
|
|
378
|
+
* #21825 and cmd.exe /d /s /c edge cases.
|
|
379
|
+
* Requires pi to be [node.exe, cli.js],
|
|
380
|
+
* NOT pi.cmd. If only pi.cmd is on PATH,
|
|
381
|
+
* we surface an actionable error.
|
|
382
|
+
* • stdio[0] = "ignore" — no parent-owned stdin pipe.
|
|
383
|
+
* • stdio[2] = logFd — stderr to a persisted log file (not
|
|
384
|
+
* a pipe that dies with the parent).
|
|
385
|
+
* • Crash window 300 ms (was 1500 ms) — via waitForNoCrash.
|
|
386
|
+
*/
|
|
387
|
+
async function spawnHeadlessDetached(
|
|
388
|
+
cwd: string,
|
|
389
|
+
bin: string,
|
|
390
|
+
prefixArgs: string[],
|
|
391
|
+
args: string[],
|
|
392
|
+
env: NodeJS.ProcessEnv,
|
|
393
|
+
): Promise<SpawnResult> {
|
|
394
|
+
// Refuse to go through cmd.exe — the managed install must be present
|
|
395
|
+
// so resolvePiCommand returned [node.exe, cli.js]. If someone has
|
|
396
|
+
// only pi.cmd on PATH, point them at the wizard / managed install.
|
|
397
|
+
if (bin.toLowerCase().endsWith(".cmd") || bin.toLowerCase().endsWith(".bat")) {
|
|
249
398
|
return {
|
|
250
399
|
success: false,
|
|
251
|
-
message:
|
|
400
|
+
message:
|
|
401
|
+
"Windows pi spawn requires node.exe + cli.js (managed install). " +
|
|
402
|
+
"Found only pi.cmd on PATH. Run the dashboard setup wizard or " +
|
|
403
|
+
"install pi via the dashboard's Packages view.",
|
|
252
404
|
};
|
|
253
405
|
}
|
|
254
406
|
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
407
|
+
// Prepare a per-session log file for stderr capture.
|
|
408
|
+
const logDir = path.join(os.homedir(), ".pi", "dashboard", "sessions");
|
|
409
|
+
try { mkdirSync(logDir, { recursive: true }); } catch { /* ignore */ }
|
|
410
|
+
const logPath = path.join(logDir, `pi-spawn-${Date.now()}-${Math.floor(Math.random() * 1e6)}.log`);
|
|
411
|
+
|
|
412
|
+
let logFd: number | undefined;
|
|
413
|
+
try {
|
|
414
|
+
logFd = openSync(logPath, "a");
|
|
415
|
+
} catch {
|
|
416
|
+
// If we can't open the log, proceed without stderr capture; still spawn.
|
|
417
|
+
logFd = undefined;
|
|
258
418
|
}
|
|
259
419
|
|
|
260
|
-
const
|
|
420
|
+
const cmdForLog = `${bin} ${[...prefixArgs, ...args].join(" ")}`;
|
|
421
|
+
console.error(`[spawn] Windows headless (detached): ${cmdForLog} (cwd=${cwd}, log=${logPath})`);
|
|
422
|
+
|
|
423
|
+
// CRITICAL: pi's `--mode rpc` listens for `process.stdin.on("end")`
|
|
424
|
+
// and calls shutdown() on EOF. With `stdio[0] = "ignore"`, stdin
|
|
425
|
+
// closes immediately and pi exits before resume completes. Use a
|
|
426
|
+
// parent-held pipe so pi's stdin stays open as long as the dashboard
|
|
427
|
+
// server is alive.
|
|
428
|
+
//
|
|
429
|
+
// Trade-off: when the dashboard server process dies, Windows closes
|
|
430
|
+
// the pipe handle, pi sees EOF, and shuts down. This is the opposite
|
|
431
|
+
// of the Unix `sh -c "tail -f /dev/null | pi"` wrapper (which keeps
|
|
432
|
+
// stdin open via an internal process-group pipe that survives
|
|
433
|
+
// parent death). On Windows we accept "pi dies with dashboard" as
|
|
434
|
+
// the cost of RPC mode working reliably. A future keeper-process
|
|
435
|
+
// approach could restore the durability invariant.
|
|
436
|
+
//
|
|
437
|
+
// detach: false — restores the behaviour of commit d331850 that was
|
|
438
|
+
// silently overridden by 5ab7956's universal `detached: true` invariant.
|
|
439
|
+
// On Windows, `detached: true` allocates a new console for the child
|
|
440
|
+
// unless all stdio slots are "ignore" (libuv `src/win/process.c` only
|
|
441
|
+
// sets CREATE_NO_WINDOW when no slot has UV_INHERIT_FD). With `stdin:
|
|
442
|
+
// "pipe"` we ALWAYS have UV_INHERIT_FD on stdio[0], so CREATE_NO_WINDOW
|
|
443
|
+
// can never fire, and `windowsHide: true` only applies SW_HIDE after
|
|
444
|
+
// allocation — producing brief console flashes on every session spawn.
|
|
445
|
+
// `detach: false` keeps the child inside the parent's Job Object (no
|
|
446
|
+
// new console needed — no flash). "pi dies with dashboard" invariant is
|
|
447
|
+
// unchanged: stdin-EOF on parent death already ties them together.
|
|
448
|
+
//
|
|
449
|
+
// See change: prep-for-develop-merge.
|
|
450
|
+
const r = await spawnDetached({
|
|
451
|
+
cmd: bin,
|
|
452
|
+
args: [...prefixArgs, ...args],
|
|
453
|
+
cwd,
|
|
454
|
+
env,
|
|
455
|
+
logFd,
|
|
456
|
+
stdinMode: "pipe",
|
|
457
|
+
detach: false,
|
|
458
|
+
});
|
|
261
459
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
message: "tmux is not installed. Install it to spawn sessions from the dashboard.",
|
|
267
|
-
};
|
|
268
|
-
}
|
|
460
|
+
// We don't need the parent's copy of the log fd; the child has its own.
|
|
461
|
+
if (logFd !== undefined) {
|
|
462
|
+
try { closeSync(logFd); } catch { /* ignore */ }
|
|
463
|
+
}
|
|
269
464
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
success: true,
|
|
277
|
-
dashboardSpawned: true,
|
|
278
|
-
message: `Pi session spawned in tmux (${exists ? "new window" : "new session"})`,
|
|
279
|
-
};
|
|
280
|
-
} catch (err: any) {
|
|
281
|
-
return {
|
|
282
|
-
success: false,
|
|
283
|
-
message: `Failed to spawn session: ${err.message}`,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
465
|
+
if (!r.ok || !r.process || !r.pid) {
|
|
466
|
+
return {
|
|
467
|
+
success: false,
|
|
468
|
+
message: `Failed to spawn pi: ${r.error ?? "unknown error"}. Command: ${cmdForLog}`,
|
|
469
|
+
};
|
|
286
470
|
}
|
|
287
471
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
spawn("cmd", ["/c", `cd /d "${cwd}" && pi`], {
|
|
299
|
-
detached: true,
|
|
300
|
-
stdio: "ignore",
|
|
301
|
-
}).unref();
|
|
302
|
-
return { success: true, dashboardSpawned: true, message: "Pi session spawned via cmd" };
|
|
303
|
-
} catch (err: any) {
|
|
304
|
-
return { success: false, message: `Failed to spawn: ${err.message}` };
|
|
305
|
-
}
|
|
306
|
-
}
|
|
472
|
+
// Short crash-detection window so we return fast on the happy path
|
|
473
|
+
// but still catch immediate crashes (missing modules, config errors).
|
|
474
|
+
const gate = await waitForNoCrash({ child: r.process, windowMs: 300 });
|
|
475
|
+
if (!gate.ok) {
|
|
476
|
+
return {
|
|
477
|
+
success: false,
|
|
478
|
+
message:
|
|
479
|
+
`Pi process exited immediately (code ${gate.exitCode}). ` +
|
|
480
|
+
`See ${logPath} for details.\nCommand: ${cmdForLog}`,
|
|
481
|
+
};
|
|
307
482
|
}
|
|
308
483
|
|
|
309
|
-
return {
|
|
484
|
+
return {
|
|
485
|
+
success: true,
|
|
486
|
+
dashboardSpawned: true,
|
|
487
|
+
message: `Pi session spawned headless (pid ${r.pid})`,
|
|
488
|
+
pid: r.pid,
|
|
489
|
+
process: r.process,
|
|
490
|
+
};
|
|
310
491
|
}
|