@buildautomaton/cli 0.1.32 → 0.1.34

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/dist/cli.js CHANGED
@@ -25064,7 +25064,7 @@ var {
25064
25064
  } = import_index.default;
25065
25065
 
25066
25066
  // src/cli-version.ts
25067
- var CLI_VERSION = "0.1.32".length > 0 ? "0.1.32" : "0.0.0-dev";
25067
+ var CLI_VERSION = "0.1.34".length > 0 ? "0.1.34" : "0.0.0-dev";
25068
25068
 
25069
25069
  // src/cli/defaults.ts
25070
25070
  var DEFAULT_API_URL = process.env.BUILDAUTOMATON_API_URL ?? "https://api.buildautomaton.com";
@@ -26696,6 +26696,30 @@ function runPendingAuth(options) {
26696
26696
  };
26697
26697
  }
26698
26698
 
26699
+ // src/dev-servers/manager/dev-server-constants.ts
26700
+ var BRIDGE_CLOSE_DEV_SERVER_GRACE_MS = 0;
26701
+ var BRIDGE_SHUTDOWN_GRACE_MS = 8e3;
26702
+
26703
+ // src/runtime/cli-process-interrupt.ts
26704
+ var cliImmediateShutdownRequested = false;
26705
+ function requestCliImmediateShutdown() {
26706
+ cliImmediateShutdownRequested = true;
26707
+ }
26708
+ function isCliImmediateShutdownRequested() {
26709
+ return cliImmediateShutdownRequested;
26710
+ }
26711
+ async function delayMsUnlessShutdownRequested(ms) {
26712
+ if (ms <= 0) return true;
26713
+ const end = Date.now() + ms;
26714
+ while (Date.now() < end) {
26715
+ if (isCliImmediateShutdownRequested()) return false;
26716
+ const chunk = Math.min(50, end - Date.now());
26717
+ if (chunk <= 0) break;
26718
+ await new Promise((r) => setTimeout(r, chunk));
26719
+ }
26720
+ return !isCliImmediateShutdownRequested();
26721
+ }
26722
+
26699
26723
  // src/sqlite/cli-database.ts
26700
26724
  import sqliteWasm from "node-sqlite3-wasm";
26701
26725
 
@@ -26999,6 +27023,12 @@ var { Database: SqliteDatabase } = sqliteWasm;
26999
27023
  var CLI_SQLITE_SYNC_RETRY_MAX = 40;
27000
27024
  var CLI_SQLITE_ASYNC_RETRY_MAX = 60;
27001
27025
  var CLI_SQLITE_ASYNC_BASE_DELAY_MS = 20;
27026
+ var CliSqliteInterrupted = class extends Error {
27027
+ name = "CliSqliteInterrupted";
27028
+ constructor() {
27029
+ super("CLI SQLite interrupted (shutdown)");
27030
+ }
27031
+ };
27002
27032
  function applyCliSqliteConcurrencyPragmas(db) {
27003
27033
  try {
27004
27034
  db.exec("PRAGMA journal_mode = WAL");
@@ -27009,7 +27039,7 @@ function applyCliSqliteConcurrencyPragmas(db) {
27009
27039
  } catch {
27010
27040
  }
27011
27041
  try {
27012
- db.run("PRAGMA busy_timeout = 8000");
27042
+ db.run("PRAGMA busy_timeout = 500");
27013
27043
  } catch {
27014
27044
  }
27015
27045
  }
@@ -27030,26 +27060,42 @@ function safeCloseCliSqliteDatabase(db) {
27030
27060
  function closeAllCliSqliteConnections() {
27031
27061
  }
27032
27062
  function isCliSqliteLockError(e) {
27063
+ if (e instanceof CliSqliteInterrupted) return false;
27033
27064
  const msg = e instanceof Error ? e.message : String(e);
27034
27065
  const lower = msg.toLowerCase();
27035
27066
  return lower.includes("database is locked") || lower.includes("sqlite_busy") || lower.includes("sqlite3_busy") || lower.includes("database") && lower.includes("locked");
27036
27067
  }
27037
27068
  function syncSleepMs(ms) {
27038
27069
  if (ms <= 0) return;
27039
- try {
27040
- const sab = new SharedArrayBuffer(4);
27041
- const ia = new Int32Array(sab);
27042
- Atomics.wait(ia, 0, 0, Math.min(ms, 1e4));
27043
- } catch {
27044
- const end = Date.now() + ms;
27045
- while (Date.now() < end) {
27070
+ const deadline = Date.now() + ms;
27071
+ while (Date.now() < deadline) {
27072
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
27073
+ const chunk = Math.min(80, deadline - Date.now());
27074
+ if (chunk <= 0) break;
27075
+ try {
27076
+ const sab = new SharedArrayBuffer(4);
27077
+ const ia = new Int32Array(sab);
27078
+ Atomics.wait(ia, 0, 0, chunk);
27079
+ } catch {
27080
+ const end = Date.now() + chunk;
27081
+ while (Date.now() < end) {
27082
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
27083
+ }
27046
27084
  }
27047
27085
  }
27048
27086
  }
27049
- function asyncDelayMs(ms) {
27050
- return new Promise((resolve20) => setTimeout(resolve20, ms));
27087
+ async function asyncDelayMsInterruptible(ms) {
27088
+ const end = Date.now() + ms;
27089
+ while (Date.now() < end) {
27090
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
27091
+ const chunk = Math.min(50, end - Date.now());
27092
+ if (chunk <= 0) break;
27093
+ await new Promise((r) => setTimeout(r, chunk));
27094
+ await yieldToEventLoop();
27095
+ }
27051
27096
  }
27052
27097
  function openCliSqliteConnection(options) {
27098
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
27053
27099
  const sqlitePath = getCliSqlitePath();
27054
27100
  ensureCliSqliteParentDir(sqlitePath);
27055
27101
  const db = new SqliteDatabase(sqlitePath);
@@ -27066,6 +27112,7 @@ function openCliSqliteConnection(options) {
27066
27112
  }
27067
27113
  function withCliSqliteSync(fn, options) {
27068
27114
  for (let attempt = 1; attempt <= CLI_SQLITE_SYNC_RETRY_MAX; attempt++) {
27115
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
27069
27116
  let db;
27070
27117
  try {
27071
27118
  db = openCliSqliteConnection(options);
@@ -27076,6 +27123,7 @@ function withCliSqliteSync(fn, options) {
27076
27123
  }
27077
27124
  } catch (e) {
27078
27125
  safeCloseCliSqliteDatabase(db);
27126
+ if (e instanceof CliSqliteInterrupted) throw e;
27079
27127
  if (!isCliSqliteLockError(e) || attempt === CLI_SQLITE_SYNC_RETRY_MAX) throw e;
27080
27128
  syncSleepMs(Math.min(500, 12 * attempt));
27081
27129
  }
@@ -27085,6 +27133,7 @@ function withCliSqliteSync(fn, options) {
27085
27133
  async function withCliSqlite(fn, options) {
27086
27134
  let lastError;
27087
27135
  for (let attempt = 1; attempt <= CLI_SQLITE_ASYNC_RETRY_MAX; attempt++) {
27136
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
27088
27137
  let db;
27089
27138
  try {
27090
27139
  db = openCliSqliteConnection(options);
@@ -27096,9 +27145,10 @@ async function withCliSqlite(fn, options) {
27096
27145
  } catch (e) {
27097
27146
  lastError = e;
27098
27147
  safeCloseCliSqliteDatabase(db);
27148
+ if (e instanceof CliSqliteInterrupted) throw e;
27099
27149
  if (!isCliSqliteLockError(e) || attempt === CLI_SQLITE_ASYNC_RETRY_MAX) throw e;
27100
27150
  const delayMs = Math.min(600, CLI_SQLITE_ASYNC_BASE_DELAY_MS * attempt);
27101
- await asyncDelayMs(delayMs);
27151
+ await asyncDelayMsInterruptible(delayMs);
27102
27152
  await yieldToEventLoop();
27103
27153
  }
27104
27154
  }
@@ -27111,9 +27161,9 @@ async function ensureCliSqliteInitialized(options) {
27111
27161
 
27112
27162
  // src/connection/close-bridge-connection.ts
27113
27163
  async function closeBridgeConnection(state, acpManager, devServerManager, log2) {
27164
+ requestCliImmediateShutdown();
27114
27165
  const say = log2 ?? logImmediate;
27115
27166
  say("Cleaning up connections\u2026");
27116
- await new Promise((resolve20) => setImmediate(resolve20));
27117
27167
  state.closedByUser = true;
27118
27168
  clearReconnectQuietTimer(state.mainQuiet);
27119
27169
  clearReconnectQuietTimer(state.firehoseQuiet);
@@ -27150,7 +27200,7 @@ async function closeBridgeConnection(state, acpManager, devServerManager, log2)
27150
27200
  }
27151
27201
  if (devServerManager) {
27152
27202
  say("Stopping local dev server processes\u2026");
27153
- await devServerManager.shutdownAllGraceful();
27203
+ await devServerManager.shutdownAllGraceful({ graceMs: BRIDGE_CLOSE_DEV_SERVER_GRACE_MS });
27154
27204
  }
27155
27205
  try {
27156
27206
  closeAllCliSqliteConnections();
@@ -27733,17 +27783,13 @@ function getAgentModelFromAgentConfig(config2) {
27733
27783
  return t !== "" ? t : null;
27734
27784
  }
27735
27785
 
27736
- // src/git/session-git-queue.ts
27737
- import { execFile as execFile7 } from "node:child_process";
27786
+ // src/git/snapshot/session-git-queue.ts
27738
27787
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
27739
- import { promisify as promisify7 } from "node:util";
27740
27788
  import * as path15 from "node:path";
27741
27789
 
27742
- // src/git/pre-turn-snapshot.ts
27790
+ // src/git/snapshot/pre-turn-snapshot.ts
27743
27791
  import * as fs15 from "node:fs";
27744
27792
  import * as path14 from "node:path";
27745
- import { execFile as execFile6 } from "node:child_process";
27746
- import { promisify as promisify6 } from "node:util";
27747
27793
 
27748
27794
  // src/git/discover-repos.ts
27749
27795
  import * as fs14 from "node:fs";
@@ -32308,12 +32354,68 @@ function gitInstanceFactory(baseDir, options) {
32308
32354
  init_git_response_error();
32309
32355
  var simpleGit = gitInstanceFactory;
32310
32356
 
32357
+ // src/git/git-runtime.ts
32358
+ var activeGitChildProcesses = /* @__PURE__ */ new Set();
32359
+ function abortActiveGitChildProcesses() {
32360
+ for (const child of activeGitChildProcesses) {
32361
+ try {
32362
+ child.kill("SIGTERM");
32363
+ } catch {
32364
+ }
32365
+ }
32366
+ }
32367
+ var GitOperationAbortedError = class extends Error {
32368
+ constructor(message = "Git operation aborted") {
32369
+ super(message);
32370
+ this.name = "GitOperationAbortedError";
32371
+ }
32372
+ };
32373
+ function throwIfGitShutdownRequested() {
32374
+ if (isCliImmediateShutdownRequested()) {
32375
+ abortActiveGitChildProcesses();
32376
+ throw new GitOperationAbortedError();
32377
+ }
32378
+ }
32379
+ async function runGitTask(fn) {
32380
+ throwIfGitShutdownRequested();
32381
+ try {
32382
+ const result = await fn();
32383
+ throwIfGitShutdownRequested();
32384
+ return result;
32385
+ } catch (e) {
32386
+ if (isCliImmediateShutdownRequested()) {
32387
+ throw new GitOperationAbortedError();
32388
+ }
32389
+ throw e;
32390
+ }
32391
+ }
32392
+ async function yieldToEventLoop2() {
32393
+ throwIfGitShutdownRequested();
32394
+ await new Promise((resolve20) => {
32395
+ setImmediate(resolve20);
32396
+ });
32397
+ throwIfGitShutdownRequested();
32398
+ }
32399
+ async function forEachWithGitYield(items, fn) {
32400
+ for (let i = 0; i < items.length; i++) {
32401
+ if (i > 0 || items.length > 1) await yieldToEventLoop2();
32402
+ await fn(items[i], i);
32403
+ }
32404
+ }
32405
+
32311
32406
  // src/git/cli-simple-git.ts
32312
32407
  function cliSimpleGit(baseDir) {
32313
- const git = simpleGit({ baseDir });
32408
+ const git = simpleGit({
32409
+ baseDir,
32410
+ trimmed: true,
32411
+ spawnOptions: {}
32412
+ });
32314
32413
  git.outputHandler((command, stdout, stderr) => {
32315
32414
  const trace = isCliTrace();
32316
32415
  const onChunk = (label) => (chunk) => {
32416
+ if (isCliImmediateShutdownRequested()) {
32417
+ abortActiveGitChildProcesses();
32418
+ }
32317
32419
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
32318
32420
  const line = text.replace(/\s+$/, "");
32319
32421
  if (trace && line) {
@@ -32402,17 +32504,37 @@ async function discoverGitReposUnderRoot(rootPath) {
32402
32504
  return out;
32403
32505
  }
32404
32506
 
32405
- // src/git/pre-turn-snapshot.ts
32507
+ // src/git/git-exec.ts
32508
+ import { execFile as execFile6, spawn as spawn2 } from "node:child_process";
32509
+ import { promisify as promisify6 } from "node:util";
32406
32510
  var execFileAsync5 = promisify6(execFile6);
32511
+ async function execGitFile(args, options) {
32512
+ throwIfGitShutdownRequested();
32513
+ try {
32514
+ const result = await execFileAsync5("git", args, {
32515
+ maxBuffer: 10 * 1024 * 1024,
32516
+ ...options
32517
+ });
32518
+ throwIfGitShutdownRequested();
32519
+ return {
32520
+ stdout: String(result.stdout ?? ""),
32521
+ stderr: String(result.stderr ?? "")
32522
+ };
32523
+ } catch (e) {
32524
+ if (isCliImmediateShutdownRequested()) {
32525
+ throw new GitOperationAbortedError();
32526
+ }
32527
+ throw e;
32528
+ }
32529
+ }
32530
+
32531
+ // src/git/snapshot/pre-turn-snapshot.ts
32407
32532
  function snapshotsDirForCwd(agentCwd) {
32408
32533
  return path14.join(agentCwd, ".buildautomaton", "snapshots");
32409
32534
  }
32410
32535
  async function gitStashCreate(repoRoot, log2) {
32411
32536
  try {
32412
- const { stdout } = await execFileAsync5("git", ["stash", "create"], {
32413
- cwd: repoRoot,
32414
- maxBuffer: 10 * 1024 * 1024
32415
- });
32537
+ const { stdout } = await execGitFile(["stash", "create"], { cwd: repoRoot });
32416
32538
  return stdout.trim();
32417
32539
  } catch (e) {
32418
32540
  log2(
@@ -32423,7 +32545,7 @@ async function gitStashCreate(repoRoot, log2) {
32423
32545
  }
32424
32546
  async function gitRun(repoRoot, args, log2, label) {
32425
32547
  try {
32426
- await execFileAsync5("git", args, { cwd: repoRoot, maxBuffer: 10 * 1024 * 1024 });
32548
+ await execGitFile(args, { cwd: repoRoot });
32427
32549
  return { ok: true };
32428
32550
  } catch (e) {
32429
32551
  const msg = e instanceof Error ? e.message : String(e);
@@ -32457,10 +32579,10 @@ async function capturePreTurnSnapshot(options) {
32457
32579
  return { ok: false, error: "No git repos to snapshot" };
32458
32580
  }
32459
32581
  const repos = [];
32460
- for (const root of repoRoots) {
32582
+ await forEachWithGitYield(repoRoots, async (root) => {
32461
32583
  const stashSha = await gitStashCreate(root, log2);
32462
32584
  repos.push({ path: root, stashSha });
32463
- }
32585
+ });
32464
32586
  const dir = snapshotsDirForCwd(agentCwd);
32465
32587
  try {
32466
32588
  fs15.mkdirSync(dir, { recursive: true });
@@ -32495,17 +32617,25 @@ async function applyPreTurnSnapshot(filePath, log2) {
32495
32617
  if (!Array.isArray(data.repos)) {
32496
32618
  return { ok: false, error: "Invalid snapshot file" };
32497
32619
  }
32498
- for (const r of data.repos) {
32499
- if (!r.path) continue;
32620
+ let applyError = null;
32621
+ await forEachWithGitYield(data.repos, async (r) => {
32622
+ if (applyError || !r.path) return;
32500
32623
  const reset = await gitRun(r.path, ["reset", "--hard", "HEAD"], log2, "reset --hard");
32501
- if (!reset.ok) return reset;
32624
+ if (!reset.ok) {
32625
+ applyError = reset;
32626
+ return;
32627
+ }
32502
32628
  const clean = await gitRun(r.path, ["clean", "-fd"], log2, "clean -fd");
32503
- if (!clean.ok) return clean;
32629
+ if (!clean.ok) {
32630
+ applyError = clean;
32631
+ return;
32632
+ }
32504
32633
  if (r.stashSha) {
32505
32634
  const ap = await gitRun(r.path, ["stash", "apply", r.stashSha], log2, "stash apply");
32506
- if (!ap.ok) return ap;
32635
+ if (!ap.ok) applyError = ap;
32507
32636
  }
32508
- }
32637
+ });
32638
+ if (applyError?.ok === false) return applyError;
32509
32639
  log2(`[snapshot] Restored pre-turn state for ${data.runId.slice(0, 8)}\u2026`);
32510
32640
  return { ok: true };
32511
32641
  }
@@ -32513,8 +32643,7 @@ function snapshotFilePath(agentCwd, runId) {
32513
32643
  return path14.join(snapshotsDirForCwd(agentCwd), `${runId}.json`);
32514
32644
  }
32515
32645
 
32516
- // src/git/session-git-queue.ts
32517
- var execFileAsync6 = promisify7(execFile7);
32646
+ // src/git/snapshot/session-git-queue.ts
32518
32647
  var MAX_FULL_FILE_TEXT_BYTES = 512 * 1024;
32519
32648
  async function readWorkspaceFileAsUtf8(absPath) {
32520
32649
  try {
@@ -32543,35 +32672,28 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
32543
32672
  return;
32544
32673
  }
32545
32674
  const multiRepo = data.repos.length > 1;
32546
- for (const repo of data.repos) {
32547
- if (!repo.stashSha) continue;
32675
+ await forEachWithGitYield(data.repos, async (repo) => {
32676
+ if (!repo.stashSha) return;
32548
32677
  let namesRaw;
32549
32678
  try {
32550
- const { stdout } = await execFileAsync6("git", ["diff", "--name-only", repo.stashSha], {
32551
- cwd: repo.path,
32552
- maxBuffer: 10 * 1024 * 1024
32553
- });
32679
+ const { stdout } = await execGitFile(["diff", "--name-only", repo.stashSha], { cwd: repo.path });
32554
32680
  namesRaw = stdout;
32555
32681
  } catch (e) {
32556
32682
  log2(
32557
32683
  `[session-git-queue] Git diff --name-only failed in ${repo.path}: ${e instanceof Error ? e.message : String(e)}`
32558
32684
  );
32559
- continue;
32685
+ return;
32560
32686
  }
32561
32687
  const lines = namesRaw.split("\n").map((l) => l.trim()).filter(Boolean);
32562
32688
  const slug = path15.basename(repo.path).replace(/[^\w.-]+/g, "_") || "repo";
32563
- for (const rel of lines) {
32564
- if (rel.includes("..")) continue;
32689
+ await forEachWithGitYield(lines, async (rel) => {
32690
+ if (rel.includes("..")) return;
32565
32691
  try {
32566
- const { stdout: patchContent } = await execFileAsync6(
32567
- "git",
32692
+ const { stdout: patchContent } = await execGitFile(
32568
32693
  ["diff", "--no-color", repo.stashSha, "--", rel],
32569
- {
32570
- cwd: repo.path,
32571
- maxBuffer: 50 * 1024 * 1024
32572
- }
32694
+ { cwd: repo.path }
32573
32695
  );
32574
- if (!patchContent.trim()) continue;
32696
+ if (!patchContent.trim()) return;
32575
32697
  const displayPath = multiRepo ? `${slug}/${rel}` : rel;
32576
32698
  const workspaceFilePath = path15.join(repo.path, rel);
32577
32699
  const newText = await readWorkspaceFileAsUtf8(workspaceFilePath);
@@ -32588,8 +32710,8 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
32588
32710
  `[session-git-queue] Git diff failed for ${rel}: ${e instanceof Error ? e.message : String(e)}`
32589
32711
  );
32590
32712
  }
32591
- }
32592
- }
32713
+ });
32714
+ });
32593
32715
  }
32594
32716
 
32595
32717
  // src/agents/acp/put-summarize-change-summaries.ts
@@ -32929,16 +33051,38 @@ __export(claude_code_acp_client_exports, {
32929
33051
  createClaudeCodeAcpClient: () => createClaudeCodeAcpClient,
32930
33052
  detectLocalAgentPresence: () => detectLocalAgentPresence
32931
33053
  });
32932
- import { execFile as execFile9 } from "node:child_process";
32933
- import { promisify as promisify9 } from "node:util";
32934
33054
 
32935
33055
  // src/agents/acp/clients/detect-command-on-path.ts
32936
- import { execFile as execFile8 } from "node:child_process";
32937
- import { promisify as promisify8 } from "node:util";
32938
- var execFileAsync7 = promisify8(execFile8);
32939
- async function isCommandOnPath(command, timeoutMs = 4e3) {
33056
+ import { execFile as execFile7 } from "node:child_process";
33057
+ import { promisify as promisify7 } from "node:util";
33058
+ var execFileAsync6 = promisify7(execFile7);
33059
+ var COMMAND_ON_PATH_PROBE_TIMEOUT_MS = 750;
33060
+ async function execFileShutdownAware(file2, args, timeoutMs) {
33061
+ if (isCliImmediateShutdownRequested()) throw new Error("shutdown");
33062
+ const ac = new AbortController();
33063
+ const shutdownPoll = setInterval(() => {
33064
+ if (isCliImmediateShutdownRequested()) ac.abort();
33065
+ }, 50);
33066
+ shutdownPoll.unref?.();
33067
+ try {
33068
+ await execFileAsync6(file2, args, { timeout: timeoutMs, signal: ac.signal });
33069
+ } finally {
33070
+ clearInterval(shutdownPoll);
33071
+ }
33072
+ }
33073
+ async function isCommandOnPath(command, timeoutMs = COMMAND_ON_PATH_PROBE_TIMEOUT_MS) {
33074
+ if (isCliImmediateShutdownRequested()) return false;
33075
+ try {
33076
+ await execFileShutdownAware("which", [command], timeoutMs);
33077
+ return true;
33078
+ } catch {
33079
+ return false;
33080
+ }
33081
+ }
33082
+ async function execProbeShutdownAware(file2, args, timeoutMs) {
33083
+ if (isCliImmediateShutdownRequested()) return false;
32940
33084
  try {
32941
- await execFileAsync7("which", [command], { timeout: timeoutMs });
33085
+ await execFileShutdownAware(file2, args, timeoutMs);
32942
33086
  return true;
32943
33087
  } catch {
32944
33088
  return false;
@@ -32946,7 +33090,7 @@ async function isCommandOnPath(command, timeoutMs = 4e3) {
32946
33090
  }
32947
33091
 
32948
33092
  // src/agents/acp/clients/sdk/sdk-stdio-acp-client.ts
32949
- import { spawn as spawn2 } from "node:child_process";
33093
+ import { spawn as spawn3 } from "node:child_process";
32950
33094
  import { Readable, Writable } from "node:stream";
32951
33095
 
32952
33096
  // src/agents/acp/clients/agent-stderr-capture.ts
@@ -33541,7 +33685,7 @@ async function createSdkStdioAcpClient(options) {
33541
33685
  getActiveConfigOptions
33542
33686
  } = options;
33543
33687
  const isWindows = process.platform === "win32";
33544
- const child = spawn2(command[0], command.slice(1), {
33688
+ const child = spawn3(command[0], command.slice(1), {
33545
33689
  cwd,
33546
33690
  stdio: ["pipe", "pipe", "pipe"],
33547
33691
  env: process.env,
@@ -33699,16 +33843,10 @@ async function createSdkStdioAcpClient(options) {
33699
33843
  }
33700
33844
 
33701
33845
  // src/agents/acp/clients/claude-code-acp-client.ts
33702
- var execFileAsync8 = promisify9(execFile9);
33703
33846
  var BACKEND_LOCAL_AGENT_TYPE = "claude-code";
33704
33847
  async function detectLocalAgentPresence() {
33705
33848
  if (await isCommandOnPath("claude")) return true;
33706
- try {
33707
- await execFileAsync8("npx", ["--yes", "@anthropic-ai/claude-code", "--version"], { timeout: 25e3 });
33708
- return true;
33709
- } catch {
33710
- return false;
33711
- }
33849
+ return execProbeShutdownAware("npx", ["--yes", "@anthropic-ai/claude-code", "--version"], 3e3);
33712
33850
  }
33713
33851
  function buildClaudeCodeAcpSpawnCommand(base, _sessionMode) {
33714
33852
  return [...base];
@@ -33760,7 +33898,7 @@ __export(cursor_acp_client_exports, {
33760
33898
  createCursorAcpClient: () => createCursorAcpClient,
33761
33899
  detectLocalAgentPresence: () => detectLocalAgentPresence3
33762
33900
  });
33763
- import { spawn as spawn3 } from "node:child_process";
33901
+ import { spawn as spawn4 } from "node:child_process";
33764
33902
  import * as readline from "node:readline";
33765
33903
 
33766
33904
  // src/agents/acp/format-session-update-kind-for-log.ts
@@ -33830,7 +33968,7 @@ async function createCursorAcpClient(options) {
33830
33968
  } = options;
33831
33969
  const dbgFs = process.env.BUILDAUTOMATON_DEBUG_ACP_FS === "1";
33832
33970
  const isWindows = process.platform === "win32";
33833
- const child = spawn3(command[0], command.slice(1), {
33971
+ const child = spawn4(command[0], command.slice(1), {
33834
33972
  cwd,
33835
33973
  stdio: ["pipe", "pipe", "pipe"],
33836
33974
  env: process.env,
@@ -35276,7 +35414,7 @@ import os8 from "node:os";
35276
35414
  import * as fs18 from "node:fs";
35277
35415
  import * as path21 from "node:path";
35278
35416
 
35279
- // src/git/worktree-add.ts
35417
+ // src/git/worktrees/worktree-add.ts
35280
35418
  async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch) {
35281
35419
  const mainGit = cliSimpleGit(mainRepoPath);
35282
35420
  await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
@@ -35381,7 +35519,7 @@ async function prepareNewSessionWorktrees(options) {
35381
35519
  };
35382
35520
  }
35383
35521
 
35384
- // src/git/rename-branch.ts
35522
+ // src/git/branches/rename-branch.ts
35385
35523
  async function gitRenameCurrentBranch(repoDir, newName) {
35386
35524
  const g = cliSimpleGit(repoDir);
35387
35525
  await g.raw(["branch", "-m", newName]);
@@ -35405,10 +35543,10 @@ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
35405
35543
  // src/worktrees/remove-session-worktrees.ts
35406
35544
  import * as fs21 from "node:fs";
35407
35545
 
35408
- // src/git/worktree-remove.ts
35546
+ // src/git/worktrees/worktree-remove.ts
35409
35547
  import * as fs20 from "node:fs";
35410
35548
 
35411
- // src/git/resolve-main-repo-from-git-file.ts
35549
+ // src/git/worktrees/resolve-main-repo-from-git-file.ts
35412
35550
  import * as fs19 from "node:fs";
35413
35551
  import * as path22 from "node:path";
35414
35552
  function resolveMainRepoFromWorktreeGitFile(wt) {
@@ -35422,7 +35560,7 @@ function resolveMainRepoFromWorktreeGitFile(wt) {
35422
35560
  return path22.dirname(gitDir);
35423
35561
  }
35424
35562
 
35425
- // src/git/worktree-remove.ts
35563
+ // src/git/worktrees/worktree-remove.ts
35426
35564
  async function gitWorktreeRemoveForce(worktreePath) {
35427
35565
  const mainRepo = resolveMainRepoFromWorktreeGitFile(worktreePath);
35428
35566
  if (mainRepo) {
@@ -35448,7 +35586,88 @@ async function removeSessionWorktrees(paths, log2) {
35448
35586
  }
35449
35587
  }
35450
35588
 
35451
- // src/git/working-directory/status/working-tree-status.ts
35589
+ // src/git/changes/lib/parse-git-status.ts
35590
+ function parseNameStatusLines(lines) {
35591
+ const m = /* @__PURE__ */ new Map();
35592
+ for (const line of lines) {
35593
+ if (!line.trim()) continue;
35594
+ const tabParts = line.split(" ");
35595
+ if (tabParts.length < 2) continue;
35596
+ const status = tabParts[0].trim();
35597
+ const code = status[0];
35598
+ if (code === "A") {
35599
+ m.set(tabParts[tabParts.length - 1], "added");
35600
+ } else if (code === "D") {
35601
+ m.set(tabParts[tabParts.length - 1], "removed");
35602
+ } else if (code === "R" || code === "C") {
35603
+ if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
35604
+ } else if (code === "M" || code === "U" || code === "T") {
35605
+ m.set(tabParts[tabParts.length - 1], "modified");
35606
+ }
35607
+ }
35608
+ return m;
35609
+ }
35610
+ function parseNumstatFirstLine(line) {
35611
+ const parts = line.split(" ");
35612
+ if (parts.length < 3) return null;
35613
+ const [a, d] = parts;
35614
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35615
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35616
+ return { additions, deletions };
35617
+ }
35618
+ function parseNumstat(lines) {
35619
+ const m = /* @__PURE__ */ new Map();
35620
+ for (const line of lines) {
35621
+ if (!line.trim()) continue;
35622
+ const parts = line.split(" ");
35623
+ if (parts.length < 3) continue;
35624
+ const [a, d, p] = parts;
35625
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35626
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35627
+ m.set(p, { additions, deletions });
35628
+ }
35629
+ return m;
35630
+ }
35631
+ async function numstatFromGitNoIndex(g, pathInRepo) {
35632
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35633
+ try {
35634
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
35635
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
35636
+ return parseNumstatFirstLine(first2);
35637
+ } catch {
35638
+ return null;
35639
+ }
35640
+ }
35641
+
35642
+ // src/git/changes/lib/working-tree-changed-path-count.ts
35643
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
35644
+ const kindByPath = parseNameStatusLines(nameStatusLines);
35645
+ const numByPath = parseNumstat(numstatLines);
35646
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
35647
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
35648
+ paths.add(p);
35649
+ }
35650
+ return paths.size;
35651
+ }
35652
+
35653
+ // src/git/changes/count-working-tree-changed-files.ts
35654
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
35655
+ return runGitTask(async () => {
35656
+ const g = cliSimpleGit(repoGitCwd);
35657
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
35658
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
35659
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
35660
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
35661
+ ]);
35662
+ return workingTreeChangedPathCount(
35663
+ String(nameStatusRaw).split("\n"),
35664
+ String(numstatRaw).split("\n"),
35665
+ String(untrackedRaw).split("\n")
35666
+ );
35667
+ });
35668
+ }
35669
+
35670
+ // src/git/commits/resolve-remote-tracking.ts
35452
35671
  async function tryConfigGet(g, key) {
35453
35672
  try {
35454
35673
  const out = await g.raw(["config", "--get", key]);
@@ -35531,30 +35750,6 @@ async function resolveBaseShaForUnpushedCommits(g) {
35531
35750
  if (!defaultRef) return null;
35532
35751
  return revParseSafe(g, defaultRef);
35533
35752
  }
35534
- function parseLogShaDateSubjectLines(raw) {
35535
- const out = [];
35536
- for (const line of String(raw).split("\n")) {
35537
- const l = line.trimEnd();
35538
- if (!l.trim()) continue;
35539
- const parts = l.split(" ");
35540
- if (parts.length < 3) continue;
35541
- const sha = parts[0].trim();
35542
- const committedAt = parts[1].trim();
35543
- const subject = parts.slice(2).join(" ").trim();
35544
- if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
35545
- out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
35546
- }
35547
- return out;
35548
- }
35549
- async function gitLogNotReachableFromBase(g, baseSha, headSha) {
35550
- if (baseSha === headSha) return [];
35551
- try {
35552
- const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
35553
- return parseLogShaDateSubjectLines(logOut);
35554
- } catch {
35555
- return [];
35556
- }
35557
- }
35558
35753
  async function commitsAheadOfRemoteTracking(repoDir) {
35559
35754
  const g = cliSimpleGit(repoDir);
35560
35755
  const headSha = await revParseSafe(g, "HEAD");
@@ -35569,44 +35764,41 @@ async function commitsAheadOfRemoteTracking(repoDir) {
35569
35764
  return 0;
35570
35765
  }
35571
35766
  }
35767
+
35768
+ // src/git/status/working-tree-status.ts
35572
35769
  async function getRepoWorkingTreeStatus(repoDir) {
35573
- const g = cliSimpleGit(repoDir);
35574
- const st = await g.status();
35575
- const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
35576
- const ahead = await commitsAheadOfRemoteTracking(repoDir);
35577
- return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0 };
35578
- }
35579
- async function listUnpushedCommits(repoDir) {
35580
- const g = cliSimpleGit(repoDir);
35581
- const headSha = await revParseSafe(g, "HEAD");
35582
- if (!headSha) return [];
35583
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
35584
- if (!baseSha) return [];
35585
- return gitLogNotReachableFromBase(g, baseSha, headSha);
35770
+ return runGitTask(async () => {
35771
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
35772
+ const hasUncommittedChanges = uncommittedFileCount > 0;
35773
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
35774
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
35775
+ });
35586
35776
  }
35587
35777
  async function aggregateSessionPathsWorkingTreeStatus(paths) {
35588
35778
  let hasUncommittedChanges = false;
35589
35779
  let hasUnpushedCommits = false;
35590
- for (const p of paths) {
35780
+ let uncommittedFileCount = 0;
35781
+ await forEachWithGitYield(paths, async (p) => {
35591
35782
  const s = await getRepoWorkingTreeStatus(p);
35783
+ uncommittedFileCount += s.uncommittedFileCount;
35592
35784
  if (s.hasUncommittedChanges) hasUncommittedChanges = true;
35593
35785
  if (s.hasUnpushedCommits) hasUnpushedCommits = true;
35594
- }
35595
- return { hasUncommittedChanges, hasUnpushedCommits };
35786
+ });
35787
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
35596
35788
  }
35597
35789
  async function pushAheadOfUpstreamForPaths(paths) {
35598
- for (const p of paths) {
35790
+ await forEachWithGitYield(paths, async (p) => {
35599
35791
  const g = cliSimpleGit(p);
35600
35792
  const ahead = await commitsAheadOfRemoteTracking(p);
35601
- if (ahead <= 0) continue;
35793
+ if (ahead <= 0) return;
35602
35794
  await g.push();
35603
- }
35795
+ });
35604
35796
  }
35605
35797
 
35606
- // src/git/working-directory/changes/types.ts
35798
+ // src/git/changes/types.ts
35607
35799
  var MAX_PATCH_CHARS = 35e4;
35608
35800
 
35609
- // src/git/working-directory/changes/repo-format.ts
35801
+ // src/git/changes/lib/repo-format.ts
35610
35802
  function posixJoinDirFile(dir, file2) {
35611
35803
  const d = dir === "." || dir === "" ? "" : dir.replace(/\\/g, "/").replace(/\/+$/, "");
35612
35804
  const f = file2.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -35660,63 +35852,80 @@ function formatRemoteDisplayLabel(remoteUrl) {
35660
35852
  return `origin \xB7 ${hostPath}`;
35661
35853
  }
35662
35854
 
35663
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
35855
+ // src/git/changes/get-working-tree-change-repo-details.ts
35664
35856
  import * as path24 from "node:path";
35665
35857
 
35666
- // src/git/working-directory/changes/parse-git-status.ts
35667
- function parseNameStatusLines(lines) {
35668
- const m = /* @__PURE__ */ new Map();
35669
- for (const line of lines) {
35670
- if (!line.trim()) continue;
35671
- const tabParts = line.split(" ");
35672
- if (tabParts.length < 2) continue;
35673
- const status = tabParts[0].trim();
35674
- const code = status[0];
35675
- if (code === "A") {
35676
- m.set(tabParts[tabParts.length - 1], "added");
35677
- } else if (code === "D") {
35678
- m.set(tabParts[tabParts.length - 1], "removed");
35679
- } else if (code === "R" || code === "C") {
35680
- if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
35681
- } else if (code === "M" || code === "U" || code === "T") {
35682
- m.set(tabParts[tabParts.length - 1], "modified");
35683
- }
35858
+ // src/git/commits/lib/parse-log-lines.ts
35859
+ function parseLogShaDateSubjectLines(raw) {
35860
+ const out = [];
35861
+ for (const line of String(raw).split("\n")) {
35862
+ const l = line.trimEnd();
35863
+ if (!l.trim()) continue;
35864
+ const parts = l.split(" ");
35865
+ if (parts.length < 3) continue;
35866
+ const sha = parts[0].trim();
35867
+ const committedAt = parts[1].trim();
35868
+ const subject = parts.slice(2).join(" ").trim();
35869
+ if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
35870
+ out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
35684
35871
  }
35685
- return m;
35686
- }
35687
- function parseNumstatFirstLine(line) {
35688
- const parts = line.split(" ");
35689
- if (parts.length < 3) return null;
35690
- const [a, d] = parts;
35691
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35692
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35693
- return { additions, deletions };
35872
+ return out;
35694
35873
  }
35695
- function parseNumstat(lines) {
35696
- const m = /* @__PURE__ */ new Map();
35697
- for (const line of lines) {
35698
- if (!line.trim()) continue;
35699
- const parts = line.split(" ");
35700
- if (parts.length < 3) continue;
35701
- const [a, d, p] = parts;
35702
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35703
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35704
- m.set(p, { additions, deletions });
35874
+
35875
+ // src/git/commits/list-unpushed-commits.ts
35876
+ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
35877
+ if (baseSha === headSha) return [];
35878
+ try {
35879
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
35880
+ return parseLogShaDateSubjectLines(logOut);
35881
+ } catch {
35882
+ return [];
35705
35883
  }
35706
- return m;
35707
35884
  }
35708
- async function numstatFromGitNoIndex(g, pathInRepo) {
35709
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35885
+ async function listUnpushedCommits(repoDir) {
35886
+ const g = cliSimpleGit(repoDir);
35887
+ const headSha = await revParseSafe(g, "HEAD");
35888
+ if (!headSha) return [];
35889
+ const baseSha = await resolveBaseShaForUnpushedCommits(g);
35890
+ if (!baseSha) return [];
35891
+ return gitLogNotReachableFromBase(g, baseSha, headSha);
35892
+ }
35893
+
35894
+ // src/git/commits/lib/sanitize-recent-commits-limit.ts
35895
+ var RECENT_COMMITS_LIMIT_MIN = 1;
35896
+ var RECENT_COMMITS_LIMIT_MAX = 50;
35897
+ var RECENT_COMMITS_LIMIT_DEFAULT = 2;
35898
+ function sanitizeRecentCommitsLimit(value) {
35899
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : Number.NaN;
35900
+ if (!Number.isFinite(n)) return RECENT_COMMITS_LIMIT_DEFAULT;
35901
+ return Math.min(RECENT_COMMITS_LIMIT_MAX, Math.max(RECENT_COMMITS_LIMIT_MIN, Math.trunc(n)));
35902
+ }
35903
+
35904
+ // src/git/commits/list-recent-commits.ts
35905
+ async function listRecentCommits(repoDir, limitInput) {
35906
+ const limit = sanitizeRecentCommitsLimit(limitInput);
35907
+ const g = cliSimpleGit(repoDir);
35908
+ const headSha = await revParseSafe(g, "HEAD");
35909
+ if (!headSha) return { commits: [], hasMore: false };
35910
+ const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
35710
35911
  try {
35711
- const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
35712
- const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
35713
- return parseNumstatFirstLine(first2);
35912
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
35913
+ const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
35914
+ ...c,
35915
+ needsPush: unpushedSet.has(c.sha)
35916
+ }));
35917
+ let hasMore = false;
35918
+ if (commits.length === limit) {
35919
+ const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
35920
+ hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
35921
+ }
35922
+ return { commits, hasMore };
35714
35923
  } catch {
35715
- return null;
35924
+ return { commits: [], hasMore: false };
35716
35925
  }
35717
35926
  }
35718
35927
 
35719
- // src/git/working-directory/changes/patch-truncate.ts
35928
+ // src/git/changes/lib/patch-truncate.ts
35720
35929
  function truncatePatch(s) {
35721
35930
  if (s.length <= MAX_PATCH_CHARS) return s;
35722
35931
  return `${s.slice(0, MAX_PATCH_CHARS)}
@@ -35724,7 +35933,7 @@ function truncatePatch(s) {
35724
35933
  \u2026 (diff truncated)`;
35725
35934
  }
35726
35935
 
35727
- // src/git/working-directory/changes/list-changed-files-for-commit.ts
35936
+ // src/git/commits/list-changed-files-for-commit.ts
35728
35937
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
35729
35938
  async function parentForCommitDiff(g, sha) {
35730
35939
  try {
@@ -35750,7 +35959,7 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35750
35959
  const paths = new Set([...kindByPath.keys(), ...numByPath.keys()].filter(Boolean));
35751
35960
  const rows = [];
35752
35961
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
35753
- for (const pathInRepo of paths) {
35962
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
35754
35963
  const relLauncher = posixJoinDirFile(normRel, pathInRepo.replace(/\\/g, "/"));
35755
35964
  const nums = numByPath.get(pathInRepo);
35756
35965
  let additions = nums?.additions ?? 0;
@@ -35762,8 +35971,8 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35762
35971
  else change = "modified";
35763
35972
  }
35764
35973
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
35765
- }
35766
- for (const row of rows) {
35974
+ });
35975
+ await forEachWithGitYield(rows, async (row) => {
35767
35976
  let pathInRepo;
35768
35977
  if (normRel === ".") {
35769
35978
  pathInRepo = row.pathRelLauncher;
@@ -35775,16 +35984,16 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35775
35984
  const raw = await g.raw(["diff", "-U20000", range, "--", pathInRepo]).catch(() => "");
35776
35985
  const t = String(raw).trim();
35777
35986
  row.patchContent = t ? truncatePatch(t) : void 0;
35778
- }
35987
+ });
35779
35988
  rows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
35780
35989
  return rows;
35781
35990
  }
35782
35991
 
35783
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
35992
+ // src/git/changes/list-changed-files-for-repo.ts
35784
35993
  import * as fs23 from "node:fs";
35785
35994
  import * as path23 from "node:path";
35786
35995
 
35787
- // src/git/working-directory/changes/count-lines.ts
35996
+ // src/git/changes/lib/count-lines.ts
35788
35997
  import { createReadStream } from "node:fs";
35789
35998
  import * as readline2 from "node:readline";
35790
35999
  async function countTextFileLines(filePath) {
@@ -35805,7 +36014,7 @@ async function countTextFileLines(filePath) {
35805
36014
  return lines;
35806
36015
  }
35807
36016
 
35808
- // src/git/working-directory/changes/hydrate-patch.ts
36017
+ // src/git/changes/hydrate-patch.ts
35809
36018
  import * as fs22 from "node:fs";
35810
36019
  var UNIFIED_HUNK_HEADER_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
35811
36020
  var MAX_HYDRATE_LINES_PER_GAP = 8e3;
@@ -35921,26 +36130,16 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
35921
36130
  return truncatePatch(out.join("\n"));
35922
36131
  }
35923
36132
 
35924
- // src/git/working-directory/changes/unified-diff-for-file.ts
36133
+ // src/git/changes/unified-diff-for-file.ts
35925
36134
  async function unifiedDiffForFile(repoCwd, pathInRepo, change) {
35926
36135
  const g = cliSimpleGit(repoCwd);
35927
- try {
35928
- let raw;
35929
- if (change === "added") {
35930
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35931
- raw = await g.raw(["diff", "--no-index", "--", devNull, pathInRepo]);
35932
- } else {
35933
- raw = await g.raw(["diff", "HEAD", "--", pathInRepo]);
35934
- }
35935
- const t = String(raw).trim();
35936
- if (!t) return void 0;
35937
- return truncatePatch(t);
35938
- } catch {
35939
- return void 0;
35940
- }
36136
+ const args = change === "added" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : change === "removed" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : ["diff", "--no-color", "HEAD", "--", pathInRepo];
36137
+ const raw = await g.raw([...args]).catch(() => "");
36138
+ const t = String(raw).trim();
36139
+ return t ? truncatePatch(t) : void 0;
35941
36140
  }
35942
36141
 
35943
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
36142
+ // src/git/changes/list-changed-files-for-repo.ts
35944
36143
  async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
35945
36144
  const g = cliSimpleGit(repoGitCwd);
35946
36145
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
@@ -35954,7 +36153,7 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
35954
36153
  const untracked = String(untrackedRaw).split("\n").map((s) => s.trim()).filter(Boolean);
35955
36154
  for (const p of untracked) paths.add(p);
35956
36155
  const rows = [];
35957
- for (const pathInRepo of paths) {
36156
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
35958
36157
  const relLauncher = posixJoinDirFile(repoRelPath, pathInRepo.replace(/\\/g, "/"));
35959
36158
  const repoFilePath = path23.join(repoGitCwd, pathInRepo);
35960
36159
  const nums = numByPath.get(pathInRepo);
@@ -35984,9 +36183,9 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
35984
36183
  else change = "modified";
35985
36184
  }
35986
36185
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
35987
- }
36186
+ });
35988
36187
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
35989
- for (const row of rows) {
36188
+ await forEachWithGitYield(rows, async (row) => {
35990
36189
  let pathInRepo;
35991
36190
  if (normRel === ".") {
35992
36191
  pathInRepo = row.pathRelLauncher;
@@ -36001,11 +36200,11 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36001
36200
  patch = await hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, pathInRepo, row.change);
36002
36201
  }
36003
36202
  row.patchContent = patch;
36004
- }
36203
+ });
36005
36204
  return rows;
36006
36205
  }
36007
36206
 
36008
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
36207
+ // src/git/changes/get-working-tree-change-repo-details.ts
36009
36208
  function normRepoRel(p) {
36010
36209
  const x = p.replace(/\\/g, "/").trim();
36011
36210
  return x === "" ? "." : x;
@@ -36024,7 +36223,9 @@ async function getWorkingTreeChangeRepoDetails(options) {
36024
36223
  throw new Error("commit sha is required for commit changes");
36025
36224
  }
36026
36225
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
36027
- for (const target of options.commitTargetPaths) {
36226
+ for (let i = 0; i < options.commitTargetPaths.length; i++) {
36227
+ if (i > 0) await yieldToEventLoop2();
36228
+ const target = options.commitTargetPaths[i];
36028
36229
  const t = path24.resolve(target);
36029
36230
  if (!await isGitRepoDirectory(t)) continue;
36030
36231
  const g = cliSimpleGit(t);
@@ -36058,7 +36259,10 @@ async function getWorkingTreeChangeRepoDetails(options) {
36058
36259
  const files = basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
36059
36260
  const st = await g.status();
36060
36261
  const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
36061
- const unpushedCommits = await listUnpushedCommits(t);
36262
+ const [unpushedCommits, recentCommitList] = await Promise.all([
36263
+ listUnpushedCommits(t),
36264
+ listRecentCommits(t, options.recentCommitsLimit)
36265
+ ]);
36062
36266
  out.push({
36063
36267
  repoRelPath: norm,
36064
36268
  repoDisplayName,
@@ -36068,6 +36272,8 @@ async function getWorkingTreeChangeRepoDetails(options) {
36068
36272
  files,
36069
36273
  hasUncommittedChanges,
36070
36274
  unpushedCommits,
36275
+ recentCommits: recentCommitList.commits,
36276
+ recentCommitsHasMore: recentCommitList.hasMore,
36071
36277
  changesView: basis.kind === "commit" ? "commit" : "working",
36072
36278
  changesCommitSha: basis.kind === "commit" ? basis.sha.trim() : null
36073
36279
  });
@@ -36076,7 +36282,7 @@ async function getWorkingTreeChangeRepoDetails(options) {
36076
36282
  return out;
36077
36283
  }
36078
36284
 
36079
- // src/git/commit-and-push.ts
36285
+ // src/git/branches/commit-and-push.ts
36080
36286
  async function gitCommitAllIfDirty(repoDir, message, options) {
36081
36287
  const g = cliSimpleGit(repoDir);
36082
36288
  const st = await g.status();
@@ -36500,7 +36706,8 @@ var SessionWorktreeManager = class {
36500
36706
  sessionWorktreeRootPath: sessionWorkingTreeRelRoot,
36501
36707
  legacyRepoNestedSessionLayout: legacyNested,
36502
36708
  repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
36503
- basis: opts?.basis
36709
+ basis: opts?.basis,
36710
+ recentCommitsLimit: opts?.recentCommitsLimit
36504
36711
  });
36505
36712
  }
36506
36713
  async pushSessionUpstream(sessionId) {
@@ -36537,30 +36744,39 @@ import path28 from "node:path";
36537
36744
  function shouldSkipWorkspaceWalkEntry(name) {
36538
36745
  return name.startsWith(".");
36539
36746
  }
36540
- function walkWorkspaceTreeSync(dir, baseDir, onFile) {
36747
+ async function walkWorkspaceTreeAsync(dir, baseDir, onFile, state) {
36541
36748
  let names;
36542
36749
  try {
36543
- names = fs25.readdirSync(dir);
36750
+ names = await fs25.promises.readdir(dir);
36544
36751
  } catch {
36545
36752
  return;
36546
36753
  }
36547
36754
  for (const name of names) {
36548
36755
  if (shouldSkipWorkspaceWalkEntry(name)) continue;
36756
+ if (state.n > 0 && state.n % INDEX_WORK_YIELD_EVERY === 0) {
36757
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
36758
+ await yieldToEventLoop();
36759
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
36760
+ }
36761
+ state.n++;
36549
36762
  const full = path28.join(dir, name);
36550
36763
  let stat3;
36551
36764
  try {
36552
- stat3 = fs25.statSync(full);
36765
+ stat3 = await fs25.promises.stat(full);
36553
36766
  } catch {
36554
36767
  continue;
36555
36768
  }
36556
36769
  const relative5 = path28.relative(baseDir, full).replace(/\\/g, "/");
36557
36770
  if (stat3.isDirectory()) {
36558
- walkWorkspaceTreeSync(full, baseDir, onFile);
36771
+ await walkWorkspaceTreeAsync(full, baseDir, onFile, state);
36559
36772
  } else if (stat3.isFile()) {
36560
36773
  onFile(relative5);
36561
36774
  }
36562
36775
  }
36563
36776
  }
36777
+ function createWalkYieldState() {
36778
+ return { n: 0 };
36779
+ }
36564
36780
 
36565
36781
  // src/files/index/file-index-sqlite-lock.ts
36566
36782
  import fs26 from "node:fs";
@@ -36593,29 +36809,28 @@ function withFileIndexSqliteLock(fn) {
36593
36809
  }
36594
36810
 
36595
36811
  // src/files/index/build-file-index.ts
36596
- var FILE_INDEX_INSERT_BUFFER = 2048;
36597
- function persistFileIndexForResolvedCwd(resolved) {
36812
+ var FILE_INDEX_INTERRUPT_CHECK_EVERY = 256;
36813
+ function assertNotShutdown() {
36814
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
36815
+ }
36816
+ function persistFileIndexPaths(resolved, paths) {
36598
36817
  return withCliSqliteSync((db) => {
36599
36818
  const h = getCwdHashForFileIndex(resolved);
36600
- const buf = [];
36601
36819
  let pathCount = 0;
36602
36820
  db.run("BEGIN IMMEDIATE");
36603
36821
  try {
36604
36822
  db.run("DELETE FROM file_index_path WHERE cwd_hash = ?", [h]);
36605
36823
  const ins = db.prepare("INSERT INTO file_index_path (cwd_hash, path) VALUES (?, ?)");
36606
36824
  try {
36607
- const flushBuf = () => {
36608
- for (const rel of buf) {
36609
- ins.run([h, rel]);
36825
+ let batch = 0;
36826
+ for (const rel of paths) {
36827
+ if (++batch >= FILE_INDEX_INTERRUPT_CHECK_EVERY) {
36828
+ batch = 0;
36829
+ assertNotShutdown();
36610
36830
  }
36611
- pathCount += buf.length;
36612
- buf.length = 0;
36613
- };
36614
- walkWorkspaceTreeSync(resolved, resolved, (rel) => {
36615
- buf.push(rel);
36616
- if (buf.length >= FILE_INDEX_INSERT_BUFFER) flushBuf();
36617
- });
36618
- flushBuf();
36831
+ ins.run([h, rel]);
36832
+ pathCount += 1;
36833
+ }
36619
36834
  } finally {
36620
36835
  ins.finalize();
36621
36836
  }
@@ -36630,12 +36845,24 @@ function persistFileIndexForResolvedCwd(resolved) {
36630
36845
  return pathCount;
36631
36846
  });
36632
36847
  }
36848
+ async function collectWorkspacePathsAsync(resolved) {
36849
+ const paths = [];
36850
+ const state = createWalkYieldState();
36851
+ await walkWorkspaceTreeAsync(resolved, resolved, (rel) => {
36852
+ assertNotShutdown();
36853
+ paths.push(rel);
36854
+ }, state);
36855
+ return paths;
36856
+ }
36633
36857
  async function buildFileIndexAsync(cwd) {
36634
36858
  return withFileIndexSqliteLock(async () => {
36635
36859
  const resolved = path29.resolve(cwd);
36636
36860
  await yieldToEventLoop();
36637
- const pathCount = persistFileIndexForResolvedCwd(resolved);
36861
+ assertNotShutdown();
36862
+ const paths = await collectWorkspacePathsAsync(resolved);
36638
36863
  await yieldToEventLoop();
36864
+ assertNotShutdown();
36865
+ const pathCount = persistFileIndexPaths(resolved, paths);
36639
36866
  return { pathCount };
36640
36867
  });
36641
36868
  }
@@ -36739,11 +36966,13 @@ function createFsWatcher(resolved, schedule) {
36739
36966
  function startFileIndexWatcher(cwd = getBridgeRoot()) {
36740
36967
  const resolved = path31.resolve(cwd);
36741
36968
  void buildFileIndexAsync(resolved).catch((e) => {
36969
+ if (e instanceof CliSqliteInterrupted) return;
36742
36970
  console.error("[file-index] Initial index build failed:", e);
36743
36971
  });
36744
36972
  let timer = null;
36745
36973
  const runRebuild = () => {
36746
36974
  void buildFileIndexAsync(resolved).catch((e) => {
36975
+ if (e instanceof CliSqliteInterrupted) return;
36747
36976
  console.error("[file-index] Watch rebuild failed:", e);
36748
36977
  });
36749
36978
  };
@@ -37008,7 +37237,7 @@ function pipedStdoutStderrFor(attemptStdio) {
37008
37237
  }
37009
37238
 
37010
37239
  // src/dev-servers/manager/shell-spawn/try-spawn-piped-via-sh.ts
37011
- import { spawn as spawn4 } from "node:child_process";
37240
+ import { spawn as spawn5 } from "node:child_process";
37012
37241
  function trySpawnPipedViaSh(command, env, cwd, signal) {
37013
37242
  const attempts = [
37014
37243
  { stdio: [devNullReadFd(), "pipe", "pipe"], endStdin: false },
@@ -37029,9 +37258,9 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
37029
37258
  if (process.platform === "win32") {
37030
37259
  opts.windowsHide = true;
37031
37260
  const com = process.env.ComSpec || "cmd.exe";
37032
- proc = spawn4(com, ["/d", "/s", "/c", command], opts);
37261
+ proc = spawn5(com, ["/d", "/s", "/c", command], opts);
37033
37262
  } else {
37034
- proc = spawn4("/bin/sh", ["-c", command], opts);
37263
+ proc = spawn5("/bin/sh", ["-c", command], opts);
37035
37264
  }
37036
37265
  if (attempt.endStdin) {
37037
37266
  proc.stdin?.end();
@@ -37051,7 +37280,7 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
37051
37280
  }
37052
37281
 
37053
37282
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-true-piped.ts
37054
- import { spawn as spawn5 } from "node:child_process";
37283
+ import { spawn as spawn6 } from "node:child_process";
37055
37284
  function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37056
37285
  try {
37057
37286
  const opts = {
@@ -37064,7 +37293,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37064
37293
  if (process.platform === "win32") {
37065
37294
  opts.windowsHide = true;
37066
37295
  }
37067
- return spawn5(command, opts);
37296
+ return spawn6(command, opts);
37068
37297
  } catch (e) {
37069
37298
  if (isSpawnEbadf(e)) return null;
37070
37299
  throw e;
@@ -37072,7 +37301,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37072
37301
  }
37073
37302
 
37074
37303
  // src/dev-servers/manager/shell-spawn/try-spawn-merged-log-file.ts
37075
- import { spawn as spawn6 } from "node:child_process";
37304
+ import { spawn as spawn7 } from "node:child_process";
37076
37305
  import fs29 from "node:fs";
37077
37306
  import { tmpdir } from "node:os";
37078
37307
  import path32 from "node:path";
@@ -37090,7 +37319,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37090
37319
  try {
37091
37320
  let proc;
37092
37321
  if (process.platform === "win32") {
37093
- proc = spawn6(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
37322
+ proc = spawn7(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
37094
37323
  env,
37095
37324
  cwd,
37096
37325
  stdio,
@@ -37098,7 +37327,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37098
37327
  ...signal ? { signal } : {}
37099
37328
  });
37100
37329
  } else {
37101
- proc = spawn6("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
37330
+ proc = spawn7("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
37102
37331
  }
37103
37332
  fs29.closeSync(logFd);
37104
37333
  return {
@@ -37119,7 +37348,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37119
37348
  }
37120
37349
 
37121
37350
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-script-log-redirect.ts
37122
- import { spawn as spawn7 } from "node:child_process";
37351
+ import { spawn as spawn8 } from "node:child_process";
37123
37352
  import fs30 from "node:fs";
37124
37353
  import { tmpdir as tmpdir2 } from "node:os";
37125
37354
  import path33 from "node:path";
@@ -37142,7 +37371,7 @@ cd ${shSingleQuote(cwd)}
37142
37371
  /bin/sh ${shSingleQuote(innerPath)} >>${shSingleQuote(logPath)} 2>&1
37143
37372
  `
37144
37373
  );
37145
- const proc = spawn7("/bin/sh", [runnerPath], {
37374
+ const proc = spawn8("/bin/sh", [runnerPath], {
37146
37375
  env,
37147
37376
  cwd: tmpRoot,
37148
37377
  stdio: "ignore",
@@ -37174,7 +37403,7 @@ CD /D ${q(cwd)}\r
37174
37403
  ${command} >> ${q(logPath)} 2>&1\r
37175
37404
  `
37176
37405
  );
37177
- const proc = spawn7(com, ["/d", "/s", "/c", q(runnerPath)], {
37406
+ const proc = spawn8(com, ["/d", "/s", "/c", q(runnerPath)], {
37178
37407
  env,
37179
37408
  cwd: tmpRoot,
37180
37409
  stdio: "ignore",
@@ -37195,7 +37424,7 @@ ${command} >> ${q(logPath)} 2>&1\r
37195
37424
  }
37196
37425
 
37197
37426
  // src/dev-servers/manager/shell-spawn/try-spawn-inherit.ts
37198
- import { spawn as spawn8 } from "node:child_process";
37427
+ import { spawn as spawn9 } from "node:child_process";
37199
37428
  function trySpawnInheritStdio(command, env, cwd, signal) {
37200
37429
  const opts = {
37201
37430
  env,
@@ -37207,9 +37436,9 @@ function trySpawnInheritStdio(command, env, cwd, signal) {
37207
37436
  if (process.platform === "win32") {
37208
37437
  opts.windowsHide = true;
37209
37438
  const com = process.env.ComSpec || "cmd.exe";
37210
- proc = spawn8(com, ["/d", "/s", "/c", command], opts);
37439
+ proc = spawn9(com, ["/d", "/s", "/c", command], opts);
37211
37440
  } else {
37212
- proc = spawn8("/bin/sh", ["-c", command], opts);
37441
+ proc = spawn9("/bin/sh", ["-c", command], opts);
37213
37442
  }
37214
37443
  return { proc, pipedStdoutStderr: false };
37215
37444
  }
@@ -37277,9 +37506,6 @@ var StreamTail = class {
37277
37506
  }
37278
37507
  };
37279
37508
 
37280
- // src/dev-servers/manager/dev-server-constants.ts
37281
- var BRIDGE_SHUTDOWN_GRACE_MS = 8e3;
37282
-
37283
37509
  // src/dev-servers/manager/dev-server-firehose-messages.ts
37284
37510
  function buildFirehoseSnapshotMessage(params) {
37285
37511
  const payload = {
@@ -37565,22 +37791,23 @@ var DevServerManager = class {
37565
37791
  }
37566
37792
  this.start(serverId);
37567
37793
  }
37568
- async shutdownAllGraceful() {
37794
+ async shutdownAllGraceful(opts) {
37795
+ const graceMs = opts?.graceMs ?? BRIDGE_SHUTDOWN_GRACE_MS;
37569
37796
  const pairs = [...this.processes.entries()];
37570
37797
  if (pairs.length === 0) return;
37571
37798
  this.log(
37572
37799
  `[dev-server] Stopping ${pairs.length} local dev server process${pairs.length === 1 ? "" : "es"}\u2026`
37573
37800
  );
37574
- await Promise.all(pairs.map(([serverId, proc]) => this.gracefulTerminateOrUnknown(serverId, proc)));
37801
+ await Promise.all(pairs.map(([serverId, proc]) => this.gracefulTerminateOrUnknown(serverId, proc, graceMs)));
37575
37802
  }
37576
- async gracefulTerminateOrUnknown(serverId, proc) {
37803
+ async gracefulTerminateOrUnknown(serverId, proc, graceMs) {
37577
37804
  const shortId = `${serverId.slice(0, 8)}\u2026`;
37578
- await sigtermAndWaitForExit(proc, BRIDGE_SHUTDOWN_GRACE_MS, this.log, shortId);
37805
+ await sigtermAndWaitForExit(proc, graceMs, this.log, shortId);
37579
37806
  if (!this.processes.has(serverId) || this.processes.get(serverId) !== proc) {
37580
37807
  return;
37581
37808
  }
37582
37809
  this.bumpGeneration(serverId);
37583
- forceKillChild(proc, this.log, shortId, BRIDGE_SHUTDOWN_GRACE_MS);
37810
+ forceKillChild(proc, this.log, shortId, graceMs);
37584
37811
  this.processes.delete(serverId);
37585
37812
  this.clearPoll(serverId);
37586
37813
  this.pipedCaptureByServerId.delete(serverId);
@@ -38175,10 +38402,13 @@ var LOCAL_AGENT_ACP_MODULES = [
38175
38402
  ];
38176
38403
  async function detectLocalAgentTypes() {
38177
38404
  try {
38405
+ if (isCliImmediateShutdownRequested()) return [];
38178
38406
  const out = [];
38179
38407
  for (let i = 0; i < LOCAL_AGENT_ACP_MODULES.length; i++) {
38408
+ if (isCliImmediateShutdownRequested()) return out;
38180
38409
  if (i > 0) {
38181
38410
  await yieldToEventLoop();
38411
+ if (isCliImmediateShutdownRequested()) return out;
38182
38412
  }
38183
38413
  const mod = LOCAL_AGENT_ACP_MODULES[i];
38184
38414
  try {
@@ -38211,6 +38441,7 @@ function createSendLocalSkillsReport(getWs, logFn) {
38211
38441
  }
38212
38442
  function createReportAutoDetectedAgents(getWs, logFn) {
38213
38443
  return async () => {
38444
+ if (isCliImmediateShutdownRequested()) return;
38214
38445
  try {
38215
38446
  const types = await detectLocalAgentTypes();
38216
38447
  const socket = getWs();
@@ -38310,6 +38541,7 @@ var handleBridgeIdentified = (msg, deps) => {
38310
38541
  });
38311
38542
  setImmediate(() => {
38312
38543
  void (async () => {
38544
+ if (isCliImmediateShutdownRequested()) return;
38313
38545
  try {
38314
38546
  await deps.reportAutoDetectedAgents?.();
38315
38547
  } catch (e) {
@@ -38317,6 +38549,7 @@ var handleBridgeIdentified = (msg, deps) => {
38317
38549
  `[Bridge service] Auto-detect agents failed: ${e instanceof Error ? e.message : String(e)}`
38318
38550
  );
38319
38551
  }
38552
+ if (isCliImmediateShutdownRequested()) return;
38320
38553
  try {
38321
38554
  await deps.warmupAgentCapabilitiesOnConnect?.();
38322
38555
  } catch (e) {
@@ -38327,6 +38560,7 @@ var handleBridgeIdentified = (msg, deps) => {
38327
38560
  })();
38328
38561
  });
38329
38562
  setImmediate(() => {
38563
+ if (isCliImmediateShutdownRequested()) return;
38330
38564
  try {
38331
38565
  deps.sendLocalSkillsReport?.();
38332
38566
  } catch (e) {
@@ -38628,12 +38862,12 @@ function createBridgePromptSenders(deps, getWs) {
38628
38862
  }
38629
38863
 
38630
38864
  // src/agents/acp/from-bridge/bridge-prompt-preamble.ts
38631
- import { execFile as execFile10 } from "node:child_process";
38632
- import { promisify as promisify10 } from "node:util";
38633
- var execFileAsync9 = promisify10(execFile10);
38865
+ import { execFile as execFile8 } from "node:child_process";
38866
+ import { promisify as promisify8 } from "node:util";
38867
+ var execFileAsync7 = promisify8(execFile8);
38634
38868
  async function readGitBranch(cwd) {
38635
38869
  try {
38636
- const { stdout } = await execFileAsync9("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
38870
+ const { stdout } = await execFileAsync7("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
38637
38871
  const b = stdout.trim();
38638
38872
  return b || null;
38639
38873
  } catch {
@@ -38881,7 +39115,7 @@ var handleSessionRequestResponseMessage = (msg, deps) => {
38881
39115
  };
38882
39116
 
38883
39117
  // src/skills/preview.ts
38884
- import { spawn as spawn9 } from "node:child_process";
39118
+ import { spawn as spawn10 } from "node:child_process";
38885
39119
  var PREVIEW_API_BASE_PATH = "/__preview";
38886
39120
  var PREVIEW_SECRET_HEADER = "X-Preview-Secret";
38887
39121
  var DEFAULT_PORT = 3e3;
@@ -38960,7 +39194,7 @@ var previewSkill = {
38960
39194
  const parts = command.split(/\s+/);
38961
39195
  const exe = parts[0];
38962
39196
  const args = parts.slice(1);
38963
- previewProcess = spawn9(isWindows && exe === "npm" ? "npm.cmd" : exe, args, {
39197
+ previewProcess = spawn10(isWindows && exe === "npm" ? "npm.cmd" : exe, args, {
38964
39198
  cwd: getBridgeRoot(),
38965
39199
  stdio: ["ignore", "pipe", "pipe"],
38966
39200
  env: {
@@ -39517,7 +39751,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39517
39751
  reply({
39518
39752
  ok: true,
39519
39753
  hasUncommittedChanges: r.hasUncommittedChanges,
39520
- hasUnpushedCommits: r.hasUnpushedCommits
39754
+ hasUnpushedCommits: r.hasUnpushedCommits,
39755
+ uncommittedFileCount: r.uncommittedFileCount
39521
39756
  });
39522
39757
  return;
39523
39758
  }
@@ -39531,7 +39766,12 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39531
39766
  return;
39532
39767
  }
39533
39768
  }
39534
- const opts = repoRel && view === "commit" && commitSha ? { repoRelPath: repoRel, basis: { kind: "commit", sha: commitSha } } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" } } : void 0;
39769
+ const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
39770
+ const opts = repoRel && view === "commit" && commitSha ? {
39771
+ repoRelPath: repoRel,
39772
+ basis: { kind: "commit", sha: commitSha },
39773
+ recentCommitsLimit
39774
+ } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
39535
39775
  const repos = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts);
39536
39776
  reply({
39537
39777
  ok: true,
@@ -39549,7 +39789,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39549
39789
  reply({
39550
39790
  ok: true,
39551
39791
  hasUncommittedChanges: st2.hasUncommittedChanges,
39552
- hasUnpushedCommits: st2.hasUnpushedCommits
39792
+ hasUnpushedCommits: st2.hasUnpushedCommits,
39793
+ uncommittedFileCount: st2.uncommittedFileCount
39553
39794
  });
39554
39795
  return;
39555
39796
  }
@@ -39574,7 +39815,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39574
39815
  reply({
39575
39816
  ok: true,
39576
39817
  hasUncommittedChanges: st.hasUncommittedChanges,
39577
- hasUnpushedCommits: st.hasUnpushedCommits
39818
+ hasUnpushedCommits: st.hasUnpushedCommits,
39819
+ uncommittedFileCount: st.uncommittedFileCount
39578
39820
  });
39579
39821
  } catch (e) {
39580
39822
  reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -40099,6 +40341,7 @@ import * as path40 from "node:path";
40099
40341
  import * as path39 from "node:path";
40100
40342
  async function probeOneAgentTypeForCapabilities(params) {
40101
40343
  const { agentType, cwd, workspaceId, log: log2, reportAgentCapabilities, bridgeReport = true } = params;
40344
+ if (isCliImmediateShutdownRequested()) return false;
40102
40345
  const resolved = resolveAgentCommand(agentType);
40103
40346
  if (!resolved) return false;
40104
40347
  let sqliteChanged = false;
@@ -40132,6 +40375,7 @@ async function probeOneAgentTypeForCapabilities(params) {
40132
40375
  }, 28e3);
40133
40376
  killTimer.unref?.();
40134
40377
  try {
40378
+ if (isCliImmediateShutdownRequested()) return false;
40135
40379
  handle = await resolved.createClient({
40136
40380
  command: resolved.command,
40137
40381
  cwd: path39.resolve(cwd),
@@ -40151,7 +40395,7 @@ async function probeOneAgentTypeForCapabilities(params) {
40151
40395
  onSessionUpdate: () => {
40152
40396
  }
40153
40397
  });
40154
- await new Promise((r) => setTimeout(r, 1200));
40398
+ if (!await delayMsUnlessShutdownRequested(1200)) return false;
40155
40399
  } catch (e) {
40156
40400
  log2(
40157
40401
  `[Bridge service] Agent capability probe (${agentType}): ${e instanceof Error ? e.message : String(e)}`
@@ -40179,6 +40423,7 @@ async function probeAgentCapabilitiesForDetectedTypes(params) {
40179
40423
  } = params;
40180
40424
  let changedCount = 0;
40181
40425
  for (let i = 0; i < agentTypes.length; i++) {
40426
+ if (isCliImmediateShutdownRequested()) return changedCount;
40182
40427
  if (i > 0) await yieldToEventLoop();
40183
40428
  const agentType = agentTypes[i];
40184
40429
  if (!agentType.trim()) continue;
@@ -40206,6 +40451,7 @@ async function probeAgentCapabilitiesForDetectedTypes(params) {
40206
40451
  // src/agents/capabilities/warmup-agent-capabilities-on-connect.ts
40207
40452
  async function warmupAgentCapabilitiesOnConnect(params) {
40208
40453
  const { workspaceId, log: log2, getWs } = params;
40454
+ if (isCliImmediateShutdownRequested()) return;
40209
40455
  const cwd = path40.resolve(getBridgeRoot());
40210
40456
  async function sendBatchFromCache() {
40211
40457
  const socket = getWs();
@@ -40218,18 +40464,21 @@ async function warmupAgentCapabilitiesOnConnect(params) {
40218
40464
  items: rows.map((r) => ({ agentType: r.agentType, configOptions: r.configOptions }))
40219
40465
  });
40220
40466
  } catch (e) {
40467
+ if (e instanceof CliSqliteInterrupted) return;
40221
40468
  log2(
40222
40469
  `[Bridge service] Agent capability batch to bridge failed: ${e instanceof Error ? e.message : String(e)}`
40223
40470
  );
40224
40471
  }
40225
40472
  }
40226
40473
  await sendBatchFromCache();
40474
+ if (isCliImmediateShutdownRequested()) return;
40227
40475
  let types = [];
40228
40476
  try {
40229
40477
  types = [...await detectLocalAgentTypes()];
40230
40478
  } catch (e) {
40231
40479
  log2(`[Bridge service] detectLocalAgentTypes failed: ${e instanceof Error ? e.message : String(e)}`);
40232
40480
  }
40481
+ if (isCliImmediateShutdownRequested()) return;
40233
40482
  try {
40234
40483
  const n = await probeAgentCapabilitiesForDetectedTypes({
40235
40484
  agentTypes: types,
@@ -40241,11 +40490,13 @@ async function warmupAgentCapabilitiesOnConnect(params) {
40241
40490
  });
40242
40491
  if (n > 0) await sendBatchFromCache();
40243
40492
  } catch (e) {
40493
+ if (e instanceof CliSqliteInterrupted) return;
40244
40494
  log2(`[Bridge service] Agent capability probe (missing cache) failed: ${e instanceof Error ? e.message : String(e)}`);
40245
40495
  }
40246
40496
  void (async () => {
40247
40497
  try {
40248
40498
  await yieldToEventLoop();
40499
+ if (isCliImmediateShutdownRequested()) return;
40249
40500
  const n = await probeAgentCapabilitiesForDetectedTypes({
40250
40501
  agentTypes: types,
40251
40502
  cwd,
@@ -40256,6 +40507,7 @@ async function warmupAgentCapabilitiesOnConnect(params) {
40256
40507
  });
40257
40508
  if (n > 0) await sendBatchFromCache();
40258
40509
  } catch (e) {
40510
+ if (e instanceof CliSqliteInterrupted) return;
40259
40511
  log2(`[Bridge service] Agent capability lazy refresh failed: ${e instanceof Error ? e.message : String(e)}`);
40260
40512
  }
40261
40513
  })();
@@ -40303,7 +40555,8 @@ async function createBridgeConnection(options) {
40303
40555
  configOptions: info.configOptions
40304
40556
  })
40305
40557
  );
40306
- } catch {
40558
+ } catch (e) {
40559
+ if (e instanceof CliSqliteInterrupted) return;
40307
40560
  }
40308
40561
  if (!changed) return;
40309
40562
  const socket = getWs();
@@ -40386,6 +40639,7 @@ async function createBridgeConnection(options) {
40386
40639
  const stopFileIndexWatcher = startFileIndexWatcher(getBridgeRoot());
40387
40640
  return {
40388
40641
  close: async () => {
40642
+ requestCliImmediateShutdown();
40389
40643
  stopFileIndexWatcher();
40390
40644
  bridgeHeartbeat.stop();
40391
40645
  await closeBridgeConnection(state, acpManager, devServerManager, logFn);
@@ -40459,47 +40713,61 @@ async function runConnectedBridge(options, restartWithoutAuth) {
40459
40713
  } = options;
40460
40714
  const firehoseServerUrl = options.firehoseServerUrl ?? options.proxyServerUrl;
40461
40715
  let cleanupKeyCommand;
40462
- const handle = await createBridgeConnection({
40463
- apiUrl,
40464
- workspaceId,
40465
- authToken,
40466
- refreshToken,
40467
- firehoseServerUrl,
40468
- justAuthenticated,
40469
- worktreesRootPath,
40470
- e2eCertificate,
40471
- log,
40472
- persistTokens: (t) => {
40473
- writeConfigForApi(apiUrl, {
40474
- workspaceId,
40475
- token: t.token,
40476
- refreshToken: t.refreshToken
40477
- });
40478
- },
40479
- onAuthInvalid: () => {
40480
- cleanupKeyCommand?.();
40481
- log("[Bridge service] Access token invalid or revoked; re-authenticating\u2026");
40482
- clearConfigForApi(apiUrl);
40483
- void handle.close().then(() => {
40484
- void restartWithoutAuth({ apiUrl, firehoseServerUrl, worktreesRootPath, e2eCertificate });
40485
- });
40486
- }
40487
- });
40716
+ let bridgeClose = null;
40488
40717
  const onSignal = (kind) => {
40718
+ requestCliImmediateShutdown();
40719
+ abortActiveGitChildProcesses();
40489
40720
  cleanupKeyCommand?.();
40490
40721
  logImmediate(
40491
40722
  kind === "interrupt" ? "Keyboard interrupt (Ctrl+C) \u2014 stopping\u2026" : "Stop requested \u2014 shutting down\u2026"
40492
40723
  );
40493
- setImmediate(() => {
40494
- void handle.close().then(() => {
40495
- process.exit(0);
40496
- });
40497
- });
40724
+ if (bridgeClose) {
40725
+ void bridgeClose().then(() => process.exit(0));
40726
+ return;
40727
+ }
40728
+ process.exit(0);
40498
40729
  };
40499
40730
  const onSigInt = () => onSignal("interrupt");
40500
40731
  const onSigTerm = () => onSignal("stop");
40501
40732
  process.on("SIGINT", onSigInt);
40502
40733
  process.on("SIGTERM", onSigTerm);
40734
+ let handle;
40735
+ try {
40736
+ handle = await createBridgeConnection({
40737
+ apiUrl,
40738
+ workspaceId,
40739
+ authToken,
40740
+ refreshToken,
40741
+ firehoseServerUrl,
40742
+ justAuthenticated,
40743
+ worktreesRootPath,
40744
+ e2eCertificate,
40745
+ log,
40746
+ persistTokens: (t) => {
40747
+ writeConfigForApi(apiUrl, {
40748
+ workspaceId,
40749
+ token: t.token,
40750
+ refreshToken: t.refreshToken
40751
+ });
40752
+ },
40753
+ onAuthInvalid: () => {
40754
+ cleanupKeyCommand?.();
40755
+ log("[Bridge service] Access token invalid or revoked; re-authenticating\u2026");
40756
+ clearConfigForApi(apiUrl);
40757
+ void handle.close().then(() => {
40758
+ void restartWithoutAuth({ apiUrl, firehoseServerUrl, worktreesRootPath, e2eCertificate });
40759
+ });
40760
+ }
40761
+ });
40762
+ } catch (e) {
40763
+ process.off("SIGINT", onSigInt);
40764
+ process.off("SIGTERM", onSigTerm);
40765
+ if (e instanceof CliSqliteInterrupted) {
40766
+ process.exit(0);
40767
+ }
40768
+ throw e;
40769
+ }
40770
+ bridgeClose = () => handle.close();
40503
40771
  if (e2eCertificate) {
40504
40772
  let openingCertificate = false;
40505
40773
  cleanupKeyCommand = installE2eCertificateKeyCommand({
@@ -40544,6 +40812,7 @@ async function runBridge(options) {
40544
40812
  }
40545
40813
  });
40546
40814
  const onSignal = (kind) => {
40815
+ requestCliImmediateShutdown();
40547
40816
  logImmediate(
40548
40817
  kind === "interrupt" ? "Keyboard interrupt (Ctrl+C) \u2014 stopping\u2026" : "Stop requested \u2014 shutting down\u2026"
40549
40818
  );