@caupulican/pi-adaptative 0.91.4 → 0.92.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/CHANGELOG.md +17 -0
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.js +7 -1
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
- package/dist/core/bash-execution-controller.d.ts.map +1 -1
- package/dist/core/bash-execution-controller.js +1 -0
- package/dist/core/bash-execution-controller.js.map +1 -1
- package/dist/core/bash-executor.d.ts +2 -0
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +13 -4
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/tools/bash.d.ts +2 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +27 -12
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/early-powershell-session.d.ts +21 -0
- package/dist/core/tools/early-powershell-session.d.ts.map +1 -0
- package/dist/core/tools/early-powershell-session.js +137 -0
- package/dist/core/tools/early-powershell-session.js.map +1 -0
- package/dist/core/tools/output-accumulator.d.ts +2 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +2 -1
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/persistent-process-coordinator.d.ts.map +1 -1
- package/dist/core/tools/persistent-process-coordinator.js +2 -11
- package/dist/core/tools/persistent-process-coordinator.js.map +1 -1
- package/dist/core/tools/shell-output-decoder.d.ts +7 -0
- package/dist/core/tools/shell-output-decoder.d.ts.map +1 -0
- package/dist/core/tools/shell-output-decoder.js +135 -0
- package/dist/core/tools/shell-output-decoder.js.map +1 -0
- package/dist/core/tools/shell-session.d.ts +23 -6
- package/dist/core/tools/shell-session.d.ts.map +1 -1
- package/dist/core/tools/shell-session.js +275 -88
- package/dist/core/tools/shell-session.js.map +1 -1
- package/dist/utils/child-process-ref.d.ts +4 -0
- package/dist/utils/child-process-ref.d.ts.map +1 -0
- package/dist/utils/child-process-ref.js +15 -0
- package/dist/utils/child-process-ref.js.map +1 -0
- package/dist/utils/powershell-session-protocol.d.ts +16 -0
- package/dist/utils/powershell-session-protocol.d.ts.map +1 -0
- package/dist/utils/powershell-session-protocol.js +63 -0
- package/dist/utils/powershell-session-protocol.js.map +1 -0
- package/dist/utils/shell.d.ts +7 -4
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +38 -23
- package/dist/utils/shell.js.map +1 -1
- package/docs/settings.md +2 -2
- package/docs/windows.md +9 -8
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
* process; the direct child's own exit is what unblocks the sentinel line, exactly like the
|
|
23
23
|
* per-command backend and like bash's stdio inheritance above. The bounded consequence: a native
|
|
24
24
|
* command's stderr no longer merges into the session's stdout pipe (there is no capturing pipeline
|
|
25
|
-
* stage to merge it) — it arrives on the session's own stderr pipe instead
|
|
26
|
-
*
|
|
27
|
-
* redirected stderr text in a `NativeCommandError`
|
|
28
|
-
* improvement (the raw stderr bytes are reported,
|
|
25
|
+
* stage to merge it) — it arrives on the session's own stderr pipe instead. A nonce-bearing barrier
|
|
26
|
+
* on that pipe joins it deterministically with the stdout completion frame before the command
|
|
27
|
+
* resolves. PowerShell 5.1's habit of wrapping redirected stderr text in a `NativeCommandError`
|
|
28
|
+
* record also disappears, which is an accuracy improvement (the raw stderr bytes are reported,
|
|
29
|
+
* not a wrapped/duplicated rendering of them).
|
|
29
30
|
*
|
|
30
31
|
* Kill semantics: timeout/abort/silence kill the WHOLE session process tree (a hung foreground
|
|
31
32
|
* command cannot be killed individually without job control) and the next exec respawns a fresh
|
|
@@ -36,11 +37,17 @@ import { randomBytes } from "node:crypto";
|
|
|
36
37
|
import { basename } from "node:path";
|
|
37
38
|
import { createSilenceWatchdog } from "@caupulican/pi-agent-core/reliability";
|
|
38
39
|
import { spawn } from "child_process";
|
|
39
|
-
import {
|
|
40
|
+
import { createPowerShellHostEnvironment, POWERSHELL_ARGS, POWERSHELL_BOOTSTRAP, POWERSHELL_SESSION_READY_MARKER, POWERSHELL_SESSION_STDERR_READY_MARKER, POWERSHELL_STDERR_BARRIER_LABEL, } from "../../utils/powershell-session-protocol.js";
|
|
41
|
+
import { getPowerShellCandidateConfigs, getShellConfig, getShellEnv, POWERSHELL_STARTUP_PROBE_TIMEOUT_MS, } from "../../utils/shell.js";
|
|
42
|
+
import { claimCliPowerShellWarmStart } from "./early-powershell-session.js";
|
|
40
43
|
import { PersistentProcessCoordinator } from "./persistent-process-coordinator.js";
|
|
44
|
+
export { POWERSHELL_SESSION_READY_MARKER, POWERSHELL_SESSION_STDERR_READY_MARKER, } from "../../utils/powershell-session-protocol.js";
|
|
41
45
|
const SENTINEL_BYTE = 0x1e;
|
|
42
46
|
/** Longest possible sentinel: "\n" + 0x1e + 16-hex nonce + ":" + exit code digits + 0x1e. */
|
|
43
47
|
const SENTINEL_HOLDBACK_BYTES = 64;
|
|
48
|
+
const MAX_STARTUP_DIAGNOSTIC_BYTES = 16 * 1024;
|
|
49
|
+
const POWERSHELL_SESSION_READY_BYTES = Buffer.from(POWERSHELL_SESSION_READY_MARKER, "latin1");
|
|
50
|
+
const POWERSHELL_SESSION_STDERR_READY_BYTES = Buffer.from(POWERSHELL_SESSION_STDERR_READY_MARKER, "latin1");
|
|
44
51
|
function escapeSingleQuotesPosix(value) {
|
|
45
52
|
return value.replace(/'/g, "'\\''");
|
|
46
53
|
}
|
|
@@ -71,41 +78,6 @@ export function buildPowerShellWire(command, nonce, cdTo) {
|
|
|
71
78
|
: command;
|
|
72
79
|
return `${nonce} ${Buffer.from(body, "utf8").toString("base64")}\n`;
|
|
73
80
|
}
|
|
74
|
-
/**
|
|
75
|
-
* PowerShell 5.1-compatible REPL bootstrap. Exit code mirrors `-Command` semantics as closely as
|
|
76
|
-
* a loop can: a native command's $LASTEXITCODE wins, a terminating error forces 1, otherwise 0.
|
|
77
|
-
* Passed as one direct `-Command` argument. `-EncodedCommand` makes PowerShell serialize host,
|
|
78
|
-
* warning, and progress records as CLIXML on stderr whenever pipes are attached; Pi then sees
|
|
79
|
-
* duplicate XML mixed into the same command's plain stdout. Node preserves this script as one
|
|
80
|
-
* argv entry without involving cmd.exe quoting.
|
|
81
|
-
*
|
|
82
|
-
* The command runs as a bare `Invoke-Expression` (no `2>&1 | Out-Default` capture pipeline) — see
|
|
83
|
-
* the module doc header for why: a native command's stdio handles are inherited directly by its
|
|
84
|
-
* child process instead of being captured through a PowerShell-internal pipe, so the sentinel is
|
|
85
|
-
* unblocked by the direct child's own exit rather than by any grandchild holding those handles.
|
|
86
|
-
*/
|
|
87
|
-
const POWERSHELL_BOOTSTRAP = [
|
|
88
|
-
"$ProgressPreference = 'SilentlyContinue'",
|
|
89
|
-
"try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}",
|
|
90
|
-
"function global:__pi_complete_status([int]$Code) { $global:LASTEXITCODE = $Code }",
|
|
91
|
-
"$__pi_in = [Console]::In",
|
|
92
|
-
"while ($true) {",
|
|
93
|
-
"\t$__pi_line = $__pi_in.ReadLine()",
|
|
94
|
-
"\tif ($null -eq $__pi_line) { break }",
|
|
95
|
-
"\t$__pi_sp = $__pi_line.IndexOf(' ')",
|
|
96
|
-
"\tif ($__pi_sp -lt 1) { continue }",
|
|
97
|
-
"\t$__pi_nonce = $__pi_line.Substring(0, $__pi_sp)",
|
|
98
|
-
"\t$__pi_cmd = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($__pi_line.Substring($__pi_sp + 1)))",
|
|
99
|
-
"\t$global:LASTEXITCODE = $null",
|
|
100
|
-
"\t$__pi_thrown = $false",
|
|
101
|
-
"\t$__pi_succeeded = $false",
|
|
102
|
-
"\ttry { Invoke-Expression $__pi_cmd; $__pi_succeeded = $? } catch { $__pi_thrown = $true; $__pi_msg = ($_ | Out-String).TrimEnd(); if ($__pi_msg) { [Console]::Out.WriteLine($__pi_msg) } }",
|
|
103
|
-
"\t$__pi_code = $global:LASTEXITCODE",
|
|
104
|
-
"\tif ($null -eq $__pi_code) { $__pi_code = if ($__pi_succeeded) { 0 } else { 1 } }",
|
|
105
|
-
"\tif ($__pi_thrown -and ($__pi_code -eq 0)) { $__pi_code = 1 }",
|
|
106
|
-
"\t[Console]::Out.Write(('{0}{1}{2}:{3}{1}' -f [char]10, [char]30, $__pi_nonce, $__pi_code))",
|
|
107
|
-
"}",
|
|
108
|
-
].join("\n");
|
|
109
81
|
function shallowEnvEquals(a, b) {
|
|
110
82
|
const aKeys = Object.keys(a);
|
|
111
83
|
const bKeys = Object.keys(b);
|
|
@@ -117,15 +89,38 @@ function shallowEnvEquals(a, b) {
|
|
|
117
89
|
}
|
|
118
90
|
return true;
|
|
119
91
|
}
|
|
92
|
+
function buildPowerShellEnvironmentPrelude(current, desired) {
|
|
93
|
+
const currentByName = new Map(Object.entries(current)
|
|
94
|
+
.filter((entry) => entry[1] !== undefined)
|
|
95
|
+
.map(([name, value]) => [name.toLowerCase(), { name, value }]));
|
|
96
|
+
const desiredByName = new Map(Object.entries(desired)
|
|
97
|
+
.filter((entry) => entry[1] !== undefined)
|
|
98
|
+
.map(([name, value]) => [name.toLowerCase(), { name, value }]));
|
|
99
|
+
const setValues = [...desiredByName.entries()].flatMap(([identity, entry]) => currentByName.get(identity)?.value === entry.value ? [] : [entry]);
|
|
100
|
+
const removeNames = [...currentByName.entries()].flatMap(([identity, entry]) => desiredByName.has(identity) ? [] : [entry.name]);
|
|
101
|
+
if (setValues.length === 0 && removeNames.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
const encoded = Buffer.from(JSON.stringify({ setValues, removeNames }), "utf8").toString("base64");
|
|
104
|
+
return [
|
|
105
|
+
`$__pi_env_delta = ConvertFrom-Json ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${encoded}')))`,
|
|
106
|
+
"foreach ($__pi_env_item in @($__pi_env_delta.setValues)) { [System.Environment]::SetEnvironmentVariable([string]$__pi_env_item.name, [string]$__pi_env_item.value, 'Process') }",
|
|
107
|
+
"foreach ($__pi_env_name in @($__pi_env_delta.removeNames)) { [System.Environment]::SetEnvironmentVariable([string]$__pi_env_name, $null, 'Process') }",
|
|
108
|
+
"Remove-Variable -Name __pi_env_delta,__pi_env_item,__pi_env_name -ErrorAction SilentlyContinue",
|
|
109
|
+
].join("\n");
|
|
110
|
+
}
|
|
120
111
|
export class PersistentShellSession {
|
|
121
|
-
constructor(key, kind) {
|
|
112
|
+
constructor(key, kind, options = {}) {
|
|
122
113
|
this.coordinator = new PersistentProcessCoordinator();
|
|
123
114
|
this.childEnv = null;
|
|
124
115
|
this.lastRequestedCwd = null;
|
|
125
116
|
this.activeExec = null;
|
|
117
|
+
this.rejectStartup = null;
|
|
126
118
|
this.disposed = false;
|
|
127
119
|
this.key = key;
|
|
128
120
|
this.kind = kind;
|
|
121
|
+
this.resolvePowerShellCandidates = options.resolvePowerShellCandidates ?? getPowerShellCandidateConfigs;
|
|
122
|
+
this.spawnProcess = options.spawn ?? spawn;
|
|
123
|
+
this.startupTimeoutMs = options.startupTimeoutMs ?? POWERSHELL_STARTUP_PROBE_TIMEOUT_MS;
|
|
129
124
|
}
|
|
130
125
|
get sessionKind() {
|
|
131
126
|
return this.kind;
|
|
@@ -134,10 +129,37 @@ export class PersistentShellSession {
|
|
|
134
129
|
exec(command, cwd, options) {
|
|
135
130
|
return this.coordinator.runSerialized(() => this.execNow(command, cwd, options));
|
|
136
131
|
}
|
|
132
|
+
/** Start and validate the long-lived shell before the first user command. Idempotent per session. */
|
|
133
|
+
prewarm(cwd, env = getShellEnv()) {
|
|
134
|
+
return this.coordinator.runSerialized(async () => {
|
|
135
|
+
if (this.disposed)
|
|
136
|
+
throw new Error(`Shell session "${this.key}" is disposed`);
|
|
137
|
+
const resolvedEnv = this.kind === "powershell" ? createPowerShellHostEnvironment(env) : env;
|
|
138
|
+
if (this.kind !== "powershell" &&
|
|
139
|
+
this.coordinator.child &&
|
|
140
|
+
this.childEnv &&
|
|
141
|
+
!shallowEnvEquals(this.childEnv, resolvedEnv)) {
|
|
142
|
+
this.killChild();
|
|
143
|
+
}
|
|
144
|
+
if (!this.coordinator.child)
|
|
145
|
+
await this.spawnChild(cwd, resolvedEnv);
|
|
146
|
+
if (this.kind === "powershell" &&
|
|
147
|
+
this.childEnv &&
|
|
148
|
+
(!shallowEnvEquals(this.childEnv, resolvedEnv) || this.lastRequestedCwd !== cwd)) {
|
|
149
|
+
const result = await this.execNow("", cwd, { onData: () => { }, env: resolvedEnv });
|
|
150
|
+
if (result.exitCode !== 0) {
|
|
151
|
+
this.killChild();
|
|
152
|
+
throw new Error(`PowerShell session reconciliation failed with exit code ${result.exitCode ?? "null"}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
this.coordinator.setLoopRef(false);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
137
158
|
dispose() {
|
|
138
159
|
if (this.disposed)
|
|
139
160
|
return;
|
|
140
161
|
this.disposed = true;
|
|
162
|
+
this.rejectStartup?.(new Error(`Shell session "${this.key}" is disposed`));
|
|
141
163
|
this.activeExec?.fail(new Error(`Shell session "${this.key}" is disposed`));
|
|
142
164
|
this.coordinator.dispose();
|
|
143
165
|
this.resetChildState();
|
|
@@ -147,35 +169,50 @@ export class PersistentShellSession {
|
|
|
147
169
|
throw new Error(`Shell session "${this.key}" is disposed`);
|
|
148
170
|
if (signal?.aborted)
|
|
149
171
|
throw new Error("aborted");
|
|
150
|
-
const
|
|
172
|
+
const requestedEnv = env ?? getShellEnv();
|
|
173
|
+
const resolvedEnv = this.kind === "powershell" ? createPowerShellHostEnvironment(requestedEnv) : requestedEnv;
|
|
151
174
|
// The environment is spawn-time shell config: an env that differs from the running
|
|
152
175
|
// session's (e.g. a spawn hook rewriting it per command) requires a fresh shell.
|
|
153
|
-
if (this.
|
|
176
|
+
if (this.kind !== "powershell" &&
|
|
177
|
+
this.coordinator.child &&
|
|
178
|
+
this.childEnv &&
|
|
179
|
+
!shallowEnvEquals(this.childEnv, resolvedEnv)) {
|
|
154
180
|
this.killChild();
|
|
155
181
|
}
|
|
156
182
|
// Re-enter the host-requested cwd only when it CHANGES between calls; an unchanged
|
|
157
183
|
// request preserves the agent's own in-session `cd` (that persistence is the feature).
|
|
158
184
|
let cdTo = null;
|
|
159
|
-
if (!this.coordinator.child)
|
|
160
|
-
this.spawnChild(cwd, resolvedEnv);
|
|
161
|
-
|
|
162
|
-
else if (this.lastRequestedCwd !== cwd) {
|
|
185
|
+
if (!this.coordinator.child)
|
|
186
|
+
await this.spawnChild(cwd, resolvedEnv);
|
|
187
|
+
if (this.lastRequestedCwd !== cwd)
|
|
163
188
|
cdTo = cwd;
|
|
164
|
-
}
|
|
165
189
|
this.lastRequestedCwd = cwd;
|
|
190
|
+
let resolvedCommand = command;
|
|
191
|
+
if (this.kind === "powershell" && this.childEnv && !shallowEnvEquals(this.childEnv, resolvedEnv)) {
|
|
192
|
+
const environmentPrelude = buildPowerShellEnvironmentPrelude(this.childEnv, resolvedEnv);
|
|
193
|
+
if (environmentPrelude)
|
|
194
|
+
resolvedCommand = `${environmentPrelude}\n${command}`;
|
|
195
|
+
this.childEnv = { ...resolvedEnv };
|
|
196
|
+
}
|
|
166
197
|
const child = this.coordinator.child;
|
|
167
198
|
if (!child?.stdin || !child.stdout || !child.stderr) {
|
|
168
199
|
this.killChild();
|
|
169
200
|
throw new Error(`Failed to start ${this.kind} session`);
|
|
170
201
|
}
|
|
171
202
|
const nonce = randomBytes(8).toString("hex");
|
|
172
|
-
const wire = this.kind === "powershell"
|
|
203
|
+
const wire = this.kind === "powershell"
|
|
204
|
+
? buildPowerShellWire(resolvedCommand, nonce, cdTo)
|
|
205
|
+
: buildBashWire(resolvedCommand, nonce, cdTo);
|
|
173
206
|
const sentinelPrefix = Buffer.from(`\n\x1e${nonce}:`, "latin1");
|
|
207
|
+
const stderrBarrier = Buffer.from(`\x1e${nonce}:${POWERSHELL_STDERR_BARRIER_LABEL}\x1e\n`, "latin1");
|
|
174
208
|
this.coordinator.setLoopRef(true);
|
|
175
209
|
try {
|
|
176
210
|
return await new Promise((resolve, reject) => {
|
|
177
211
|
let settled = false;
|
|
178
|
-
let
|
|
212
|
+
let stdoutPending = Buffer.alloc(0);
|
|
213
|
+
let stderrPending = Buffer.alloc(0);
|
|
214
|
+
let commandExitCode;
|
|
215
|
+
let stderrBarrierSeen = this.kind !== "powershell";
|
|
179
216
|
let timeoutTimer;
|
|
180
217
|
const silenceWatchdog = timeoutSeconds === undefined && silenceMs !== undefined && silenceMs > 0
|
|
181
218
|
? createSilenceWatchdog({
|
|
@@ -202,51 +239,75 @@ export class PersistentShellSession {
|
|
|
202
239
|
this.killChild();
|
|
203
240
|
settle(() => reject(new Error("aborted")));
|
|
204
241
|
};
|
|
205
|
-
const
|
|
242
|
+
const emitStdoutPending = (upTo) => {
|
|
206
243
|
if (upTo <= 0)
|
|
207
244
|
return;
|
|
208
|
-
onData(
|
|
209
|
-
|
|
245
|
+
onData(stdoutPending.subarray(0, upTo));
|
|
246
|
+
stdoutPending = stdoutPending.subarray(upTo);
|
|
247
|
+
};
|
|
248
|
+
const emitStderrPending = (upTo) => {
|
|
249
|
+
if (upTo <= 0)
|
|
250
|
+
return;
|
|
251
|
+
onData(stderrPending.subarray(0, upTo));
|
|
252
|
+
stderrPending = stderrPending.subarray(upTo);
|
|
253
|
+
};
|
|
254
|
+
const resolveWhenComplete = () => {
|
|
255
|
+
const exitCode = commandExitCode;
|
|
256
|
+
if (exitCode === undefined || !stderrBarrierSeen)
|
|
257
|
+
return;
|
|
258
|
+
settle(() => resolve({ exitCode }));
|
|
210
259
|
};
|
|
211
260
|
this.activeExec = {
|
|
212
261
|
onStdout: (data) => {
|
|
213
262
|
silenceWatchdog?.touch();
|
|
214
|
-
|
|
215
|
-
|
|
263
|
+
if (commandExitCode !== undefined) {
|
|
264
|
+
onData(data);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
stdoutPending = stdoutPending.length === 0 ? data : Buffer.concat([stdoutPending, data]);
|
|
268
|
+
const prefixIndex = stdoutPending.indexOf(sentinelPrefix);
|
|
216
269
|
if (prefixIndex !== -1) {
|
|
217
|
-
const closeIndex =
|
|
270
|
+
const closeIndex = stdoutPending.indexOf(SENTINEL_BYTE, prefixIndex + sentinelPrefix.length);
|
|
218
271
|
if (closeIndex !== -1) {
|
|
219
|
-
const codeText =
|
|
272
|
+
const codeText = stdoutPending
|
|
220
273
|
.subarray(prefixIndex + sentinelPrefix.length, closeIndex)
|
|
221
274
|
.toString("latin1");
|
|
222
|
-
|
|
275
|
+
emitStdoutPending(prefixIndex);
|
|
276
|
+
stdoutPending = stdoutPending.subarray(closeIndex - prefixIndex + 1);
|
|
277
|
+
emitStdoutPending(stdoutPending.length);
|
|
223
278
|
const parsed = Number.parseInt(codeText, 10);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// pipe (see module doc header) instead of being pre-merged into the same
|
|
227
|
-
// stdout bytes the sentinel rides on. Node fires already-queued stream
|
|
228
|
-
// 'data' events in event-loop order; queueing the resolution behind a
|
|
229
|
-
// microtask guarantees any stderr chunk the kernel delivered alongside (or
|
|
230
|
-
// just before) this stdout chunk has already run its 'data' handler — and
|
|
231
|
-
// thus reached `onData` — before the promise settles, so callers never see a
|
|
232
|
-
// truncated stderr tail immediately after resolution.
|
|
233
|
-
queueMicrotask(() => settle(() => resolve({ exitCode: Number.isNaN(parsed) ? null : parsed })));
|
|
279
|
+
commandExitCode = Number.isNaN(parsed) ? null : parsed;
|
|
280
|
+
resolveWhenComplete();
|
|
234
281
|
return;
|
|
235
282
|
}
|
|
236
283
|
}
|
|
237
284
|
// Stream promptly but retain a tail large enough to hold any split sentinel.
|
|
238
|
-
|
|
285
|
+
emitStdoutPending(stdoutPending.length - SENTINEL_HOLDBACK_BYTES);
|
|
239
286
|
},
|
|
240
287
|
onStderr: (data) => {
|
|
241
|
-
// The command's stderr is merged into stdout at the shell; this pipe only
|
|
242
|
-
// carries the session shell's own diagnostics. Forward for visibility.
|
|
243
288
|
silenceWatchdog?.touch();
|
|
244
|
-
|
|
289
|
+
if (this.kind !== "powershell" || stderrBarrierSeen) {
|
|
290
|
+
onData(data);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
stderrPending = stderrPending.length === 0 ? data : Buffer.concat([stderrPending, data]);
|
|
294
|
+
const barrierIndex = stderrPending.indexOf(stderrBarrier);
|
|
295
|
+
if (barrierIndex !== -1) {
|
|
296
|
+
emitStderrPending(barrierIndex);
|
|
297
|
+
stderrPending = stderrPending.subarray(stderrBarrier.length);
|
|
298
|
+
emitStderrPending(stderrPending.length);
|
|
299
|
+
stderrBarrierSeen = true;
|
|
300
|
+
resolveWhenComplete();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Preserve enough bytes to recognize a barrier split across chunks.
|
|
304
|
+
emitStderrPending(stderrPending.length - stderrBarrier.length + 1);
|
|
245
305
|
},
|
|
246
306
|
onChildClose: (code) => {
|
|
247
307
|
// The command terminated the shell itself (e.g. `exit 3`) or the shell
|
|
248
308
|
// crashed: report its exit code like the per-command backend would.
|
|
249
|
-
|
|
309
|
+
emitStdoutPending(stdoutPending.length);
|
|
310
|
+
emitStderrPending(stderrPending.length);
|
|
250
311
|
settle(() => resolve({ exitCode: code }));
|
|
251
312
|
},
|
|
252
313
|
fail: (error) => {
|
|
@@ -274,20 +335,148 @@ export class PersistentShellSession {
|
|
|
274
335
|
this.coordinator.setLoopRef(false);
|
|
275
336
|
}
|
|
276
337
|
}
|
|
277
|
-
spawnChild(cwd, env) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
338
|
+
async spawnChild(cwd, env) {
|
|
339
|
+
let spawnedCwd = cwd;
|
|
340
|
+
let spawnedEnv = env;
|
|
341
|
+
if (this.kind === "powershell") {
|
|
342
|
+
({ cwd: spawnedCwd, env: spawnedEnv } = await this.spawnPowerShellChild(cwd, env));
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const { shell } = getShellConfig(undefined, this.kind);
|
|
346
|
+
const args = basename(shell).toLowerCase().includes("bash") ? ["--noprofile", "--norc"] : [];
|
|
347
|
+
const child = this.spawnProcess(shell, args, {
|
|
348
|
+
cwd,
|
|
349
|
+
env,
|
|
350
|
+
detached: process.platform !== "win32",
|
|
351
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
352
|
+
windowsHide: true,
|
|
353
|
+
});
|
|
354
|
+
this.attachReadyChild(child);
|
|
355
|
+
}
|
|
356
|
+
this.childEnv = { ...spawnedEnv };
|
|
357
|
+
this.lastRequestedCwd = spawnedCwd;
|
|
358
|
+
}
|
|
359
|
+
async spawnPowerShellChild(cwd, env) {
|
|
360
|
+
const warmStart = await claimCliPowerShellWarmStart();
|
|
361
|
+
if (warmStart) {
|
|
362
|
+
try {
|
|
363
|
+
this.attachReadyChild(warmStart.child);
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
warmStart.releaseStartupListeners();
|
|
367
|
+
}
|
|
368
|
+
return { cwd: warmStart.cwd, env: warmStart.env };
|
|
369
|
+
}
|
|
370
|
+
const failures = [];
|
|
371
|
+
for (const candidate of this.resolvePowerShellCandidates()) {
|
|
372
|
+
try {
|
|
373
|
+
await this.spawnPowerShellCandidate(candidate.shell, cwd, env);
|
|
374
|
+
return { cwd, env };
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
378
|
+
failures.push(`${candidate.shell}: ${message}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const detail = failures.length > 0 ? ` Candidate failures: ${failures.join("; ")}` : "";
|
|
382
|
+
throw new Error(`PowerShell 7 (pwsh) was not found. Install pwsh before using the Windows shell.${detail}`);
|
|
383
|
+
}
|
|
384
|
+
spawnPowerShellCandidate(shell, cwd, env) {
|
|
385
|
+
return new Promise((resolve, reject) => {
|
|
386
|
+
let startupSettled = false;
|
|
387
|
+
let startupStdout = Buffer.alloc(0);
|
|
388
|
+
let startupStderr = Buffer.alloc(0);
|
|
389
|
+
let stdoutReady = false;
|
|
390
|
+
let stderrReady = false;
|
|
391
|
+
let timeoutTimer;
|
|
392
|
+
const settleStartup = (finish) => {
|
|
393
|
+
if (startupSettled)
|
|
394
|
+
return;
|
|
395
|
+
startupSettled = true;
|
|
396
|
+
if (timeoutTimer)
|
|
397
|
+
clearTimeout(timeoutTimer);
|
|
398
|
+
if (this.rejectStartup === rejectStartup)
|
|
399
|
+
this.rejectStartup = null;
|
|
400
|
+
finish();
|
|
401
|
+
};
|
|
402
|
+
const startupDiagnostic = () => {
|
|
403
|
+
const combined = Buffer.concat([startupStdout, startupStderr]).toString("utf8").trim();
|
|
404
|
+
return combined ? `: ${combined}` : "";
|
|
405
|
+
};
|
|
406
|
+
const rejectStartup = (error) => {
|
|
407
|
+
settleStartup(() => reject(error));
|
|
408
|
+
};
|
|
409
|
+
const resolveWhenReady = () => {
|
|
410
|
+
if (!stdoutReady || !stderrReady)
|
|
411
|
+
return;
|
|
412
|
+
settleStartup(resolve);
|
|
413
|
+
};
|
|
414
|
+
let child;
|
|
415
|
+
try {
|
|
416
|
+
child = this.spawnProcess(shell, [...POWERSHELL_ARGS, POWERSHELL_BOOTSTRAP], {
|
|
417
|
+
cwd,
|
|
418
|
+
env,
|
|
419
|
+
detached: process.platform !== "win32",
|
|
420
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
421
|
+
windowsHide: true,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
426
|
+
rejectStartup(new Error(`spawn failed: ${message}`));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.rejectStartup = rejectStartup;
|
|
430
|
+
this.coordinator.attach(child, {
|
|
431
|
+
onStdout: (data) => {
|
|
432
|
+
if (startupSettled) {
|
|
433
|
+
this.activeExec?.onStdout(data);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
startupStdout =
|
|
437
|
+
startupStdout.length === 0
|
|
438
|
+
? data
|
|
439
|
+
: Buffer.concat([startupStdout, data]).subarray(-MAX_STARTUP_DIAGNOSTIC_BYTES);
|
|
440
|
+
stdoutReady ||= startupStdout.indexOf(POWERSHELL_SESSION_READY_BYTES) !== -1;
|
|
441
|
+
resolveWhenReady();
|
|
442
|
+
},
|
|
443
|
+
onStderr: (data) => {
|
|
444
|
+
if (startupSettled) {
|
|
445
|
+
this.activeExec?.onStderr(data);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
startupStderr =
|
|
449
|
+
startupStderr.length === 0
|
|
450
|
+
? data
|
|
451
|
+
: Buffer.concat([startupStderr, data]).subarray(-MAX_STARTUP_DIAGNOSTIC_BYTES);
|
|
452
|
+
stderrReady ||= startupStderr.indexOf(POWERSHELL_SESSION_STDERR_READY_BYTES) !== -1;
|
|
453
|
+
resolveWhenReady();
|
|
454
|
+
},
|
|
455
|
+
onError: (error) => {
|
|
456
|
+
this.resetChildState();
|
|
457
|
+
if (!startupSettled)
|
|
458
|
+
rejectStartup(new Error(`${error.message}${startupDiagnostic()}`));
|
|
459
|
+
else
|
|
460
|
+
this.activeExec?.fail(error);
|
|
461
|
+
},
|
|
462
|
+
onClose: (code) => {
|
|
463
|
+
this.resetChildState();
|
|
464
|
+
if (!startupSettled) {
|
|
465
|
+
rejectStartup(new Error(`exited with code ${code ?? "null"} before readiness${startupDiagnostic()}`));
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
this.activeExec?.onChildClose(code);
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
timeoutTimer = setTimeout(() => {
|
|
473
|
+
this.coordinator.kill();
|
|
474
|
+
this.resetChildState();
|
|
475
|
+
rejectStartup(new Error(`startup timed out after ${this.startupTimeoutMs}ms${startupDiagnostic()}`));
|
|
476
|
+
}, this.startupTimeoutMs);
|
|
290
477
|
});
|
|
478
|
+
}
|
|
479
|
+
attachReadyChild(child) {
|
|
291
480
|
this.coordinator.attach(child, {
|
|
292
481
|
onStdout: (data) => this.activeExec?.onStdout(data),
|
|
293
482
|
onStderr: (data) => this.activeExec?.onStderr(data),
|
|
@@ -300,8 +489,6 @@ export class PersistentShellSession {
|
|
|
300
489
|
this.activeExec?.onChildClose(code);
|
|
301
490
|
},
|
|
302
491
|
});
|
|
303
|
-
this.childEnv = env;
|
|
304
|
-
this.lastRequestedCwd = cwd;
|
|
305
492
|
}
|
|
306
493
|
resetChildState() {
|
|
307
494
|
this.childEnv = null;
|