@unlinearity/cli-wechat-bridge 1.0.7 → 1.1.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.
@@ -4,6 +4,7 @@ import crypto from "node:crypto";
4
4
  import { spawn as spawnChild } from "node:child_process";
5
5
  import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
6
6
  import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
7
+ import { killProcessTreeSync } from "./bridge-process-reaper.js";
7
8
  import * as shared from "./bridge-adapters.shared.js";
8
9
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
9
10
  import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
@@ -35,7 +36,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
35
36
  pendingTurnThreadId = null;
36
37
  interruptPendingTurnStart = false;
37
38
  pendingThreadFollowId = null;
38
- pendingApprovalRequest = null;
39
+ pendingApprovalRequests = [];
39
40
  pendingUserInputRequest = null;
40
41
  queuedTurnNotifications = [];
41
42
  queuedTurnServerRequests = [];
@@ -137,7 +138,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
137
138
  throw new Error("codex is still working. Wait for the current reply or use /stop.");
138
139
  }
139
140
  if (this.pendingApproval) {
140
- throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
141
+ throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
141
142
  }
142
143
  if (this.startupBlocker) {
143
144
  throw new Error("Codex is waiting for local terminal input before the session can continue.");
@@ -185,18 +186,35 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
185
186
  return true;
186
187
  }
187
188
  async resolveApproval(action) {
188
- if (!this.pendingApproval) {
189
+ if (!this.pendingApproval && this.pendingApprovalRequests.length === 0) {
189
190
  return false;
190
191
  }
191
- if (this.pendingApprovalRequest && this.rpcSocket) {
192
- const request = this.pendingApprovalRequest;
193
- await this.respondToApprovalRequest(request, action);
192
+ if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
193
+ for (const request of this.pendingApprovalRequests) {
194
+ await this.respondToApprovalRequest(request, action);
195
+ }
194
196
  this.clearPendingApprovalState();
195
197
  this.setStatus("busy");
196
198
  return true;
197
199
  }
198
200
  return await super.resolveApproval(action);
199
201
  }
202
+ async resolveAllApprovals(action) {
203
+ if (this.pendingApprovalRequests.length === 0 && !this.pendingApproval) {
204
+ return 0;
205
+ }
206
+ if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
207
+ const count = this.pendingApprovalRequests.length;
208
+ for (const request of this.pendingApprovalRequests) {
209
+ await this.respondToApprovalRequest(request, action);
210
+ }
211
+ this.clearPendingApprovalState();
212
+ this.setStatus("busy");
213
+ return count;
214
+ }
215
+ const ok = await super.resolveApproval(action);
216
+ return ok ? 1 : 0;
217
+ }
200
218
  async submitUserInput(answers) {
201
219
  if (!this.pendingUserInputRequest) {
202
220
  return false;
@@ -450,7 +468,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
450
468
  };
451
469
  child.once("exit", () => finish());
452
470
  try {
453
- child.kill();
471
+ if (child.pid) {
472
+ killProcessTreeSync(child.pid);
473
+ }
474
+ else {
475
+ child.kill();
476
+ }
454
477
  }
455
478
  catch {
456
479
  finish();
@@ -495,9 +518,27 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
495
518
  this.sessionPartialLine = "";
496
519
  this.seedSessionReplayCutoff(startedAtMs);
497
520
  }
498
- let content;
521
+ let chunk;
499
522
  try {
500
- content = fs.readFileSync(this.sessionFilePath, "utf8");
523
+ const stat = fs.statSync(this.sessionFilePath);
524
+ if (stat.size < this.sessionReadOffset) {
525
+ this.sessionReadOffset = 0;
526
+ this.sessionPartialLine = "";
527
+ }
528
+ if (stat.size === this.sessionReadOffset) {
529
+ return;
530
+ }
531
+ const fd = fs.openSync(this.sessionFilePath, "r");
532
+ try {
533
+ const bytesToRead = stat.size - this.sessionReadOffset;
534
+ const buf = Buffer.alloc(bytesToRead);
535
+ fs.readSync(fd, buf, 0, bytesToRead, this.sessionReadOffset);
536
+ chunk = buf.toString("utf8");
537
+ this.sessionReadOffset = stat.size;
538
+ }
539
+ finally {
540
+ fs.closeSync(fd);
541
+ }
501
542
  }
502
543
  catch {
503
544
  this.sessionFilePath = null;
@@ -505,15 +546,6 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
505
546
  this.sessionPartialLine = "";
506
547
  return;
507
548
  }
508
- if (content.length < this.sessionReadOffset) {
509
- this.sessionReadOffset = 0;
510
- this.sessionPartialLine = "";
511
- }
512
- if (content.length === this.sessionReadOffset) {
513
- return;
514
- }
515
- const chunk = content.slice(this.sessionReadOffset);
516
- this.sessionReadOffset = content.length;
517
549
  const lines = `${this.sessionPartialLine}${chunk}`.split(/\r?\n/);
518
550
  this.sessionPartialLine = lines.pop() ?? "";
519
551
  for (const line of lines) {
@@ -774,7 +806,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
774
806
  this.recoverStaleBusyStateIfNeeded();
775
807
  this.recoverStaleActiveTurnStateIfNeeded();
776
808
  if (this.pendingApproval) {
777
- throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
809
+ throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
778
810
  }
779
811
  if (this.pendingUserInputRequest || this.state.pendingUserInput) {
780
812
  throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
@@ -1238,7 +1270,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1238
1270
  throw new Error("A thread id is required to resume a Codex thread.");
1239
1271
  }
1240
1272
  if (this.pendingApproval) {
1241
- throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
1273
+ throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
1242
1274
  }
1243
1275
  if (!options.startup &&
1244
1276
  (this.pendingTurnStart ||
@@ -1339,7 +1371,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1339
1371
  status: this.state.status,
1340
1372
  pendingTurnStart: this.pendingTurnStart,
1341
1373
  hasActiveTurn: Boolean(this.activeTurn),
1342
- hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequest),
1374
+ hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequests.length),
1343
1375
  hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
1344
1376
  activeTurnId: this.state.activeTurnId,
1345
1377
  })) {
@@ -1357,7 +1389,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1357
1389
  if (!this.activeTurn ||
1358
1390
  this.pendingTurnStart ||
1359
1391
  this.pendingApproval ||
1360
- this.pendingApprovalRequest ||
1392
+ this.pendingApprovalRequests.length ||
1361
1393
  this.state.status === "busy" ||
1362
1394
  this.state.status === "awaiting_approval" ||
1363
1395
  this.state.activeTurnId) {
@@ -1577,7 +1609,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1577
1609
  return true;
1578
1610
  }
1579
1611
  clearPendingApprovalState() {
1580
- this.pendingApprovalRequest = null;
1612
+ this.pendingApprovalRequests = [];
1581
1613
  this.pendingApproval = null;
1582
1614
  this.state.pendingApproval = null;
1583
1615
  this.state.pendingApprovalOrigin = undefined;
@@ -1627,10 +1659,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1627
1659
  const requestId = ++this.rpcRequestCounter;
1628
1660
  const requestKey = this.rpcRequestKey(requestId);
1629
1661
  const responsePromise = new Promise((resolve, reject) => {
1662
+ const timer = setTimeout(() => {
1663
+ this.pendingRpcRequests.delete(requestKey);
1664
+ reject(new Error(`Codex RPC request timed out after 30s (method: ${method})`));
1665
+ }, 30_000);
1630
1666
  this.pendingRpcRequests.set(requestKey, {
1631
1667
  method,
1632
- resolve,
1633
- reject,
1668
+ resolve: (value) => { clearTimeout(timer); resolve(value); },
1669
+ reject: (err) => { clearTimeout(timer); reject(err); },
1634
1670
  });
1635
1671
  });
1636
1672
  try {
@@ -1837,10 +1873,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1837
1873
  case "serverRequest/resolved": {
1838
1874
  const requestId = getCodexRpcRequestId(params.requestId);
1839
1875
  if (requestId !== null &&
1840
- this.pendingApprovalRequest &&
1841
- requestId === this.pendingApprovalRequest.requestId &&
1842
- trackedTurn.turnId === this.pendingApprovalRequest.turnId) {
1843
- this.clearPendingApprovalState();
1876
+ this.pendingApprovalRequests.some((r) => r.requestId === requestId && r.turnId === trackedTurn.turnId)) {
1877
+ this.pendingApprovalRequests = this.pendingApprovalRequests.filter((r) => r.requestId !== requestId);
1878
+ if (this.pendingApprovalRequests.length === 0) {
1879
+ this.clearPendingApprovalState();
1880
+ }
1844
1881
  if (this.state.status === "awaiting_approval") {
1845
1882
  this.setStatus("busy", "Codex approval resolved.");
1846
1883
  }
@@ -1962,14 +1999,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1962
1999
  });
1963
2000
  return;
1964
2001
  }
1965
- this.pendingApprovalRequest = {
2002
+ this.pendingApprovalRequests.push({
1966
2003
  requestId,
1967
2004
  method,
1968
2005
  threadId: trackedTurn.threadId,
1969
2006
  turnId: trackedTurn.turnId,
1970
2007
  origin: trackedTurn.origin,
1971
2008
  params,
1972
- };
2009
+ });
1973
2010
  this.pendingApproval = request;
1974
2011
  this.state.pendingApproval = request;
1975
2012
  this.state.pendingApprovalOrigin = trackedTurn.origin;
@@ -2133,8 +2170,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2133
2170
  : completedTrackedTurn.origin === "local"
2134
2171
  ? "Local terminal turn completed."
2135
2172
  : this.currentPreview;
2136
- if (this.pendingApprovalRequest &&
2137
- this.pendingApprovalRequest.turnId === trackedTurn.turnId) {
2173
+ if (this.pendingApprovalRequests.length > 0 &&
2174
+ this.pendingApprovalRequests.some((r) => r.turnId === trackedTurn.turnId)) {
2138
2175
  this.clearPendingApprovalState();
2139
2176
  }
2140
2177
  if (this.pendingUserInputRequest &&
@@ -2228,7 +2265,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2228
2265
  this.activeTurn.origin !== "wechat" ||
2229
2266
  this.pendingTurnStart ||
2230
2267
  this.pendingApproval ||
2231
- this.pendingApprovalRequest ||
2268
+ this.pendingApprovalRequests.length ||
2232
2269
  this.pendingUserInputRequest ||
2233
2270
  this.state.pendingUserInput ||
2234
2271
  !this.collectTurnOutput(turnId)) {
@@ -2246,7 +2283,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2246
2283
  const activeTurn = this.activeTurn;
2247
2284
  const finalText = this.collectTurnOutput(turnId);
2248
2285
  const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
2249
- const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequest);
2286
+ const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequests.length);
2250
2287
  const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
2251
2288
  const nowMs = Date.now();
2252
2289
  if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
@@ -2326,7 +2363,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2326
2363
  };
2327
2364
  child.once("exit", () => finish());
2328
2365
  try {
2329
- child.kill();
2366
+ if (child.pid) {
2367
+ killProcessTreeSync(child.pid);
2368
+ }
2369
+ else {
2370
+ child.kill();
2371
+ }
2330
2372
  }
2331
2373
  catch {
2332
2374
  finish();
@@ -2357,7 +2399,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2357
2399
  }
2358
2400
  if (this.nativeProcess) {
2359
2401
  try {
2360
- this.nativeProcess.kill();
2402
+ if (this.nativeProcess.pid) {
2403
+ killProcessTreeSync(this.nativeProcess.pid);
2404
+ }
2405
+ else {
2406
+ this.nativeProcess.kill();
2407
+ }
2361
2408
  }
2362
2409
  catch {
2363
2410
  // Best effort cleanup after panel client failure.
@@ -1,10 +1,11 @@
1
1
  import net from "node:net";
2
2
  import { spawn as spawnPty } from "node-pty";
3
+ import { t } from "../i18n/index.js";
3
4
  import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, sendLocalCompanionMessage, updateLocalCompanionOccupancy, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
4
5
  import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
5
6
  import * as shared from "./bridge-adapters.shared.js";
6
7
  import { LOCAL_CLIENT_PROTOCOL_VERSION } from "../runtime/runtime-types.js";
7
- const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, } = shared;
8
+ const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, buildSpawnDiagnostic, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, spawnFallbackProcess, } = shared;
8
9
  export class LocalCompanionProxyAdapter {
9
10
  options;
10
11
  state;
@@ -136,6 +137,10 @@ export class LocalCompanionProxyAdapter {
136
137
  });
137
138
  return Boolean(result);
138
139
  }
140
+ async resolveAllApprovals(action) {
141
+ const ok = await this.resolveApproval(action);
142
+ return ok ? 1 : 0;
143
+ }
139
144
  async submitUserInput(_answers) {
140
145
  return false;
141
146
  }
@@ -294,10 +299,12 @@ export class LocalCompanionProxyAdapter {
294
299
  }
295
300
  }
296
301
  detachPanelSocket() {
302
+ this.rejectPendingRequests("Companion socket disconnected unexpectedly.");
297
303
  this.detachMessageListener?.();
298
304
  this.detachMessageListener = null;
299
305
  if (this.socket) {
300
306
  this.socket.removeAllListeners();
307
+ this.socket.on("error", () => { });
301
308
  this.socket.destroy();
302
309
  this.socket = null;
303
310
  }
@@ -393,7 +400,14 @@ export class LocalCompanionProxyAdapter {
393
400
  }
394
401
  const id = `${++this.requestCounter}`;
395
402
  const response = new Promise((resolve, reject) => {
396
- this.pendingRequests.set(id, { resolve, reject });
403
+ const timer = setTimeout(() => {
404
+ this.pendingRequests.delete(id);
405
+ reject(new Error(`${this.options.kind} companion request timed out after 30s (command: ${payload.command})`));
406
+ }, 30_000);
407
+ this.pendingRequests.set(id, {
408
+ resolve: (value) => { clearTimeout(timer); resolve(value); },
409
+ reject: (err) => { clearTimeout(timer); reject(err); },
410
+ });
397
411
  });
398
412
  sendLocalCompanionMessage(socket, {
399
413
  type: "request",
@@ -501,10 +515,27 @@ export class AbstractPtyAdapter {
501
515
  try {
502
516
  spawnTarget = resolveSpawnTarget(this.options.command, this.options.kind);
503
517
  const env = this.buildEnv();
504
- const ptyProcess = spawnPty(spawnTarget.file, [...spawnTarget.args, ...this.buildSpawnArgs()], buildPtySpawnOptions({
505
- cwd: this.options.cwd,
506
- env,
507
- }));
518
+ const spawnArgs = [...spawnTarget.args, ...this.buildSpawnArgs()];
519
+ let ptyProcess;
520
+ let usedFallback = false;
521
+ try {
522
+ ptyProcess = spawnPty(spawnTarget.file, spawnArgs, buildPtySpawnOptions({
523
+ cwd: this.options.cwd,
524
+ env,
525
+ }));
526
+ }
527
+ catch (ptyErr) {
528
+ usedFallback = true;
529
+ try {
530
+ ptyProcess = spawnFallbackProcess(spawnTarget.file, spawnArgs, {
531
+ cwd: this.options.cwd,
532
+ env,
533
+ });
534
+ }
535
+ catch (fallbackErr) {
536
+ throw new Error(buildSpawnDiagnostic(ptyErr, spawnTarget), { cause: fallbackErr });
537
+ }
538
+ }
508
539
  this.pty = ptyProcess;
509
540
  this.shuttingDown = false;
510
541
  this.hasAcceptedInput = false;
@@ -515,6 +546,13 @@ export class AbstractPtyAdapter {
515
546
  ptyProcess.onData((data) => this.handleData(data));
516
547
  ptyProcess.onExit(({ exitCode }) => this.handleExit(exitCode));
517
548
  this.afterStart();
549
+ if (usedFallback) {
550
+ this.emit({
551
+ type: "stdout",
552
+ text: t("pty.fallback.warning"),
553
+ timestamp: nowIso(),
554
+ });
555
+ }
518
556
  this.setStatus("idle", `${this.options.kind} adapter is ready.`);
519
557
  }
520
558
  catch (err) {
@@ -576,6 +614,10 @@ export class AbstractPtyAdapter {
576
614
  this.state.pendingApproval = null;
577
615
  return true;
578
616
  }
617
+ async resolveAllApprovals(action) {
618
+ const ok = await this.resolveApproval(action);
619
+ return ok ? 1 : 0;
620
+ }
579
621
  async submitUserInput(_answers) {
580
622
  return false;
581
623
  }
@@ -588,15 +630,21 @@ export class AbstractPtyAdapter {
588
630
  return;
589
631
  }
590
632
  this.shuttingDown = true;
633
+ const pty = this.pty;
634
+ this.pty = null;
635
+ this.state.status = "stopped";
636
+ this.state.pid = undefined;
591
637
  try {
592
- this.pty.kill();
638
+ const exitPromise = new Promise((resolve) => {
639
+ const timeout = setTimeout(resolve, 3_000);
640
+ pty.onExit(() => { clearTimeout(timeout); resolve(); });
641
+ });
642
+ pty.kill();
643
+ await exitPromise;
593
644
  }
594
645
  catch {
595
646
  // Best effort shutdown.
596
647
  }
597
- this.pty = null;
598
- this.state.status = "stopped";
599
- this.state.pid = undefined;
600
648
  }
601
649
  getState() {
602
650
  return JSON.parse(JSON.stringify(this.state));
@@ -110,7 +110,7 @@ export class OpenCodeServerAdapter {
110
110
  throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
111
111
  }
112
112
  if (this.pendingPermission) {
113
- throw new Error("An OpenCode approval request is pending. Reply with /confirm <code> or /deny.");
113
+ throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
114
114
  }
115
115
  const normalized = normalizeOutput(text).trim();
116
116
  if (!normalized) {
@@ -163,7 +163,7 @@ export class OpenCodeServerAdapter {
163
163
  throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
164
164
  }
165
165
  if (this.pendingPermission) {
166
- throw new Error("An OpenCode approval request is pending. Reply with /confirm <code> or /deny.");
166
+ throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
167
167
  }
168
168
  this.outputBatcher.clear();
169
169
  this.clearStreamedPartState();
@@ -254,6 +254,10 @@ export class OpenCodeServerAdapter {
254
254
  this.setStatus("busy");
255
255
  return true;
256
256
  }
257
+ async resolveAllApprovals(action) {
258
+ const ok = await this.resolveApproval(action);
259
+ return ok ? 1 : 0;
260
+ }
257
261
  async submitUserInput(_answers) {
258
262
  return false;
259
263
  }
@@ -285,21 +289,25 @@ export class OpenCodeServerAdapter {
285
289
  if (this.nativeProcess) {
286
290
  const proc = this.nativeProcess;
287
291
  this.nativeProcess = null;
288
- try {
289
- killProcessTreeSync(proc.pid);
290
- }
291
- catch {
292
- // Best effort.
292
+ if (proc.pid != null) {
293
+ try {
294
+ killProcessTreeSync(proc.pid);
295
+ }
296
+ catch {
297
+ // Best effort.
298
+ }
293
299
  }
294
300
  }
295
301
  if (this.serverProcess) {
296
302
  const proc = this.serverProcess;
297
303
  this.serverProcess = null;
298
- try {
299
- killProcessTreeSync(proc.pid);
300
- }
301
- catch {
302
- // Best effort.
304
+ if (proc.pid != null) {
305
+ try {
306
+ killProcessTreeSync(proc.pid);
307
+ }
308
+ catch {
309
+ // Best effort.
310
+ }
303
311
  }
304
312
  }
305
313
  this.client = null;
@@ -2,7 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import net from "node:net";
4
4
  import { fileURLToPath } from "node:url";
5
- import { spawnSync } from "node:child_process";
5
+ import { spawn, spawnSync } from "node:child_process";
6
+ import { t } from "../i18n/index.js";
6
7
  import { spawn as spawnPty } from "node-pty";
7
8
  import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
8
9
  import { getNotificationThreadId, isRecord, } from "./bridge-adapter-common.js";
@@ -1214,3 +1215,102 @@ export function resolveSpawnTarget(command, kind, options = {}) {
1214
1215
  }
1215
1216
  return { file: resolved, args: [...forwardArgs] };
1216
1217
  }
1218
+ export function spawnFallbackProcess(file, args, options) {
1219
+ const child = spawn(file, args, {
1220
+ cwd: options.cwd,
1221
+ env: { ...options.env, TERM: "dumb" },
1222
+ stdio: ["pipe", "pipe", "pipe"],
1223
+ windowsHide: false,
1224
+ shell: false,
1225
+ });
1226
+ if (!child.pid) {
1227
+ throw new Error(`Failed to spawn fallback process: ${file}`);
1228
+ }
1229
+ const dataListeners = [];
1230
+ const exitListeners = [];
1231
+ child.stdout?.on("data", (chunk) => {
1232
+ const text = chunk.toString("utf8");
1233
+ for (const listener of dataListeners) {
1234
+ listener(text);
1235
+ }
1236
+ });
1237
+ child.stderr?.on("data", (chunk) => {
1238
+ const text = chunk.toString("utf8");
1239
+ for (const listener of dataListeners) {
1240
+ listener(text);
1241
+ }
1242
+ });
1243
+ child.on("exit", (code, signal) => {
1244
+ const exitCode = code ?? (signal ? 128 : 1);
1245
+ for (const listener of exitListeners) {
1246
+ listener({ exitCode });
1247
+ }
1248
+ });
1249
+ return {
1250
+ pid: child.pid,
1251
+ write(data) {
1252
+ child.stdin?.write(data);
1253
+ },
1254
+ kill(signal) {
1255
+ try {
1256
+ if (process.platform === "win32") {
1257
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
1258
+ stdio: "ignore",
1259
+ windowsHide: true,
1260
+ });
1261
+ }
1262
+ else {
1263
+ child.kill(signal);
1264
+ }
1265
+ }
1266
+ catch {
1267
+ // Best effort.
1268
+ }
1269
+ },
1270
+ onData(callback) {
1271
+ dataListeners.push(callback);
1272
+ return {
1273
+ dispose() {
1274
+ const index = dataListeners.indexOf(callback);
1275
+ if (index >= 0)
1276
+ dataListeners.splice(index, 1);
1277
+ },
1278
+ };
1279
+ },
1280
+ onExit(callback) {
1281
+ exitListeners.push(callback);
1282
+ return {
1283
+ dispose() {
1284
+ const index = exitListeners.indexOf(callback);
1285
+ if (index >= 0)
1286
+ exitListeners.splice(index, 1);
1287
+ },
1288
+ };
1289
+ },
1290
+ };
1291
+ }
1292
+ export function buildSpawnDiagnostic(error, spawnTarget, platform = process.platform) {
1293
+ const errorMessage = error instanceof Error ? error.message : String(error);
1294
+ const target = spawnTarget ? spawnTarget.file : "(unknown)";
1295
+ const lines = [
1296
+ t("spawn.diagnostic.title", { target, error: errorMessage }),
1297
+ t("spawn.diagnostic.fixesHeader"),
1298
+ ];
1299
+ if (errorMessage.includes("posix_spawnp") || errorMessage.includes("node-pty")) {
1300
+ lines.push(t("spawn.diagnostic.nodePty"));
1301
+ if (platform === "darwin") {
1302
+ lines.push(t("spawn.diagnostic.xcode"));
1303
+ }
1304
+ }
1305
+ else if (errorMessage.includes("ENOENT") || errorMessage.includes("spawn")) {
1306
+ lines.push(t("spawn.diagnostic.notFound", { target }));
1307
+ }
1308
+ else {
1309
+ lines.push(t("spawn.diagnostic.generic"));
1310
+ }
1311
+ lines.push(t("spawn.diagnostic.nodeVersion"));
1312
+ if (platform === "win32") {
1313
+ lines.push(t("spawn.diagnostic.winAdmin"));
1314
+ }
1315
+ return lines.join("\n");
1316
+ }
@@ -67,7 +67,7 @@ export class ShellAdapter extends AbstractPtyAdapter {
67
67
  throw new Error("shell is still working. Wait for the current reply or use /stop.");
68
68
  }
69
69
  if (this.pendingApproval || this.state.status === "awaiting_approval") {
70
- throw new Error("A shell approval request is pending. Reply with /confirm <code> or /deny.");
70
+ throw new Error("A shell approval request is pending. Reply with /confirm or /deny.");
71
71
  }
72
72
  const rejectionMessage = getInteractiveShellCommandRejectionMessage(text);
73
73
  if (rejectionMessage) {
@@ -328,9 +328,14 @@ export function killProcessTreeSync(pid) {
328
328
  }
329
329
  else {
330
330
  try {
331
- process.kill(pid);
331
+ process.kill(-pid);
332
+ }
333
+ catch {
334
+ try {
335
+ process.kill(pid);
336
+ }
337
+ catch { /* best effort */ }
332
338
  }
333
- catch { /* best effort */ }
334
339
  }
335
340
  }
336
341
  export async function reapPeerBridgeProcesses(params = {}) {