@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli.js +6 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
  5. package/dist/core/autonomy/lane-tool-surface.js +7 -1
  6. package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
  7. package/dist/core/bash-execution-controller.d.ts.map +1 -1
  8. package/dist/core/bash-execution-controller.js +1 -0
  9. package/dist/core/bash-execution-controller.js.map +1 -1
  10. package/dist/core/bash-executor.d.ts +2 -0
  11. package/dist/core/bash-executor.d.ts.map +1 -1
  12. package/dist/core/bash-executor.js +13 -4
  13. package/dist/core/bash-executor.js.map +1 -1
  14. package/dist/core/tools/bash.d.ts +2 -0
  15. package/dist/core/tools/bash.d.ts.map +1 -1
  16. package/dist/core/tools/bash.js +27 -12
  17. package/dist/core/tools/bash.js.map +1 -1
  18. package/dist/core/tools/early-powershell-session.d.ts +21 -0
  19. package/dist/core/tools/early-powershell-session.d.ts.map +1 -0
  20. package/dist/core/tools/early-powershell-session.js +137 -0
  21. package/dist/core/tools/early-powershell-session.js.map +1 -0
  22. package/dist/core/tools/output-accumulator.d.ts +2 -0
  23. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  24. package/dist/core/tools/output-accumulator.js +2 -1
  25. package/dist/core/tools/output-accumulator.js.map +1 -1
  26. package/dist/core/tools/persistent-process-coordinator.d.ts.map +1 -1
  27. package/dist/core/tools/persistent-process-coordinator.js +2 -11
  28. package/dist/core/tools/persistent-process-coordinator.js.map +1 -1
  29. package/dist/core/tools/shell-output-decoder.d.ts +7 -0
  30. package/dist/core/tools/shell-output-decoder.d.ts.map +1 -0
  31. package/dist/core/tools/shell-output-decoder.js +135 -0
  32. package/dist/core/tools/shell-output-decoder.js.map +1 -0
  33. package/dist/core/tools/shell-session.d.ts +23 -6
  34. package/dist/core/tools/shell-session.d.ts.map +1 -1
  35. package/dist/core/tools/shell-session.js +275 -88
  36. package/dist/core/tools/shell-session.js.map +1 -1
  37. package/dist/utils/child-process-ref.d.ts +4 -0
  38. package/dist/utils/child-process-ref.d.ts.map +1 -0
  39. package/dist/utils/child-process-ref.js +15 -0
  40. package/dist/utils/child-process-ref.js.map +1 -0
  41. package/dist/utils/powershell-session-protocol.d.ts +16 -0
  42. package/dist/utils/powershell-session-protocol.d.ts.map +1 -0
  43. package/dist/utils/powershell-session-protocol.js +63 -0
  44. package/dist/utils/powershell-session-protocol.js.map +1 -0
  45. package/dist/utils/shell.d.ts +7 -4
  46. package/dist/utils/shell.d.ts.map +1 -1
  47. package/dist/utils/shell.js +38 -23
  48. package/dist/utils/shell.js.map +1 -1
  49. package/docs/settings.md +2 -2
  50. package/docs/windows.md +9 -8
  51. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  52. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  53. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  54. package/examples/extensions/sandbox/package-lock.json +2 -2
  55. package/examples/extensions/sandbox/package.json +1 -1
  56. package/examples/extensions/with-deps/package-lock.json +2 -2
  57. package/examples/extensions/with-deps/package.json +1 -1
  58. package/npm-shrinkwrap.json +12 -12
  59. 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, forwarded via
26
- * `onStderr` -> `onData` same as the shell's own diagnostics. PowerShell 5.1's habit of wrapping
27
- * redirected stderr text in a `NativeCommandError` record also disappears, which is an accuracy
28
- * improvement (the raw stderr bytes are reported, not a wrapped/duplicated rendering of them).
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 { getShellConfig, getShellEnv } from "../../utils/shell.js";
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 resolvedEnv = env ?? getShellEnv();
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.coordinator.child && this.childEnv && !shallowEnvEquals(this.childEnv, resolvedEnv)) {
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" ? buildPowerShellWire(command, nonce, cdTo) : buildBashWire(command, nonce, cdTo);
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 pending = Buffer.alloc(0);
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 emitPending = (upTo) => {
242
+ const emitStdoutPending = (upTo) => {
206
243
  if (upTo <= 0)
207
244
  return;
208
- onData(pending.subarray(0, upTo));
209
- pending = pending.subarray(upTo);
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
- pending = pending.length === 0 ? data : Buffer.concat([pending, data]);
215
- const prefixIndex = pending.indexOf(sentinelPrefix);
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 = pending.indexOf(SENTINEL_BYTE, prefixIndex + sentinelPrefix.length);
270
+ const closeIndex = stdoutPending.indexOf(SENTINEL_BYTE, prefixIndex + sentinelPrefix.length);
218
271
  if (closeIndex !== -1) {
219
- const codeText = pending
272
+ const codeText = stdoutPending
220
273
  .subarray(prefixIndex + sentinelPrefix.length, closeIndex)
221
274
  .toString("latin1");
222
- emitPending(prefixIndex);
275
+ emitStdoutPending(prefixIndex);
276
+ stdoutPending = stdoutPending.subarray(closeIndex - prefixIndex + 1);
277
+ emitStdoutPending(stdoutPending.length);
223
278
  const parsed = Number.parseInt(codeText, 10);
224
- // Defer the settle by one microtask turn: with the bare Invoke-Expression
225
- // form a native command's stderr now arrives on the session's OWN stderr
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
- emitPending(pending.length - SENTINEL_HOLDBACK_BYTES);
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
- onData(data);
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
- emitPending(pending.length);
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
- const { shell } = getShellConfig(undefined, this.kind);
279
- const args = this.kind === "powershell"
280
- ? ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", POWERSHELL_BOOTSTRAP]
281
- : basename(shell).toLowerCase().includes("bash")
282
- ? ["--noprofile", "--norc"]
283
- : [];
284
- const child = spawn(shell, args, {
285
- cwd,
286
- env,
287
- detached: process.platform !== "win32",
288
- stdio: ["pipe", "pipe", "pipe"],
289
- windowsHide: true,
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;