@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/index.js CHANGED
@@ -23962,7 +23962,7 @@ function installBridgeProcessResilience() {
23962
23962
  }
23963
23963
 
23964
23964
  // src/cli-version.ts
23965
- var CLI_VERSION = "0.1.32".length > 0 ? "0.1.32" : "0.0.0-dev";
23965
+ var CLI_VERSION = "0.1.34".length > 0 ? "0.1.34" : "0.0.0-dev";
23966
23966
 
23967
23967
  // src/connection/heartbeat/constants.ts
23968
23968
  var BRIDGE_APP_HEARTBEAT_INTERVAL_MS = 1e4;
@@ -25022,6 +25022,30 @@ function runPendingAuth(options) {
25022
25022
  };
25023
25023
  }
25024
25024
 
25025
+ // src/dev-servers/manager/dev-server-constants.ts
25026
+ var BRIDGE_CLOSE_DEV_SERVER_GRACE_MS = 0;
25027
+ var BRIDGE_SHUTDOWN_GRACE_MS = 8e3;
25028
+
25029
+ // src/runtime/cli-process-interrupt.ts
25030
+ var cliImmediateShutdownRequested = false;
25031
+ function requestCliImmediateShutdown() {
25032
+ cliImmediateShutdownRequested = true;
25033
+ }
25034
+ function isCliImmediateShutdownRequested() {
25035
+ return cliImmediateShutdownRequested;
25036
+ }
25037
+ async function delayMsUnlessShutdownRequested(ms) {
25038
+ if (ms <= 0) return true;
25039
+ const end = Date.now() + ms;
25040
+ while (Date.now() < end) {
25041
+ if (isCliImmediateShutdownRequested()) return false;
25042
+ const chunk = Math.min(50, end - Date.now());
25043
+ if (chunk <= 0) break;
25044
+ await new Promise((r) => setTimeout(r, chunk));
25045
+ }
25046
+ return !isCliImmediateShutdownRequested();
25047
+ }
25048
+
25025
25049
  // src/sqlite/cli-database.ts
25026
25050
  import sqliteWasm from "node-sqlite3-wasm";
25027
25051
 
@@ -25325,6 +25349,12 @@ var { Database: SqliteDatabase } = sqliteWasm;
25325
25349
  var CLI_SQLITE_SYNC_RETRY_MAX = 40;
25326
25350
  var CLI_SQLITE_ASYNC_RETRY_MAX = 60;
25327
25351
  var CLI_SQLITE_ASYNC_BASE_DELAY_MS = 20;
25352
+ var CliSqliteInterrupted = class extends Error {
25353
+ name = "CliSqliteInterrupted";
25354
+ constructor() {
25355
+ super("CLI SQLite interrupted (shutdown)");
25356
+ }
25357
+ };
25328
25358
  function applyCliSqliteConcurrencyPragmas(db) {
25329
25359
  try {
25330
25360
  db.exec("PRAGMA journal_mode = WAL");
@@ -25335,7 +25365,7 @@ function applyCliSqliteConcurrencyPragmas(db) {
25335
25365
  } catch {
25336
25366
  }
25337
25367
  try {
25338
- db.run("PRAGMA busy_timeout = 8000");
25368
+ db.run("PRAGMA busy_timeout = 500");
25339
25369
  } catch {
25340
25370
  }
25341
25371
  }
@@ -25356,26 +25386,42 @@ function safeCloseCliSqliteDatabase(db) {
25356
25386
  function closeAllCliSqliteConnections() {
25357
25387
  }
25358
25388
  function isCliSqliteLockError(e) {
25389
+ if (e instanceof CliSqliteInterrupted) return false;
25359
25390
  const msg = e instanceof Error ? e.message : String(e);
25360
25391
  const lower = msg.toLowerCase();
25361
25392
  return lower.includes("database is locked") || lower.includes("sqlite_busy") || lower.includes("sqlite3_busy") || lower.includes("database") && lower.includes("locked");
25362
25393
  }
25363
25394
  function syncSleepMs(ms) {
25364
25395
  if (ms <= 0) return;
25365
- try {
25366
- const sab = new SharedArrayBuffer(4);
25367
- const ia = new Int32Array(sab);
25368
- Atomics.wait(ia, 0, 0, Math.min(ms, 1e4));
25369
- } catch {
25370
- const end = Date.now() + ms;
25371
- while (Date.now() < end) {
25396
+ const deadline = Date.now() + ms;
25397
+ while (Date.now() < deadline) {
25398
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
25399
+ const chunk = Math.min(80, deadline - Date.now());
25400
+ if (chunk <= 0) break;
25401
+ try {
25402
+ const sab = new SharedArrayBuffer(4);
25403
+ const ia = new Int32Array(sab);
25404
+ Atomics.wait(ia, 0, 0, chunk);
25405
+ } catch {
25406
+ const end = Date.now() + chunk;
25407
+ while (Date.now() < end) {
25408
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
25409
+ }
25372
25410
  }
25373
25411
  }
25374
25412
  }
25375
- function asyncDelayMs(ms) {
25376
- return new Promise((resolve18) => setTimeout(resolve18, ms));
25413
+ async function asyncDelayMsInterruptible(ms) {
25414
+ const end = Date.now() + ms;
25415
+ while (Date.now() < end) {
25416
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
25417
+ const chunk = Math.min(50, end - Date.now());
25418
+ if (chunk <= 0) break;
25419
+ await new Promise((r) => setTimeout(r, chunk));
25420
+ await yieldToEventLoop();
25421
+ }
25377
25422
  }
25378
25423
  function openCliSqliteConnection(options) {
25424
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
25379
25425
  const sqlitePath = getCliSqlitePath();
25380
25426
  ensureCliSqliteParentDir(sqlitePath);
25381
25427
  const db = new SqliteDatabase(sqlitePath);
@@ -25392,6 +25438,7 @@ function openCliSqliteConnection(options) {
25392
25438
  }
25393
25439
  function withCliSqliteSync(fn, options) {
25394
25440
  for (let attempt = 1; attempt <= CLI_SQLITE_SYNC_RETRY_MAX; attempt++) {
25441
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
25395
25442
  let db;
25396
25443
  try {
25397
25444
  db = openCliSqliteConnection(options);
@@ -25402,6 +25449,7 @@ function withCliSqliteSync(fn, options) {
25402
25449
  }
25403
25450
  } catch (e) {
25404
25451
  safeCloseCliSqliteDatabase(db);
25452
+ if (e instanceof CliSqliteInterrupted) throw e;
25405
25453
  if (!isCliSqliteLockError(e) || attempt === CLI_SQLITE_SYNC_RETRY_MAX) throw e;
25406
25454
  syncSleepMs(Math.min(500, 12 * attempt));
25407
25455
  }
@@ -25411,6 +25459,7 @@ function withCliSqliteSync(fn, options) {
25411
25459
  async function withCliSqlite(fn, options) {
25412
25460
  let lastError;
25413
25461
  for (let attempt = 1; attempt <= CLI_SQLITE_ASYNC_RETRY_MAX; attempt++) {
25462
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
25414
25463
  let db;
25415
25464
  try {
25416
25465
  db = openCliSqliteConnection(options);
@@ -25422,9 +25471,10 @@ async function withCliSqlite(fn, options) {
25422
25471
  } catch (e) {
25423
25472
  lastError = e;
25424
25473
  safeCloseCliSqliteDatabase(db);
25474
+ if (e instanceof CliSqliteInterrupted) throw e;
25425
25475
  if (!isCliSqliteLockError(e) || attempt === CLI_SQLITE_ASYNC_RETRY_MAX) throw e;
25426
25476
  const delayMs = Math.min(600, CLI_SQLITE_ASYNC_BASE_DELAY_MS * attempt);
25427
- await asyncDelayMs(delayMs);
25477
+ await asyncDelayMsInterruptible(delayMs);
25428
25478
  await yieldToEventLoop();
25429
25479
  }
25430
25480
  }
@@ -25437,9 +25487,9 @@ async function ensureCliSqliteInitialized(options) {
25437
25487
 
25438
25488
  // src/connection/close-bridge-connection.ts
25439
25489
  async function closeBridgeConnection(state, acpManager, devServerManager, log2) {
25490
+ requestCliImmediateShutdown();
25440
25491
  const say = log2 ?? logImmediate;
25441
25492
  say("Cleaning up connections\u2026");
25442
- await new Promise((resolve18) => setImmediate(resolve18));
25443
25493
  state.closedByUser = true;
25444
25494
  clearReconnectQuietTimer(state.mainQuiet);
25445
25495
  clearReconnectQuietTimer(state.firehoseQuiet);
@@ -25476,7 +25526,7 @@ async function closeBridgeConnection(state, acpManager, devServerManager, log2)
25476
25526
  }
25477
25527
  if (devServerManager) {
25478
25528
  say("Stopping local dev server processes\u2026");
25479
- await devServerManager.shutdownAllGraceful();
25529
+ await devServerManager.shutdownAllGraceful({ graceMs: BRIDGE_CLOSE_DEV_SERVER_GRACE_MS });
25480
25530
  }
25481
25531
  try {
25482
25532
  closeAllCliSqliteConnections();
@@ -25500,17 +25550,13 @@ function resolveSessionParentPathForAgentProcess(resolvedSessionParentPath) {
25500
25550
  return getBridgeRoot();
25501
25551
  }
25502
25552
 
25503
- // src/git/session-git-queue.ts
25504
- import { execFile as execFile7 } from "node:child_process";
25553
+ // src/git/snapshot/session-git-queue.ts
25505
25554
  import { readFile, stat } from "node:fs/promises";
25506
- import { promisify as promisify7 } from "node:util";
25507
25555
  import * as path15 from "node:path";
25508
25556
 
25509
- // src/git/pre-turn-snapshot.ts
25557
+ // src/git/snapshot/pre-turn-snapshot.ts
25510
25558
  import * as fs14 from "node:fs";
25511
25559
  import * as path14 from "node:path";
25512
- import { execFile as execFile6 } from "node:child_process";
25513
- import { promisify as promisify6 } from "node:util";
25514
25560
 
25515
25561
  // src/git/discover-repos.ts
25516
25562
  import * as fs13 from "node:fs";
@@ -30075,12 +30121,68 @@ function gitInstanceFactory(baseDir, options) {
30075
30121
  init_git_response_error();
30076
30122
  var simpleGit = gitInstanceFactory;
30077
30123
 
30124
+ // src/git/git-runtime.ts
30125
+ var activeGitChildProcesses = /* @__PURE__ */ new Set();
30126
+ function abortActiveGitChildProcesses() {
30127
+ for (const child of activeGitChildProcesses) {
30128
+ try {
30129
+ child.kill("SIGTERM");
30130
+ } catch {
30131
+ }
30132
+ }
30133
+ }
30134
+ var GitOperationAbortedError = class extends Error {
30135
+ constructor(message = "Git operation aborted") {
30136
+ super(message);
30137
+ this.name = "GitOperationAbortedError";
30138
+ }
30139
+ };
30140
+ function throwIfGitShutdownRequested() {
30141
+ if (isCliImmediateShutdownRequested()) {
30142
+ abortActiveGitChildProcesses();
30143
+ throw new GitOperationAbortedError();
30144
+ }
30145
+ }
30146
+ async function runGitTask(fn) {
30147
+ throwIfGitShutdownRequested();
30148
+ try {
30149
+ const result = await fn();
30150
+ throwIfGitShutdownRequested();
30151
+ return result;
30152
+ } catch (e) {
30153
+ if (isCliImmediateShutdownRequested()) {
30154
+ throw new GitOperationAbortedError();
30155
+ }
30156
+ throw e;
30157
+ }
30158
+ }
30159
+ async function yieldToEventLoop2() {
30160
+ throwIfGitShutdownRequested();
30161
+ await new Promise((resolve18) => {
30162
+ setImmediate(resolve18);
30163
+ });
30164
+ throwIfGitShutdownRequested();
30165
+ }
30166
+ async function forEachWithGitYield(items, fn) {
30167
+ for (let i = 0; i < items.length; i++) {
30168
+ if (i > 0 || items.length > 1) await yieldToEventLoop2();
30169
+ await fn(items[i], i);
30170
+ }
30171
+ }
30172
+
30078
30173
  // src/git/cli-simple-git.ts
30079
30174
  function cliSimpleGit(baseDir) {
30080
- const git = simpleGit({ baseDir });
30175
+ const git = simpleGit({
30176
+ baseDir,
30177
+ trimmed: true,
30178
+ spawnOptions: {}
30179
+ });
30081
30180
  git.outputHandler((command, stdout, stderr) => {
30082
30181
  const trace = isCliTrace();
30083
30182
  const onChunk = (label) => (chunk) => {
30183
+ if (isCliImmediateShutdownRequested()) {
30184
+ abortActiveGitChildProcesses();
30185
+ }
30084
30186
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
30085
30187
  const line = text.replace(/\s+$/, "");
30086
30188
  if (trace && line) {
@@ -30169,17 +30271,37 @@ async function discoverGitReposUnderRoot(rootPath) {
30169
30271
  return out;
30170
30272
  }
30171
30273
 
30172
- // src/git/pre-turn-snapshot.ts
30274
+ // src/git/git-exec.ts
30275
+ import { execFile as execFile6, spawn as spawn4 } from "node:child_process";
30276
+ import { promisify as promisify6 } from "node:util";
30173
30277
  var execFileAsync5 = promisify6(execFile6);
30278
+ async function execGitFile(args, options) {
30279
+ throwIfGitShutdownRequested();
30280
+ try {
30281
+ const result = await execFileAsync5("git", args, {
30282
+ maxBuffer: 10 * 1024 * 1024,
30283
+ ...options
30284
+ });
30285
+ throwIfGitShutdownRequested();
30286
+ return {
30287
+ stdout: String(result.stdout ?? ""),
30288
+ stderr: String(result.stderr ?? "")
30289
+ };
30290
+ } catch (e) {
30291
+ if (isCliImmediateShutdownRequested()) {
30292
+ throw new GitOperationAbortedError();
30293
+ }
30294
+ throw e;
30295
+ }
30296
+ }
30297
+
30298
+ // src/git/snapshot/pre-turn-snapshot.ts
30174
30299
  function snapshotsDirForCwd(agentCwd) {
30175
30300
  return path14.join(agentCwd, ".buildautomaton", "snapshots");
30176
30301
  }
30177
30302
  async function gitStashCreate(repoRoot, log2) {
30178
30303
  try {
30179
- const { stdout } = await execFileAsync5("git", ["stash", "create"], {
30180
- cwd: repoRoot,
30181
- maxBuffer: 10 * 1024 * 1024
30182
- });
30304
+ const { stdout } = await execGitFile(["stash", "create"], { cwd: repoRoot });
30183
30305
  return stdout.trim();
30184
30306
  } catch (e) {
30185
30307
  log2(
@@ -30190,7 +30312,7 @@ async function gitStashCreate(repoRoot, log2) {
30190
30312
  }
30191
30313
  async function gitRun(repoRoot, args, log2, label) {
30192
30314
  try {
30193
- await execFileAsync5("git", args, { cwd: repoRoot, maxBuffer: 10 * 1024 * 1024 });
30315
+ await execGitFile(args, { cwd: repoRoot });
30194
30316
  return { ok: true };
30195
30317
  } catch (e) {
30196
30318
  const msg = e instanceof Error ? e.message : String(e);
@@ -30224,10 +30346,10 @@ async function capturePreTurnSnapshot(options) {
30224
30346
  return { ok: false, error: "No git repos to snapshot" };
30225
30347
  }
30226
30348
  const repos = [];
30227
- for (const root of repoRoots) {
30349
+ await forEachWithGitYield(repoRoots, async (root) => {
30228
30350
  const stashSha = await gitStashCreate(root, log2);
30229
30351
  repos.push({ path: root, stashSha });
30230
- }
30352
+ });
30231
30353
  const dir = snapshotsDirForCwd(agentCwd);
30232
30354
  try {
30233
30355
  fs14.mkdirSync(dir, { recursive: true });
@@ -30262,17 +30384,25 @@ async function applyPreTurnSnapshot(filePath, log2) {
30262
30384
  if (!Array.isArray(data.repos)) {
30263
30385
  return { ok: false, error: "Invalid snapshot file" };
30264
30386
  }
30265
- for (const r of data.repos) {
30266
- if (!r.path) continue;
30387
+ let applyError = null;
30388
+ await forEachWithGitYield(data.repos, async (r) => {
30389
+ if (applyError || !r.path) return;
30267
30390
  const reset = await gitRun(r.path, ["reset", "--hard", "HEAD"], log2, "reset --hard");
30268
- if (!reset.ok) return reset;
30391
+ if (!reset.ok) {
30392
+ applyError = reset;
30393
+ return;
30394
+ }
30269
30395
  const clean = await gitRun(r.path, ["clean", "-fd"], log2, "clean -fd");
30270
- if (!clean.ok) return clean;
30396
+ if (!clean.ok) {
30397
+ applyError = clean;
30398
+ return;
30399
+ }
30271
30400
  if (r.stashSha) {
30272
30401
  const ap = await gitRun(r.path, ["stash", "apply", r.stashSha], log2, "stash apply");
30273
- if (!ap.ok) return ap;
30402
+ if (!ap.ok) applyError = ap;
30274
30403
  }
30275
- }
30404
+ });
30405
+ if (applyError?.ok === false) return applyError;
30276
30406
  log2(`[snapshot] Restored pre-turn state for ${data.runId.slice(0, 8)}\u2026`);
30277
30407
  return { ok: true };
30278
30408
  }
@@ -30280,8 +30410,7 @@ function snapshotFilePath(agentCwd, runId) {
30280
30410
  return path14.join(snapshotsDirForCwd(agentCwd), `${runId}.json`);
30281
30411
  }
30282
30412
 
30283
- // src/git/session-git-queue.ts
30284
- var execFileAsync6 = promisify7(execFile7);
30413
+ // src/git/snapshot/session-git-queue.ts
30285
30414
  var MAX_FULL_FILE_TEXT_BYTES = 512 * 1024;
30286
30415
  async function readWorkspaceFileAsUtf8(absPath) {
30287
30416
  try {
@@ -30310,35 +30439,28 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
30310
30439
  return;
30311
30440
  }
30312
30441
  const multiRepo = data.repos.length > 1;
30313
- for (const repo of data.repos) {
30314
- if (!repo.stashSha) continue;
30442
+ await forEachWithGitYield(data.repos, async (repo) => {
30443
+ if (!repo.stashSha) return;
30315
30444
  let namesRaw;
30316
30445
  try {
30317
- const { stdout } = await execFileAsync6("git", ["diff", "--name-only", repo.stashSha], {
30318
- cwd: repo.path,
30319
- maxBuffer: 10 * 1024 * 1024
30320
- });
30446
+ const { stdout } = await execGitFile(["diff", "--name-only", repo.stashSha], { cwd: repo.path });
30321
30447
  namesRaw = stdout;
30322
30448
  } catch (e) {
30323
30449
  log2(
30324
30450
  `[session-git-queue] Git diff --name-only failed in ${repo.path}: ${e instanceof Error ? e.message : String(e)}`
30325
30451
  );
30326
- continue;
30452
+ return;
30327
30453
  }
30328
30454
  const lines = namesRaw.split("\n").map((l) => l.trim()).filter(Boolean);
30329
30455
  const slug = path15.basename(repo.path).replace(/[^\w.-]+/g, "_") || "repo";
30330
- for (const rel of lines) {
30331
- if (rel.includes("..")) continue;
30456
+ await forEachWithGitYield(lines, async (rel) => {
30457
+ if (rel.includes("..")) return;
30332
30458
  try {
30333
- const { stdout: patchContent } = await execFileAsync6(
30334
- "git",
30459
+ const { stdout: patchContent } = await execGitFile(
30335
30460
  ["diff", "--no-color", repo.stashSha, "--", rel],
30336
- {
30337
- cwd: repo.path,
30338
- maxBuffer: 50 * 1024 * 1024
30339
- }
30461
+ { cwd: repo.path }
30340
30462
  );
30341
- if (!patchContent.trim()) continue;
30463
+ if (!patchContent.trim()) return;
30342
30464
  const displayPath = multiRepo ? `${slug}/${rel}` : rel;
30343
30465
  const workspaceFilePath = path15.join(repo.path, rel);
30344
30466
  const newText = await readWorkspaceFileAsUtf8(workspaceFilePath);
@@ -30355,8 +30477,8 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
30355
30477
  `[session-git-queue] Git diff failed for ${rel}: ${e instanceof Error ? e.message : String(e)}`
30356
30478
  );
30357
30479
  }
30358
- }
30359
- }
30480
+ });
30481
+ });
30360
30482
  }
30361
30483
 
30362
30484
  // src/agents/acp/put-summarize-change-summaries.ts
@@ -30722,34 +30844,50 @@ __export(claude_code_acp_client_exports, {
30722
30844
  createClaudeCodeAcpClient: () => createClaudeCodeAcpClient,
30723
30845
  detectLocalAgentPresence: () => detectLocalAgentPresence
30724
30846
  });
30725
- import { execFile as execFile9 } from "node:child_process";
30726
- import { promisify as promisify9 } from "node:util";
30727
30847
 
30728
30848
  // src/agents/acp/clients/detect-command-on-path.ts
30729
- import { execFile as execFile8 } from "node:child_process";
30730
- import { promisify as promisify8 } from "node:util";
30731
- var execFileAsync7 = promisify8(execFile8);
30732
- async function isCommandOnPath(command, timeoutMs = 4e3) {
30849
+ import { execFile as execFile7 } from "node:child_process";
30850
+ import { promisify as promisify7 } from "node:util";
30851
+ var execFileAsync6 = promisify7(execFile7);
30852
+ var COMMAND_ON_PATH_PROBE_TIMEOUT_MS = 750;
30853
+ async function execFileShutdownAware(file2, args, timeoutMs) {
30854
+ if (isCliImmediateShutdownRequested()) throw new Error("shutdown");
30855
+ const ac = new AbortController();
30856
+ const shutdownPoll = setInterval(() => {
30857
+ if (isCliImmediateShutdownRequested()) ac.abort();
30858
+ }, 50);
30859
+ shutdownPoll.unref?.();
30860
+ try {
30861
+ await execFileAsync6(file2, args, { timeout: timeoutMs, signal: ac.signal });
30862
+ } finally {
30863
+ clearInterval(shutdownPoll);
30864
+ }
30865
+ }
30866
+ async function isCommandOnPath(command, timeoutMs = COMMAND_ON_PATH_PROBE_TIMEOUT_MS) {
30867
+ if (isCliImmediateShutdownRequested()) return false;
30733
30868
  try {
30734
- await execFileAsync7("which", [command], { timeout: timeoutMs });
30869
+ await execFileShutdownAware("which", [command], timeoutMs);
30735
30870
  return true;
30736
30871
  } catch {
30737
30872
  return false;
30738
30873
  }
30739
30874
  }
30740
-
30741
- // src/agents/acp/clients/claude-code-acp-client.ts
30742
- var execFileAsync8 = promisify9(execFile9);
30743
- var BACKEND_LOCAL_AGENT_TYPE = "claude-code";
30744
- async function detectLocalAgentPresence() {
30745
- if (await isCommandOnPath("claude")) return true;
30875
+ async function execProbeShutdownAware(file2, args, timeoutMs) {
30876
+ if (isCliImmediateShutdownRequested()) return false;
30746
30877
  try {
30747
- await execFileAsync8("npx", ["--yes", "@anthropic-ai/claude-code", "--version"], { timeout: 25e3 });
30878
+ await execFileShutdownAware(file2, args, timeoutMs);
30748
30879
  return true;
30749
30880
  } catch {
30750
30881
  return false;
30751
30882
  }
30752
30883
  }
30884
+
30885
+ // src/agents/acp/clients/claude-code-acp-client.ts
30886
+ var BACKEND_LOCAL_AGENT_TYPE = "claude-code";
30887
+ async function detectLocalAgentPresence() {
30888
+ if (await isCommandOnPath("claude")) return true;
30889
+ return execProbeShutdownAware("npx", ["--yes", "@anthropic-ai/claude-code", "--version"], 3e3);
30890
+ }
30753
30891
  function buildClaudeCodeAcpSpawnCommand(base, _sessionMode) {
30754
30892
  return [...base];
30755
30893
  }
@@ -30800,7 +30938,7 @@ __export(cursor_acp_client_exports, {
30800
30938
  createCursorAcpClient: () => createCursorAcpClient,
30801
30939
  detectLocalAgentPresence: () => detectLocalAgentPresence3
30802
30940
  });
30803
- import { spawn as spawn4 } from "node:child_process";
30941
+ import { spawn as spawn5 } from "node:child_process";
30804
30942
  import * as readline from "node:readline";
30805
30943
 
30806
30944
  // src/agents/acp/format-session-update-kind-for-log.ts
@@ -30870,7 +31008,7 @@ async function createCursorAcpClient(options) {
30870
31008
  } = options;
30871
31009
  const dbgFs = process.env.BUILDAUTOMATON_DEBUG_ACP_FS === "1";
30872
31010
  const isWindows = process.platform === "win32";
30873
- const child = spawn4(command[0], command.slice(1), {
31011
+ const child = spawn5(command[0], command.slice(1), {
30874
31012
  cwd,
30875
31013
  stdio: ["pipe", "pipe", "pipe"],
30876
31014
  env: process.env,
@@ -32316,7 +32454,7 @@ import os8 from "node:os";
32316
32454
  import * as fs17 from "node:fs";
32317
32455
  import * as path20 from "node:path";
32318
32456
 
32319
- // src/git/worktree-add.ts
32457
+ // src/git/worktrees/worktree-add.ts
32320
32458
  async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch) {
32321
32459
  const mainGit = cliSimpleGit(mainRepoPath);
32322
32460
  await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
@@ -32421,7 +32559,7 @@ async function prepareNewSessionWorktrees(options) {
32421
32559
  };
32422
32560
  }
32423
32561
 
32424
- // src/git/rename-branch.ts
32562
+ // src/git/branches/rename-branch.ts
32425
32563
  async function gitRenameCurrentBranch(repoDir, newName) {
32426
32564
  const g = cliSimpleGit(repoDir);
32427
32565
  await g.raw(["branch", "-m", newName]);
@@ -32445,10 +32583,10 @@ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
32445
32583
  // src/worktrees/remove-session-worktrees.ts
32446
32584
  import * as fs20 from "node:fs";
32447
32585
 
32448
- // src/git/worktree-remove.ts
32586
+ // src/git/worktrees/worktree-remove.ts
32449
32587
  import * as fs19 from "node:fs";
32450
32588
 
32451
- // src/git/resolve-main-repo-from-git-file.ts
32589
+ // src/git/worktrees/resolve-main-repo-from-git-file.ts
32452
32590
  import * as fs18 from "node:fs";
32453
32591
  import * as path21 from "node:path";
32454
32592
  function resolveMainRepoFromWorktreeGitFile(wt) {
@@ -32462,7 +32600,7 @@ function resolveMainRepoFromWorktreeGitFile(wt) {
32462
32600
  return path21.dirname(gitDir);
32463
32601
  }
32464
32602
 
32465
- // src/git/worktree-remove.ts
32603
+ // src/git/worktrees/worktree-remove.ts
32466
32604
  async function gitWorktreeRemoveForce(worktreePath) {
32467
32605
  const mainRepo = resolveMainRepoFromWorktreeGitFile(worktreePath);
32468
32606
  if (mainRepo) {
@@ -32488,7 +32626,88 @@ async function removeSessionWorktrees(paths, log2) {
32488
32626
  }
32489
32627
  }
32490
32628
 
32491
- // src/git/working-directory/status/working-tree-status.ts
32629
+ // src/git/changes/lib/parse-git-status.ts
32630
+ function parseNameStatusLines(lines) {
32631
+ const m = /* @__PURE__ */ new Map();
32632
+ for (const line of lines) {
32633
+ if (!line.trim()) continue;
32634
+ const tabParts = line.split(" ");
32635
+ if (tabParts.length < 2) continue;
32636
+ const status = tabParts[0].trim();
32637
+ const code = status[0];
32638
+ if (code === "A") {
32639
+ m.set(tabParts[tabParts.length - 1], "added");
32640
+ } else if (code === "D") {
32641
+ m.set(tabParts[tabParts.length - 1], "removed");
32642
+ } else if (code === "R" || code === "C") {
32643
+ if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
32644
+ } else if (code === "M" || code === "U" || code === "T") {
32645
+ m.set(tabParts[tabParts.length - 1], "modified");
32646
+ }
32647
+ }
32648
+ return m;
32649
+ }
32650
+ function parseNumstatFirstLine(line) {
32651
+ const parts = line.split(" ");
32652
+ if (parts.length < 3) return null;
32653
+ const [a, d] = parts;
32654
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32655
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32656
+ return { additions, deletions };
32657
+ }
32658
+ function parseNumstat(lines) {
32659
+ const m = /* @__PURE__ */ new Map();
32660
+ for (const line of lines) {
32661
+ if (!line.trim()) continue;
32662
+ const parts = line.split(" ");
32663
+ if (parts.length < 3) continue;
32664
+ const [a, d, p] = parts;
32665
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32666
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32667
+ m.set(p, { additions, deletions });
32668
+ }
32669
+ return m;
32670
+ }
32671
+ async function numstatFromGitNoIndex(g, pathInRepo) {
32672
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
32673
+ try {
32674
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
32675
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
32676
+ return parseNumstatFirstLine(first2);
32677
+ } catch {
32678
+ return null;
32679
+ }
32680
+ }
32681
+
32682
+ // src/git/changes/lib/working-tree-changed-path-count.ts
32683
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
32684
+ const kindByPath = parseNameStatusLines(nameStatusLines);
32685
+ const numByPath = parseNumstat(numstatLines);
32686
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
32687
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
32688
+ paths.add(p);
32689
+ }
32690
+ return paths.size;
32691
+ }
32692
+
32693
+ // src/git/changes/count-working-tree-changed-files.ts
32694
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
32695
+ return runGitTask(async () => {
32696
+ const g = cliSimpleGit(repoGitCwd);
32697
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
32698
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
32699
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
32700
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
32701
+ ]);
32702
+ return workingTreeChangedPathCount(
32703
+ String(nameStatusRaw).split("\n"),
32704
+ String(numstatRaw).split("\n"),
32705
+ String(untrackedRaw).split("\n")
32706
+ );
32707
+ });
32708
+ }
32709
+
32710
+ // src/git/commits/resolve-remote-tracking.ts
32492
32711
  async function tryConfigGet(g, key) {
32493
32712
  try {
32494
32713
  const out = await g.raw(["config", "--get", key]);
@@ -32571,30 +32790,6 @@ async function resolveBaseShaForUnpushedCommits(g) {
32571
32790
  if (!defaultRef) return null;
32572
32791
  return revParseSafe(g, defaultRef);
32573
32792
  }
32574
- function parseLogShaDateSubjectLines(raw) {
32575
- const out = [];
32576
- for (const line of String(raw).split("\n")) {
32577
- const l = line.trimEnd();
32578
- if (!l.trim()) continue;
32579
- const parts = l.split(" ");
32580
- if (parts.length < 3) continue;
32581
- const sha = parts[0].trim();
32582
- const committedAt = parts[1].trim();
32583
- const subject = parts.slice(2).join(" ").trim();
32584
- if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
32585
- out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
32586
- }
32587
- return out;
32588
- }
32589
- async function gitLogNotReachableFromBase(g, baseSha, headSha) {
32590
- if (baseSha === headSha) return [];
32591
- try {
32592
- const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
32593
- return parseLogShaDateSubjectLines(logOut);
32594
- } catch {
32595
- return [];
32596
- }
32597
- }
32598
32793
  async function commitsAheadOfRemoteTracking(repoDir) {
32599
32794
  const g = cliSimpleGit(repoDir);
32600
32795
  const headSha = await revParseSafe(g, "HEAD");
@@ -32609,44 +32804,41 @@ async function commitsAheadOfRemoteTracking(repoDir) {
32609
32804
  return 0;
32610
32805
  }
32611
32806
  }
32807
+
32808
+ // src/git/status/working-tree-status.ts
32612
32809
  async function getRepoWorkingTreeStatus(repoDir) {
32613
- const g = cliSimpleGit(repoDir);
32614
- const st = await g.status();
32615
- const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
32616
- const ahead = await commitsAheadOfRemoteTracking(repoDir);
32617
- return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0 };
32618
- }
32619
- async function listUnpushedCommits(repoDir) {
32620
- const g = cliSimpleGit(repoDir);
32621
- const headSha = await revParseSafe(g, "HEAD");
32622
- if (!headSha) return [];
32623
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
32624
- if (!baseSha) return [];
32625
- return gitLogNotReachableFromBase(g, baseSha, headSha);
32810
+ return runGitTask(async () => {
32811
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
32812
+ const hasUncommittedChanges = uncommittedFileCount > 0;
32813
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
32814
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
32815
+ });
32626
32816
  }
32627
32817
  async function aggregateSessionPathsWorkingTreeStatus(paths) {
32628
32818
  let hasUncommittedChanges = false;
32629
32819
  let hasUnpushedCommits = false;
32630
- for (const p of paths) {
32820
+ let uncommittedFileCount = 0;
32821
+ await forEachWithGitYield(paths, async (p) => {
32631
32822
  const s = await getRepoWorkingTreeStatus(p);
32823
+ uncommittedFileCount += s.uncommittedFileCount;
32632
32824
  if (s.hasUncommittedChanges) hasUncommittedChanges = true;
32633
32825
  if (s.hasUnpushedCommits) hasUnpushedCommits = true;
32634
- }
32635
- return { hasUncommittedChanges, hasUnpushedCommits };
32826
+ });
32827
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
32636
32828
  }
32637
32829
  async function pushAheadOfUpstreamForPaths(paths) {
32638
- for (const p of paths) {
32830
+ await forEachWithGitYield(paths, async (p) => {
32639
32831
  const g = cliSimpleGit(p);
32640
32832
  const ahead = await commitsAheadOfRemoteTracking(p);
32641
- if (ahead <= 0) continue;
32833
+ if (ahead <= 0) return;
32642
32834
  await g.push();
32643
- }
32835
+ });
32644
32836
  }
32645
32837
 
32646
- // src/git/working-directory/changes/types.ts
32838
+ // src/git/changes/types.ts
32647
32839
  var MAX_PATCH_CHARS = 35e4;
32648
32840
 
32649
- // src/git/working-directory/changes/repo-format.ts
32841
+ // src/git/changes/lib/repo-format.ts
32650
32842
  function posixJoinDirFile(dir, file2) {
32651
32843
  const d = dir === "." || dir === "" ? "" : dir.replace(/\\/g, "/").replace(/\/+$/, "");
32652
32844
  const f = file2.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -32700,63 +32892,80 @@ function formatRemoteDisplayLabel(remoteUrl) {
32700
32892
  return `origin \xB7 ${hostPath}`;
32701
32893
  }
32702
32894
 
32703
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
32895
+ // src/git/changes/get-working-tree-change-repo-details.ts
32704
32896
  import * as path23 from "node:path";
32705
32897
 
32706
- // src/git/working-directory/changes/parse-git-status.ts
32707
- function parseNameStatusLines(lines) {
32708
- const m = /* @__PURE__ */ new Map();
32709
- for (const line of lines) {
32710
- if (!line.trim()) continue;
32711
- const tabParts = line.split(" ");
32712
- if (tabParts.length < 2) continue;
32713
- const status = tabParts[0].trim();
32714
- const code = status[0];
32715
- if (code === "A") {
32716
- m.set(tabParts[tabParts.length - 1], "added");
32717
- } else if (code === "D") {
32718
- m.set(tabParts[tabParts.length - 1], "removed");
32719
- } else if (code === "R" || code === "C") {
32720
- if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
32721
- } else if (code === "M" || code === "U" || code === "T") {
32722
- m.set(tabParts[tabParts.length - 1], "modified");
32723
- }
32898
+ // src/git/commits/lib/parse-log-lines.ts
32899
+ function parseLogShaDateSubjectLines(raw) {
32900
+ const out = [];
32901
+ for (const line of String(raw).split("\n")) {
32902
+ const l = line.trimEnd();
32903
+ if (!l.trim()) continue;
32904
+ const parts = l.split(" ");
32905
+ if (parts.length < 3) continue;
32906
+ const sha = parts[0].trim();
32907
+ const committedAt = parts[1].trim();
32908
+ const subject = parts.slice(2).join(" ").trim();
32909
+ if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
32910
+ out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
32724
32911
  }
32725
- return m;
32726
- }
32727
- function parseNumstatFirstLine(line) {
32728
- const parts = line.split(" ");
32729
- if (parts.length < 3) return null;
32730
- const [a, d] = parts;
32731
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32732
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32733
- return { additions, deletions };
32912
+ return out;
32734
32913
  }
32735
- function parseNumstat(lines) {
32736
- const m = /* @__PURE__ */ new Map();
32737
- for (const line of lines) {
32738
- if (!line.trim()) continue;
32739
- const parts = line.split(" ");
32740
- if (parts.length < 3) continue;
32741
- const [a, d, p] = parts;
32742
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32743
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32744
- m.set(p, { additions, deletions });
32914
+
32915
+ // src/git/commits/list-unpushed-commits.ts
32916
+ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
32917
+ if (baseSha === headSha) return [];
32918
+ try {
32919
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
32920
+ return parseLogShaDateSubjectLines(logOut);
32921
+ } catch {
32922
+ return [];
32745
32923
  }
32746
- return m;
32747
32924
  }
32748
- async function numstatFromGitNoIndex(g, pathInRepo) {
32749
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
32925
+ async function listUnpushedCommits(repoDir) {
32926
+ const g = cliSimpleGit(repoDir);
32927
+ const headSha = await revParseSafe(g, "HEAD");
32928
+ if (!headSha) return [];
32929
+ const baseSha = await resolveBaseShaForUnpushedCommits(g);
32930
+ if (!baseSha) return [];
32931
+ return gitLogNotReachableFromBase(g, baseSha, headSha);
32932
+ }
32933
+
32934
+ // src/git/commits/lib/sanitize-recent-commits-limit.ts
32935
+ var RECENT_COMMITS_LIMIT_MIN = 1;
32936
+ var RECENT_COMMITS_LIMIT_MAX = 50;
32937
+ var RECENT_COMMITS_LIMIT_DEFAULT = 2;
32938
+ function sanitizeRecentCommitsLimit(value) {
32939
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : Number.NaN;
32940
+ if (!Number.isFinite(n)) return RECENT_COMMITS_LIMIT_DEFAULT;
32941
+ return Math.min(RECENT_COMMITS_LIMIT_MAX, Math.max(RECENT_COMMITS_LIMIT_MIN, Math.trunc(n)));
32942
+ }
32943
+
32944
+ // src/git/commits/list-recent-commits.ts
32945
+ async function listRecentCommits(repoDir, limitInput) {
32946
+ const limit = sanitizeRecentCommitsLimit(limitInput);
32947
+ const g = cliSimpleGit(repoDir);
32948
+ const headSha = await revParseSafe(g, "HEAD");
32949
+ if (!headSha) return { commits: [], hasMore: false };
32950
+ const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
32750
32951
  try {
32751
- const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
32752
- const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
32753
- return parseNumstatFirstLine(first2);
32952
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
32953
+ const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
32954
+ ...c,
32955
+ needsPush: unpushedSet.has(c.sha)
32956
+ }));
32957
+ let hasMore = false;
32958
+ if (commits.length === limit) {
32959
+ const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
32960
+ hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
32961
+ }
32962
+ return { commits, hasMore };
32754
32963
  } catch {
32755
- return null;
32964
+ return { commits: [], hasMore: false };
32756
32965
  }
32757
32966
  }
32758
32967
 
32759
- // src/git/working-directory/changes/patch-truncate.ts
32968
+ // src/git/changes/lib/patch-truncate.ts
32760
32969
  function truncatePatch(s) {
32761
32970
  if (s.length <= MAX_PATCH_CHARS) return s;
32762
32971
  return `${s.slice(0, MAX_PATCH_CHARS)}
@@ -32764,7 +32973,7 @@ function truncatePatch(s) {
32764
32973
  \u2026 (diff truncated)`;
32765
32974
  }
32766
32975
 
32767
- // src/git/working-directory/changes/list-changed-files-for-commit.ts
32976
+ // src/git/commits/list-changed-files-for-commit.ts
32768
32977
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32769
32978
  async function parentForCommitDiff(g, sha) {
32770
32979
  try {
@@ -32790,7 +32999,7 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32790
32999
  const paths = new Set([...kindByPath.keys(), ...numByPath.keys()].filter(Boolean));
32791
33000
  const rows = [];
32792
33001
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
32793
- for (const pathInRepo of paths) {
33002
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
32794
33003
  const relLauncher = posixJoinDirFile(normRel, pathInRepo.replace(/\\/g, "/"));
32795
33004
  const nums = numByPath.get(pathInRepo);
32796
33005
  let additions = nums?.additions ?? 0;
@@ -32802,8 +33011,8 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32802
33011
  else change = "modified";
32803
33012
  }
32804
33013
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
32805
- }
32806
- for (const row of rows) {
33014
+ });
33015
+ await forEachWithGitYield(rows, async (row) => {
32807
33016
  let pathInRepo;
32808
33017
  if (normRel === ".") {
32809
33018
  pathInRepo = row.pathRelLauncher;
@@ -32815,16 +33024,16 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32815
33024
  const raw = await g.raw(["diff", "-U20000", range, "--", pathInRepo]).catch(() => "");
32816
33025
  const t = String(raw).trim();
32817
33026
  row.patchContent = t ? truncatePatch(t) : void 0;
32818
- }
33027
+ });
32819
33028
  rows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
32820
33029
  return rows;
32821
33030
  }
32822
33031
 
32823
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
33032
+ // src/git/changes/list-changed-files-for-repo.ts
32824
33033
  import * as fs22 from "node:fs";
32825
33034
  import * as path22 from "node:path";
32826
33035
 
32827
- // src/git/working-directory/changes/count-lines.ts
33036
+ // src/git/changes/lib/count-lines.ts
32828
33037
  import { createReadStream } from "node:fs";
32829
33038
  import * as readline2 from "node:readline";
32830
33039
  async function countTextFileLines(filePath) {
@@ -32845,7 +33054,7 @@ async function countTextFileLines(filePath) {
32845
33054
  return lines;
32846
33055
  }
32847
33056
 
32848
- // src/git/working-directory/changes/hydrate-patch.ts
33057
+ // src/git/changes/hydrate-patch.ts
32849
33058
  import * as fs21 from "node:fs";
32850
33059
  var UNIFIED_HUNK_HEADER_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
32851
33060
  var MAX_HYDRATE_LINES_PER_GAP = 8e3;
@@ -32961,26 +33170,16 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
32961
33170
  return truncatePatch(out.join("\n"));
32962
33171
  }
32963
33172
 
32964
- // src/git/working-directory/changes/unified-diff-for-file.ts
33173
+ // src/git/changes/unified-diff-for-file.ts
32965
33174
  async function unifiedDiffForFile(repoCwd, pathInRepo, change) {
32966
33175
  const g = cliSimpleGit(repoCwd);
32967
- try {
32968
- let raw;
32969
- if (change === "added") {
32970
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
32971
- raw = await g.raw(["diff", "--no-index", "--", devNull, pathInRepo]);
32972
- } else {
32973
- raw = await g.raw(["diff", "HEAD", "--", pathInRepo]);
32974
- }
32975
- const t = String(raw).trim();
32976
- if (!t) return void 0;
32977
- return truncatePatch(t);
32978
- } catch {
32979
- return void 0;
32980
- }
33176
+ const args = change === "added" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : change === "removed" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : ["diff", "--no-color", "HEAD", "--", pathInRepo];
33177
+ const raw = await g.raw([...args]).catch(() => "");
33178
+ const t = String(raw).trim();
33179
+ return t ? truncatePatch(t) : void 0;
32981
33180
  }
32982
33181
 
32983
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
33182
+ // src/git/changes/list-changed-files-for-repo.ts
32984
33183
  async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
32985
33184
  const g = cliSimpleGit(repoGitCwd);
32986
33185
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
@@ -32994,7 +33193,7 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
32994
33193
  const untracked = String(untrackedRaw).split("\n").map((s) => s.trim()).filter(Boolean);
32995
33194
  for (const p of untracked) paths.add(p);
32996
33195
  const rows = [];
32997
- for (const pathInRepo of paths) {
33196
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
32998
33197
  const relLauncher = posixJoinDirFile(repoRelPath, pathInRepo.replace(/\\/g, "/"));
32999
33198
  const repoFilePath = path22.join(repoGitCwd, pathInRepo);
33000
33199
  const nums = numByPath.get(pathInRepo);
@@ -33024,9 +33223,9 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33024
33223
  else change = "modified";
33025
33224
  }
33026
33225
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
33027
- }
33226
+ });
33028
33227
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
33029
- for (const row of rows) {
33228
+ await forEachWithGitYield(rows, async (row) => {
33030
33229
  let pathInRepo;
33031
33230
  if (normRel === ".") {
33032
33231
  pathInRepo = row.pathRelLauncher;
@@ -33041,11 +33240,11 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33041
33240
  patch = await hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, pathInRepo, row.change);
33042
33241
  }
33043
33242
  row.patchContent = patch;
33044
- }
33243
+ });
33045
33244
  return rows;
33046
33245
  }
33047
33246
 
33048
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
33247
+ // src/git/changes/get-working-tree-change-repo-details.ts
33049
33248
  function normRepoRel(p) {
33050
33249
  const x = p.replace(/\\/g, "/").trim();
33051
33250
  return x === "" ? "." : x;
@@ -33064,7 +33263,9 @@ async function getWorkingTreeChangeRepoDetails(options) {
33064
33263
  throw new Error("commit sha is required for commit changes");
33065
33264
  }
33066
33265
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
33067
- for (const target of options.commitTargetPaths) {
33266
+ for (let i = 0; i < options.commitTargetPaths.length; i++) {
33267
+ if (i > 0) await yieldToEventLoop2();
33268
+ const target = options.commitTargetPaths[i];
33068
33269
  const t = path23.resolve(target);
33069
33270
  if (!await isGitRepoDirectory(t)) continue;
33070
33271
  const g = cliSimpleGit(t);
@@ -33098,7 +33299,10 @@ async function getWorkingTreeChangeRepoDetails(options) {
33098
33299
  const files = basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
33099
33300
  const st = await g.status();
33100
33301
  const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
33101
- const unpushedCommits = await listUnpushedCommits(t);
33302
+ const [unpushedCommits, recentCommitList] = await Promise.all([
33303
+ listUnpushedCommits(t),
33304
+ listRecentCommits(t, options.recentCommitsLimit)
33305
+ ]);
33102
33306
  out.push({
33103
33307
  repoRelPath: norm,
33104
33308
  repoDisplayName,
@@ -33108,6 +33312,8 @@ async function getWorkingTreeChangeRepoDetails(options) {
33108
33312
  files,
33109
33313
  hasUncommittedChanges,
33110
33314
  unpushedCommits,
33315
+ recentCommits: recentCommitList.commits,
33316
+ recentCommitsHasMore: recentCommitList.hasMore,
33111
33317
  changesView: basis.kind === "commit" ? "commit" : "working",
33112
33318
  changesCommitSha: basis.kind === "commit" ? basis.sha.trim() : null
33113
33319
  });
@@ -33116,7 +33322,7 @@ async function getWorkingTreeChangeRepoDetails(options) {
33116
33322
  return out;
33117
33323
  }
33118
33324
 
33119
- // src/git/commit-and-push.ts
33325
+ // src/git/branches/commit-and-push.ts
33120
33326
  async function gitCommitAllIfDirty(repoDir, message, options) {
33121
33327
  const g = cliSimpleGit(repoDir);
33122
33328
  const st = await g.status();
@@ -33540,7 +33746,8 @@ var SessionWorktreeManager = class {
33540
33746
  sessionWorktreeRootPath: sessionWorkingTreeRelRoot,
33541
33747
  legacyRepoNestedSessionLayout: legacyNested,
33542
33748
  repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
33543
- basis: opts?.basis
33749
+ basis: opts?.basis,
33750
+ recentCommitsLimit: opts?.recentCommitsLimit
33544
33751
  });
33545
33752
  }
33546
33753
  async pushSessionUpstream(sessionId) {
@@ -33577,30 +33784,39 @@ import path27 from "node:path";
33577
33784
  function shouldSkipWorkspaceWalkEntry(name) {
33578
33785
  return name.startsWith(".");
33579
33786
  }
33580
- function walkWorkspaceTreeSync(dir, baseDir, onFile) {
33787
+ async function walkWorkspaceTreeAsync(dir, baseDir, onFile, state) {
33581
33788
  let names;
33582
33789
  try {
33583
- names = fs24.readdirSync(dir);
33790
+ names = await fs24.promises.readdir(dir);
33584
33791
  } catch {
33585
33792
  return;
33586
33793
  }
33587
33794
  for (const name of names) {
33588
33795
  if (shouldSkipWorkspaceWalkEntry(name)) continue;
33796
+ if (state.n > 0 && state.n % INDEX_WORK_YIELD_EVERY === 0) {
33797
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
33798
+ await yieldToEventLoop();
33799
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
33800
+ }
33801
+ state.n++;
33589
33802
  const full = path27.join(dir, name);
33590
33803
  let stat2;
33591
33804
  try {
33592
- stat2 = fs24.statSync(full);
33805
+ stat2 = await fs24.promises.stat(full);
33593
33806
  } catch {
33594
33807
  continue;
33595
33808
  }
33596
33809
  const relative5 = path27.relative(baseDir, full).replace(/\\/g, "/");
33597
33810
  if (stat2.isDirectory()) {
33598
- walkWorkspaceTreeSync(full, baseDir, onFile);
33811
+ await walkWorkspaceTreeAsync(full, baseDir, onFile, state);
33599
33812
  } else if (stat2.isFile()) {
33600
33813
  onFile(relative5);
33601
33814
  }
33602
33815
  }
33603
33816
  }
33817
+ function createWalkYieldState() {
33818
+ return { n: 0 };
33819
+ }
33604
33820
 
33605
33821
  // src/files/index/file-index-sqlite-lock.ts
33606
33822
  import fs25 from "node:fs";
@@ -33633,29 +33849,28 @@ function withFileIndexSqliteLock(fn) {
33633
33849
  }
33634
33850
 
33635
33851
  // src/files/index/build-file-index.ts
33636
- var FILE_INDEX_INSERT_BUFFER = 2048;
33637
- function persistFileIndexForResolvedCwd(resolved) {
33852
+ var FILE_INDEX_INTERRUPT_CHECK_EVERY = 256;
33853
+ function assertNotShutdown() {
33854
+ if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
33855
+ }
33856
+ function persistFileIndexPaths(resolved, paths) {
33638
33857
  return withCliSqliteSync((db) => {
33639
33858
  const h = getCwdHashForFileIndex(resolved);
33640
- const buf = [];
33641
33859
  let pathCount = 0;
33642
33860
  db.run("BEGIN IMMEDIATE");
33643
33861
  try {
33644
33862
  db.run("DELETE FROM file_index_path WHERE cwd_hash = ?", [h]);
33645
33863
  const ins = db.prepare("INSERT INTO file_index_path (cwd_hash, path) VALUES (?, ?)");
33646
33864
  try {
33647
- const flushBuf = () => {
33648
- for (const rel of buf) {
33649
- ins.run([h, rel]);
33865
+ let batch = 0;
33866
+ for (const rel of paths) {
33867
+ if (++batch >= FILE_INDEX_INTERRUPT_CHECK_EVERY) {
33868
+ batch = 0;
33869
+ assertNotShutdown();
33650
33870
  }
33651
- pathCount += buf.length;
33652
- buf.length = 0;
33653
- };
33654
- walkWorkspaceTreeSync(resolved, resolved, (rel) => {
33655
- buf.push(rel);
33656
- if (buf.length >= FILE_INDEX_INSERT_BUFFER) flushBuf();
33657
- });
33658
- flushBuf();
33871
+ ins.run([h, rel]);
33872
+ pathCount += 1;
33873
+ }
33659
33874
  } finally {
33660
33875
  ins.finalize();
33661
33876
  }
@@ -33670,12 +33885,24 @@ function persistFileIndexForResolvedCwd(resolved) {
33670
33885
  return pathCount;
33671
33886
  });
33672
33887
  }
33888
+ async function collectWorkspacePathsAsync(resolved) {
33889
+ const paths = [];
33890
+ const state = createWalkYieldState();
33891
+ await walkWorkspaceTreeAsync(resolved, resolved, (rel) => {
33892
+ assertNotShutdown();
33893
+ paths.push(rel);
33894
+ }, state);
33895
+ return paths;
33896
+ }
33673
33897
  async function buildFileIndexAsync(cwd) {
33674
33898
  return withFileIndexSqliteLock(async () => {
33675
33899
  const resolved = path28.resolve(cwd);
33676
33900
  await yieldToEventLoop();
33677
- const pathCount = persistFileIndexForResolvedCwd(resolved);
33901
+ assertNotShutdown();
33902
+ const paths = await collectWorkspacePathsAsync(resolved);
33678
33903
  await yieldToEventLoop();
33904
+ assertNotShutdown();
33905
+ const pathCount = persistFileIndexPaths(resolved, paths);
33679
33906
  return { pathCount };
33680
33907
  });
33681
33908
  }
@@ -33779,11 +34006,13 @@ function createFsWatcher(resolved, schedule) {
33779
34006
  function startFileIndexWatcher(cwd = getBridgeRoot()) {
33780
34007
  const resolved = path30.resolve(cwd);
33781
34008
  void buildFileIndexAsync(resolved).catch((e) => {
34009
+ if (e instanceof CliSqliteInterrupted) return;
33782
34010
  console.error("[file-index] Initial index build failed:", e);
33783
34011
  });
33784
34012
  let timer = null;
33785
34013
  const runRebuild = () => {
33786
34014
  void buildFileIndexAsync(resolved).catch((e) => {
34015
+ if (e instanceof CliSqliteInterrupted) return;
33787
34016
  console.error("[file-index] Watch rebuild failed:", e);
33788
34017
  });
33789
34018
  };
@@ -34048,7 +34277,7 @@ function pipedStdoutStderrFor(attemptStdio) {
34048
34277
  }
34049
34278
 
34050
34279
  // src/dev-servers/manager/shell-spawn/try-spawn-piped-via-sh.ts
34051
- import { spawn as spawn5 } from "node:child_process";
34280
+ import { spawn as spawn6 } from "node:child_process";
34052
34281
  function trySpawnPipedViaSh(command, env, cwd, signal) {
34053
34282
  const attempts = [
34054
34283
  { stdio: [devNullReadFd(), "pipe", "pipe"], endStdin: false },
@@ -34069,9 +34298,9 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
34069
34298
  if (process.platform === "win32") {
34070
34299
  opts.windowsHide = true;
34071
34300
  const com = process.env.ComSpec || "cmd.exe";
34072
- proc = spawn5(com, ["/d", "/s", "/c", command], opts);
34301
+ proc = spawn6(com, ["/d", "/s", "/c", command], opts);
34073
34302
  } else {
34074
- proc = spawn5("/bin/sh", ["-c", command], opts);
34303
+ proc = spawn6("/bin/sh", ["-c", command], opts);
34075
34304
  }
34076
34305
  if (attempt.endStdin) {
34077
34306
  proc.stdin?.end();
@@ -34091,7 +34320,7 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
34091
34320
  }
34092
34321
 
34093
34322
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-true-piped.ts
34094
- import { spawn as spawn6 } from "node:child_process";
34323
+ import { spawn as spawn7 } from "node:child_process";
34095
34324
  function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34096
34325
  try {
34097
34326
  const opts = {
@@ -34104,7 +34333,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34104
34333
  if (process.platform === "win32") {
34105
34334
  opts.windowsHide = true;
34106
34335
  }
34107
- return spawn6(command, opts);
34336
+ return spawn7(command, opts);
34108
34337
  } catch (e) {
34109
34338
  if (isSpawnEbadf(e)) return null;
34110
34339
  throw e;
@@ -34112,7 +34341,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34112
34341
  }
34113
34342
 
34114
34343
  // src/dev-servers/manager/shell-spawn/try-spawn-merged-log-file.ts
34115
- import { spawn as spawn7 } from "node:child_process";
34344
+ import { spawn as spawn8 } from "node:child_process";
34116
34345
  import fs28 from "node:fs";
34117
34346
  import { tmpdir } from "node:os";
34118
34347
  import path31 from "node:path";
@@ -34130,7 +34359,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34130
34359
  try {
34131
34360
  let proc;
34132
34361
  if (process.platform === "win32") {
34133
- proc = spawn7(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
34362
+ proc = spawn8(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
34134
34363
  env,
34135
34364
  cwd,
34136
34365
  stdio,
@@ -34138,7 +34367,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34138
34367
  ...signal ? { signal } : {}
34139
34368
  });
34140
34369
  } else {
34141
- proc = spawn7("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
34370
+ proc = spawn8("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
34142
34371
  }
34143
34372
  fs28.closeSync(logFd);
34144
34373
  return {
@@ -34159,7 +34388,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34159
34388
  }
34160
34389
 
34161
34390
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-script-log-redirect.ts
34162
- import { spawn as spawn8 } from "node:child_process";
34391
+ import { spawn as spawn9 } from "node:child_process";
34163
34392
  import fs29 from "node:fs";
34164
34393
  import { tmpdir as tmpdir2 } from "node:os";
34165
34394
  import path32 from "node:path";
@@ -34182,7 +34411,7 @@ cd ${shSingleQuote(cwd)}
34182
34411
  /bin/sh ${shSingleQuote(innerPath)} >>${shSingleQuote(logPath)} 2>&1
34183
34412
  `
34184
34413
  );
34185
- const proc = spawn8("/bin/sh", [runnerPath], {
34414
+ const proc = spawn9("/bin/sh", [runnerPath], {
34186
34415
  env,
34187
34416
  cwd: tmpRoot,
34188
34417
  stdio: "ignore",
@@ -34214,7 +34443,7 @@ CD /D ${q(cwd)}\r
34214
34443
  ${command} >> ${q(logPath)} 2>&1\r
34215
34444
  `
34216
34445
  );
34217
- const proc = spawn8(com, ["/d", "/s", "/c", q(runnerPath)], {
34446
+ const proc = spawn9(com, ["/d", "/s", "/c", q(runnerPath)], {
34218
34447
  env,
34219
34448
  cwd: tmpRoot,
34220
34449
  stdio: "ignore",
@@ -34235,7 +34464,7 @@ ${command} >> ${q(logPath)} 2>&1\r
34235
34464
  }
34236
34465
 
34237
34466
  // src/dev-servers/manager/shell-spawn/try-spawn-inherit.ts
34238
- import { spawn as spawn9 } from "node:child_process";
34467
+ import { spawn as spawn10 } from "node:child_process";
34239
34468
  function trySpawnInheritStdio(command, env, cwd, signal) {
34240
34469
  const opts = {
34241
34470
  env,
@@ -34247,9 +34476,9 @@ function trySpawnInheritStdio(command, env, cwd, signal) {
34247
34476
  if (process.platform === "win32") {
34248
34477
  opts.windowsHide = true;
34249
34478
  const com = process.env.ComSpec || "cmd.exe";
34250
- proc = spawn9(com, ["/d", "/s", "/c", command], opts);
34479
+ proc = spawn10(com, ["/d", "/s", "/c", command], opts);
34251
34480
  } else {
34252
- proc = spawn9("/bin/sh", ["-c", command], opts);
34481
+ proc = spawn10("/bin/sh", ["-c", command], opts);
34253
34482
  }
34254
34483
  return { proc, pipedStdoutStderr: false };
34255
34484
  }
@@ -34317,9 +34546,6 @@ var StreamTail = class {
34317
34546
  }
34318
34547
  };
34319
34548
 
34320
- // src/dev-servers/manager/dev-server-constants.ts
34321
- var BRIDGE_SHUTDOWN_GRACE_MS = 8e3;
34322
-
34323
34549
  // src/dev-servers/manager/dev-server-firehose-messages.ts
34324
34550
  function buildFirehoseSnapshotMessage(params) {
34325
34551
  const payload = {
@@ -34605,22 +34831,23 @@ var DevServerManager = class {
34605
34831
  }
34606
34832
  this.start(serverId);
34607
34833
  }
34608
- async shutdownAllGraceful() {
34834
+ async shutdownAllGraceful(opts) {
34835
+ const graceMs = opts?.graceMs ?? BRIDGE_SHUTDOWN_GRACE_MS;
34609
34836
  const pairs = [...this.processes.entries()];
34610
34837
  if (pairs.length === 0) return;
34611
34838
  this.log(
34612
34839
  `[dev-server] Stopping ${pairs.length} local dev server process${pairs.length === 1 ? "" : "es"}\u2026`
34613
34840
  );
34614
- await Promise.all(pairs.map(([serverId, proc]) => this.gracefulTerminateOrUnknown(serverId, proc)));
34841
+ await Promise.all(pairs.map(([serverId, proc]) => this.gracefulTerminateOrUnknown(serverId, proc, graceMs)));
34615
34842
  }
34616
- async gracefulTerminateOrUnknown(serverId, proc) {
34843
+ async gracefulTerminateOrUnknown(serverId, proc, graceMs) {
34617
34844
  const shortId = `${serverId.slice(0, 8)}\u2026`;
34618
- await sigtermAndWaitForExit(proc, BRIDGE_SHUTDOWN_GRACE_MS, this.log, shortId);
34845
+ await sigtermAndWaitForExit(proc, graceMs, this.log, shortId);
34619
34846
  if (!this.processes.has(serverId) || this.processes.get(serverId) !== proc) {
34620
34847
  return;
34621
34848
  }
34622
34849
  this.bumpGeneration(serverId);
34623
- forceKillChild(proc, this.log, shortId, BRIDGE_SHUTDOWN_GRACE_MS);
34850
+ forceKillChild(proc, this.log, shortId, graceMs);
34624
34851
  this.processes.delete(serverId);
34625
34852
  this.clearPoll(serverId);
34626
34853
  this.pipedCaptureByServerId.delete(serverId);
@@ -35047,10 +35274,13 @@ var LOCAL_AGENT_ACP_MODULES = [
35047
35274
  ];
35048
35275
  async function detectLocalAgentTypes() {
35049
35276
  try {
35277
+ if (isCliImmediateShutdownRequested()) return [];
35050
35278
  const out = [];
35051
35279
  for (let i = 0; i < LOCAL_AGENT_ACP_MODULES.length; i++) {
35280
+ if (isCliImmediateShutdownRequested()) return out;
35052
35281
  if (i > 0) {
35053
35282
  await yieldToEventLoop();
35283
+ if (isCliImmediateShutdownRequested()) return out;
35054
35284
  }
35055
35285
  const mod = LOCAL_AGENT_ACP_MODULES[i];
35056
35286
  try {
@@ -35083,6 +35313,7 @@ function createSendLocalSkillsReport(getWs, logFn) {
35083
35313
  }
35084
35314
  function createReportAutoDetectedAgents(getWs, logFn) {
35085
35315
  return async () => {
35316
+ if (isCliImmediateShutdownRequested()) return;
35086
35317
  try {
35087
35318
  const types = await detectLocalAgentTypes();
35088
35319
  const socket = getWs();
@@ -35182,6 +35413,7 @@ var handleBridgeIdentified = (msg, deps) => {
35182
35413
  });
35183
35414
  setImmediate(() => {
35184
35415
  void (async () => {
35416
+ if (isCliImmediateShutdownRequested()) return;
35185
35417
  try {
35186
35418
  await deps.reportAutoDetectedAgents?.();
35187
35419
  } catch (e) {
@@ -35189,6 +35421,7 @@ var handleBridgeIdentified = (msg, deps) => {
35189
35421
  `[Bridge service] Auto-detect agents failed: ${e instanceof Error ? e.message : String(e)}`
35190
35422
  );
35191
35423
  }
35424
+ if (isCliImmediateShutdownRequested()) return;
35192
35425
  try {
35193
35426
  await deps.warmupAgentCapabilitiesOnConnect?.();
35194
35427
  } catch (e) {
@@ -35199,6 +35432,7 @@ var handleBridgeIdentified = (msg, deps) => {
35199
35432
  })();
35200
35433
  });
35201
35434
  setImmediate(() => {
35435
+ if (isCliImmediateShutdownRequested()) return;
35202
35436
  try {
35203
35437
  deps.sendLocalSkillsReport?.();
35204
35438
  } catch (e) {
@@ -35500,12 +35734,12 @@ function createBridgePromptSenders(deps, getWs) {
35500
35734
  }
35501
35735
 
35502
35736
  // src/agents/acp/from-bridge/bridge-prompt-preamble.ts
35503
- import { execFile as execFile10 } from "node:child_process";
35504
- import { promisify as promisify10 } from "node:util";
35505
- var execFileAsync9 = promisify10(execFile10);
35737
+ import { execFile as execFile8 } from "node:child_process";
35738
+ import { promisify as promisify8 } from "node:util";
35739
+ var execFileAsync7 = promisify8(execFile8);
35506
35740
  async function readGitBranch(cwd) {
35507
35741
  try {
35508
- const { stdout } = await execFileAsync9("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
35742
+ const { stdout } = await execFileAsync7("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
35509
35743
  const b = stdout.trim();
35510
35744
  return b || null;
35511
35745
  } catch {
@@ -36215,7 +36449,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36215
36449
  reply({
36216
36450
  ok: true,
36217
36451
  hasUncommittedChanges: r.hasUncommittedChanges,
36218
- hasUnpushedCommits: r.hasUnpushedCommits
36452
+ hasUnpushedCommits: r.hasUnpushedCommits,
36453
+ uncommittedFileCount: r.uncommittedFileCount
36219
36454
  });
36220
36455
  return;
36221
36456
  }
@@ -36229,7 +36464,12 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36229
36464
  return;
36230
36465
  }
36231
36466
  }
36232
- const opts = repoRel && view === "commit" && commitSha ? { repoRelPath: repoRel, basis: { kind: "commit", sha: commitSha } } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" } } : void 0;
36467
+ const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
36468
+ const opts = repoRel && view === "commit" && commitSha ? {
36469
+ repoRelPath: repoRel,
36470
+ basis: { kind: "commit", sha: commitSha },
36471
+ recentCommitsLimit
36472
+ } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
36233
36473
  const repos = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts);
36234
36474
  reply({
36235
36475
  ok: true,
@@ -36247,7 +36487,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36247
36487
  reply({
36248
36488
  ok: true,
36249
36489
  hasUncommittedChanges: st2.hasUncommittedChanges,
36250
- hasUnpushedCommits: st2.hasUnpushedCommits
36490
+ hasUnpushedCommits: st2.hasUnpushedCommits,
36491
+ uncommittedFileCount: st2.uncommittedFileCount
36251
36492
  });
36252
36493
  return;
36253
36494
  }
@@ -36272,7 +36513,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36272
36513
  reply({
36273
36514
  ok: true,
36274
36515
  hasUncommittedChanges: st.hasUncommittedChanges,
36275
- hasUnpushedCommits: st.hasUnpushedCommits
36516
+ hasUnpushedCommits: st.hasUnpushedCommits,
36517
+ uncommittedFileCount: st.uncommittedFileCount
36276
36518
  });
36277
36519
  } catch (e) {
36278
36520
  reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -36876,6 +37118,7 @@ import * as path39 from "node:path";
36876
37118
  import * as path38 from "node:path";
36877
37119
  async function probeOneAgentTypeForCapabilities(params) {
36878
37120
  const { agentType, cwd, workspaceId, log: log2, reportAgentCapabilities, bridgeReport = true } = params;
37121
+ if (isCliImmediateShutdownRequested()) return false;
36879
37122
  const resolved = resolveAgentCommand(agentType);
36880
37123
  if (!resolved) return false;
36881
37124
  let sqliteChanged = false;
@@ -36909,6 +37152,7 @@ async function probeOneAgentTypeForCapabilities(params) {
36909
37152
  }, 28e3);
36910
37153
  killTimer.unref?.();
36911
37154
  try {
37155
+ if (isCliImmediateShutdownRequested()) return false;
36912
37156
  handle = await resolved.createClient({
36913
37157
  command: resolved.command,
36914
37158
  cwd: path38.resolve(cwd),
@@ -36928,7 +37172,7 @@ async function probeOneAgentTypeForCapabilities(params) {
36928
37172
  onSessionUpdate: () => {
36929
37173
  }
36930
37174
  });
36931
- await new Promise((r) => setTimeout(r, 1200));
37175
+ if (!await delayMsUnlessShutdownRequested(1200)) return false;
36932
37176
  } catch (e) {
36933
37177
  log2(
36934
37178
  `[Bridge service] Agent capability probe (${agentType}): ${e instanceof Error ? e.message : String(e)}`
@@ -36956,6 +37200,7 @@ async function probeAgentCapabilitiesForDetectedTypes(params) {
36956
37200
  } = params;
36957
37201
  let changedCount = 0;
36958
37202
  for (let i = 0; i < agentTypes.length; i++) {
37203
+ if (isCliImmediateShutdownRequested()) return changedCount;
36959
37204
  if (i > 0) await yieldToEventLoop();
36960
37205
  const agentType = agentTypes[i];
36961
37206
  if (!agentType.trim()) continue;
@@ -36983,6 +37228,7 @@ async function probeAgentCapabilitiesForDetectedTypes(params) {
36983
37228
  // src/agents/capabilities/warmup-agent-capabilities-on-connect.ts
36984
37229
  async function warmupAgentCapabilitiesOnConnect(params) {
36985
37230
  const { workspaceId, log: log2, getWs } = params;
37231
+ if (isCliImmediateShutdownRequested()) return;
36986
37232
  const cwd = path39.resolve(getBridgeRoot());
36987
37233
  async function sendBatchFromCache() {
36988
37234
  const socket = getWs();
@@ -36995,18 +37241,21 @@ async function warmupAgentCapabilitiesOnConnect(params) {
36995
37241
  items: rows.map((r) => ({ agentType: r.agentType, configOptions: r.configOptions }))
36996
37242
  });
36997
37243
  } catch (e) {
37244
+ if (e instanceof CliSqliteInterrupted) return;
36998
37245
  log2(
36999
37246
  `[Bridge service] Agent capability batch to bridge failed: ${e instanceof Error ? e.message : String(e)}`
37000
37247
  );
37001
37248
  }
37002
37249
  }
37003
37250
  await sendBatchFromCache();
37251
+ if (isCliImmediateShutdownRequested()) return;
37004
37252
  let types = [];
37005
37253
  try {
37006
37254
  types = [...await detectLocalAgentTypes()];
37007
37255
  } catch (e) {
37008
37256
  log2(`[Bridge service] detectLocalAgentTypes failed: ${e instanceof Error ? e.message : String(e)}`);
37009
37257
  }
37258
+ if (isCliImmediateShutdownRequested()) return;
37010
37259
  try {
37011
37260
  const n = await probeAgentCapabilitiesForDetectedTypes({
37012
37261
  agentTypes: types,
@@ -37018,11 +37267,13 @@ async function warmupAgentCapabilitiesOnConnect(params) {
37018
37267
  });
37019
37268
  if (n > 0) await sendBatchFromCache();
37020
37269
  } catch (e) {
37270
+ if (e instanceof CliSqliteInterrupted) return;
37021
37271
  log2(`[Bridge service] Agent capability probe (missing cache) failed: ${e instanceof Error ? e.message : String(e)}`);
37022
37272
  }
37023
37273
  void (async () => {
37024
37274
  try {
37025
37275
  await yieldToEventLoop();
37276
+ if (isCliImmediateShutdownRequested()) return;
37026
37277
  const n = await probeAgentCapabilitiesForDetectedTypes({
37027
37278
  agentTypes: types,
37028
37279
  cwd,
@@ -37033,6 +37284,7 @@ async function warmupAgentCapabilitiesOnConnect(params) {
37033
37284
  });
37034
37285
  if (n > 0) await sendBatchFromCache();
37035
37286
  } catch (e) {
37287
+ if (e instanceof CliSqliteInterrupted) return;
37036
37288
  log2(`[Bridge service] Agent capability lazy refresh failed: ${e instanceof Error ? e.message : String(e)}`);
37037
37289
  }
37038
37290
  })();
@@ -37080,7 +37332,8 @@ async function createBridgeConnection(options) {
37080
37332
  configOptions: info.configOptions
37081
37333
  })
37082
37334
  );
37083
- } catch {
37335
+ } catch (e) {
37336
+ if (e instanceof CliSqliteInterrupted) return;
37084
37337
  }
37085
37338
  if (!changed) return;
37086
37339
  const socket = getWs();
@@ -37163,6 +37416,7 @@ async function createBridgeConnection(options) {
37163
37416
  const stopFileIndexWatcher = startFileIndexWatcher(getBridgeRoot());
37164
37417
  return {
37165
37418
  close: async () => {
37419
+ requestCliImmediateShutdown();
37166
37420
  stopFileIndexWatcher();
37167
37421
  bridgeHeartbeat.stop();
37168
37422
  await closeBridgeConnection(state, acpManager, devServerManager, logFn);
@@ -37236,47 +37490,61 @@ async function runConnectedBridge(options, restartWithoutAuth) {
37236
37490
  } = options;
37237
37491
  const firehoseServerUrl = options.firehoseServerUrl ?? options.proxyServerUrl;
37238
37492
  let cleanupKeyCommand;
37239
- const handle = await createBridgeConnection({
37240
- apiUrl,
37241
- workspaceId,
37242
- authToken,
37243
- refreshToken,
37244
- firehoseServerUrl,
37245
- justAuthenticated,
37246
- worktreesRootPath,
37247
- e2eCertificate,
37248
- log,
37249
- persistTokens: (t) => {
37250
- writeConfigForApi(apiUrl, {
37251
- workspaceId,
37252
- token: t.token,
37253
- refreshToken: t.refreshToken
37254
- });
37255
- },
37256
- onAuthInvalid: () => {
37257
- cleanupKeyCommand?.();
37258
- log("[Bridge service] Access token invalid or revoked; re-authenticating\u2026");
37259
- clearConfigForApi(apiUrl);
37260
- void handle.close().then(() => {
37261
- void restartWithoutAuth({ apiUrl, firehoseServerUrl, worktreesRootPath, e2eCertificate });
37262
- });
37263
- }
37264
- });
37493
+ let bridgeClose = null;
37265
37494
  const onSignal = (kind) => {
37495
+ requestCliImmediateShutdown();
37496
+ abortActiveGitChildProcesses();
37266
37497
  cleanupKeyCommand?.();
37267
37498
  logImmediate(
37268
37499
  kind === "interrupt" ? "Keyboard interrupt (Ctrl+C) \u2014 stopping\u2026" : "Stop requested \u2014 shutting down\u2026"
37269
37500
  );
37270
- setImmediate(() => {
37271
- void handle.close().then(() => {
37272
- process.exit(0);
37273
- });
37274
- });
37501
+ if (bridgeClose) {
37502
+ void bridgeClose().then(() => process.exit(0));
37503
+ return;
37504
+ }
37505
+ process.exit(0);
37275
37506
  };
37276
37507
  const onSigInt = () => onSignal("interrupt");
37277
37508
  const onSigTerm = () => onSignal("stop");
37278
37509
  process.on("SIGINT", onSigInt);
37279
37510
  process.on("SIGTERM", onSigTerm);
37511
+ let handle;
37512
+ try {
37513
+ handle = await createBridgeConnection({
37514
+ apiUrl,
37515
+ workspaceId,
37516
+ authToken,
37517
+ refreshToken,
37518
+ firehoseServerUrl,
37519
+ justAuthenticated,
37520
+ worktreesRootPath,
37521
+ e2eCertificate,
37522
+ log,
37523
+ persistTokens: (t) => {
37524
+ writeConfigForApi(apiUrl, {
37525
+ workspaceId,
37526
+ token: t.token,
37527
+ refreshToken: t.refreshToken
37528
+ });
37529
+ },
37530
+ onAuthInvalid: () => {
37531
+ cleanupKeyCommand?.();
37532
+ log("[Bridge service] Access token invalid or revoked; re-authenticating\u2026");
37533
+ clearConfigForApi(apiUrl);
37534
+ void handle.close().then(() => {
37535
+ void restartWithoutAuth({ apiUrl, firehoseServerUrl, worktreesRootPath, e2eCertificate });
37536
+ });
37537
+ }
37538
+ });
37539
+ } catch (e) {
37540
+ process.off("SIGINT", onSigInt);
37541
+ process.off("SIGTERM", onSigTerm);
37542
+ if (e instanceof CliSqliteInterrupted) {
37543
+ process.exit(0);
37544
+ }
37545
+ throw e;
37546
+ }
37547
+ bridgeClose = () => handle.close();
37280
37548
  if (e2eCertificate) {
37281
37549
  let openingCertificate = false;
37282
37550
  cleanupKeyCommand = installE2eCertificateKeyCommand({
@@ -37321,6 +37589,7 @@ async function runBridge(options) {
37321
37589
  }
37322
37590
  });
37323
37591
  const onSignal = (kind) => {
37592
+ requestCliImmediateShutdown();
37324
37593
  logImmediate(
37325
37594
  kind === "interrupt" ? "Keyboard interrupt (Ctrl+C) \u2014 stopping\u2026" : "Stop requested \u2014 shutting down\u2026"
37326
37595
  );