@buildautomaton/cli 0.1.33 → 0.1.35

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
@@ -11857,8 +11857,8 @@ function getElementAtPath(obj, path43) {
11857
11857
  }
11858
11858
  function promiseAllObject(promisesObj) {
11859
11859
  const keys = Object.keys(promisesObj);
11860
- const promises3 = keys.map((key) => promisesObj[key]);
11861
- return Promise.all(promises3).then((results) => {
11860
+ const promises5 = keys.map((key) => promisesObj[key]);
11861
+ return Promise.all(promises5).then((results) => {
11862
11862
  const resolvedObj = {};
11863
11863
  for (let i = 0; i < keys.length; i++) {
11864
11864
  resolvedObj[keys[i]] = results[i];
@@ -25064,7 +25064,7 @@ var {
25064
25064
  } = import_index.default;
25065
25065
 
25066
25066
  // src/cli-version.ts
25067
- var CLI_VERSION = "0.1.33".length > 0 ? "0.1.33" : "0.0.0-dev";
25067
+ var CLI_VERSION = "0.1.35".length > 0 ? "0.1.35" : "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";
@@ -25607,20 +25607,22 @@ function createWsBridge(options) {
25607
25607
  onOpen?.();
25608
25608
  });
25609
25609
  ws.on("message", (raw) => {
25610
- try {
25611
- let data;
25612
- if (typeof raw === "string") {
25613
- data = JSON.parse(raw);
25614
- } else if (Buffer.isBuffer(raw) || raw instanceof ArrayBuffer) {
25615
- const str = Buffer.isBuffer(raw) ? raw.toString("utf8") : Buffer.from(raw).toString("utf8");
25616
- data = JSON.parse(str);
25617
- } else {
25618
- data = raw;
25610
+ setImmediate(() => {
25611
+ try {
25612
+ let data;
25613
+ if (typeof raw === "string") {
25614
+ data = JSON.parse(raw);
25615
+ } else if (Buffer.isBuffer(raw) || raw instanceof ArrayBuffer) {
25616
+ const str = Buffer.isBuffer(raw) ? raw.toString("utf8") : Buffer.from(raw).toString("utf8");
25617
+ data = JSON.parse(str);
25618
+ } else {
25619
+ data = raw;
25620
+ }
25621
+ onMessage?.(data);
25622
+ } catch {
25623
+ onMessage?.(raw);
25619
25624
  }
25620
- onMessage?.(data);
25621
- } catch {
25622
- onMessage?.(raw);
25623
- }
25625
+ });
25624
25626
  });
25625
25627
  ws.on("close", (code, reason) => {
25626
25628
  disposeClientPing();
@@ -27783,17 +27785,13 @@ function getAgentModelFromAgentConfig(config2) {
27783
27785
  return t !== "" ? t : null;
27784
27786
  }
27785
27787
 
27786
- // src/git/session-git-queue.ts
27787
- import { execFile as execFile7 } from "node:child_process";
27788
+ // src/git/snapshot/session-git-queue.ts
27788
27789
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
27789
- import { promisify as promisify7 } from "node:util";
27790
27790
  import * as path15 from "node:path";
27791
27791
 
27792
- // src/git/pre-turn-snapshot.ts
27792
+ // src/git/snapshot/pre-turn-snapshot.ts
27793
27793
  import * as fs15 from "node:fs";
27794
27794
  import * as path14 from "node:path";
27795
- import { execFile as execFile6 } from "node:child_process";
27796
- import { promisify as promisify6 } from "node:util";
27797
27795
 
27798
27796
  // src/git/discover-repos.ts
27799
27797
  import * as fs14 from "node:fs";
@@ -32358,12 +32356,68 @@ function gitInstanceFactory(baseDir, options) {
32358
32356
  init_git_response_error();
32359
32357
  var simpleGit = gitInstanceFactory;
32360
32358
 
32359
+ // src/git/git-runtime.ts
32360
+ var activeGitChildProcesses = /* @__PURE__ */ new Set();
32361
+ function abortActiveGitChildProcesses() {
32362
+ for (const child of activeGitChildProcesses) {
32363
+ try {
32364
+ child.kill("SIGTERM");
32365
+ } catch {
32366
+ }
32367
+ }
32368
+ }
32369
+ var GitOperationAbortedError = class extends Error {
32370
+ constructor(message = "Git operation aborted") {
32371
+ super(message);
32372
+ this.name = "GitOperationAbortedError";
32373
+ }
32374
+ };
32375
+ function throwIfGitShutdownRequested() {
32376
+ if (isCliImmediateShutdownRequested()) {
32377
+ abortActiveGitChildProcesses();
32378
+ throw new GitOperationAbortedError();
32379
+ }
32380
+ }
32381
+ async function runGitTask(fn) {
32382
+ throwIfGitShutdownRequested();
32383
+ try {
32384
+ const result = await fn();
32385
+ throwIfGitShutdownRequested();
32386
+ return result;
32387
+ } catch (e) {
32388
+ if (isCliImmediateShutdownRequested()) {
32389
+ throw new GitOperationAbortedError();
32390
+ }
32391
+ throw e;
32392
+ }
32393
+ }
32394
+ async function yieldToEventLoop2() {
32395
+ throwIfGitShutdownRequested();
32396
+ await new Promise((resolve20) => {
32397
+ setImmediate(resolve20);
32398
+ });
32399
+ throwIfGitShutdownRequested();
32400
+ }
32401
+ async function forEachWithGitYield(items, fn) {
32402
+ for (let i = 0; i < items.length; i++) {
32403
+ if (i > 0 || items.length > 1) await yieldToEventLoop2();
32404
+ await fn(items[i], i);
32405
+ }
32406
+ }
32407
+
32361
32408
  // src/git/cli-simple-git.ts
32362
32409
  function cliSimpleGit(baseDir) {
32363
- const git = simpleGit({ baseDir });
32410
+ const git = simpleGit({
32411
+ baseDir,
32412
+ trimmed: true,
32413
+ spawnOptions: {}
32414
+ });
32364
32415
  git.outputHandler((command, stdout, stderr) => {
32365
32416
  const trace = isCliTrace();
32366
32417
  const onChunk = (label) => (chunk) => {
32418
+ if (isCliImmediateShutdownRequested()) {
32419
+ abortActiveGitChildProcesses();
32420
+ }
32367
32421
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
32368
32422
  const line = text.replace(/\s+$/, "");
32369
32423
  if (trace && line) {
@@ -32452,17 +32506,37 @@ async function discoverGitReposUnderRoot(rootPath) {
32452
32506
  return out;
32453
32507
  }
32454
32508
 
32455
- // src/git/pre-turn-snapshot.ts
32509
+ // src/git/git-exec.ts
32510
+ import { execFile as execFile6, spawn as spawn2 } from "node:child_process";
32511
+ import { promisify as promisify6 } from "node:util";
32456
32512
  var execFileAsync5 = promisify6(execFile6);
32513
+ async function execGitFile(args, options) {
32514
+ throwIfGitShutdownRequested();
32515
+ try {
32516
+ const result = await execFileAsync5("git", args, {
32517
+ maxBuffer: 10 * 1024 * 1024,
32518
+ ...options
32519
+ });
32520
+ throwIfGitShutdownRequested();
32521
+ return {
32522
+ stdout: String(result.stdout ?? ""),
32523
+ stderr: String(result.stderr ?? "")
32524
+ };
32525
+ } catch (e) {
32526
+ if (isCliImmediateShutdownRequested()) {
32527
+ throw new GitOperationAbortedError();
32528
+ }
32529
+ throw e;
32530
+ }
32531
+ }
32532
+
32533
+ // src/git/snapshot/pre-turn-snapshot.ts
32457
32534
  function snapshotsDirForCwd(agentCwd) {
32458
32535
  return path14.join(agentCwd, ".buildautomaton", "snapshots");
32459
32536
  }
32460
32537
  async function gitStashCreate(repoRoot, log2) {
32461
32538
  try {
32462
- const { stdout } = await execFileAsync5("git", ["stash", "create"], {
32463
- cwd: repoRoot,
32464
- maxBuffer: 10 * 1024 * 1024
32465
- });
32539
+ const { stdout } = await execGitFile(["stash", "create"], { cwd: repoRoot });
32466
32540
  return stdout.trim();
32467
32541
  } catch (e) {
32468
32542
  log2(
@@ -32473,7 +32547,7 @@ async function gitStashCreate(repoRoot, log2) {
32473
32547
  }
32474
32548
  async function gitRun(repoRoot, args, log2, label) {
32475
32549
  try {
32476
- await execFileAsync5("git", args, { cwd: repoRoot, maxBuffer: 10 * 1024 * 1024 });
32550
+ await execGitFile(args, { cwd: repoRoot });
32477
32551
  return { ok: true };
32478
32552
  } catch (e) {
32479
32553
  const msg = e instanceof Error ? e.message : String(e);
@@ -32507,10 +32581,10 @@ async function capturePreTurnSnapshot(options) {
32507
32581
  return { ok: false, error: "No git repos to snapshot" };
32508
32582
  }
32509
32583
  const repos = [];
32510
- for (const root of repoRoots) {
32584
+ await forEachWithGitYield(repoRoots, async (root) => {
32511
32585
  const stashSha = await gitStashCreate(root, log2);
32512
32586
  repos.push({ path: root, stashSha });
32513
- }
32587
+ });
32514
32588
  const dir = snapshotsDirForCwd(agentCwd);
32515
32589
  try {
32516
32590
  fs15.mkdirSync(dir, { recursive: true });
@@ -32545,17 +32619,25 @@ async function applyPreTurnSnapshot(filePath, log2) {
32545
32619
  if (!Array.isArray(data.repos)) {
32546
32620
  return { ok: false, error: "Invalid snapshot file" };
32547
32621
  }
32548
- for (const r of data.repos) {
32549
- if (!r.path) continue;
32622
+ let applyError = null;
32623
+ await forEachWithGitYield(data.repos, async (r) => {
32624
+ if (applyError || !r.path) return;
32550
32625
  const reset = await gitRun(r.path, ["reset", "--hard", "HEAD"], log2, "reset --hard");
32551
- if (!reset.ok) return reset;
32626
+ if (!reset.ok) {
32627
+ applyError = reset;
32628
+ return;
32629
+ }
32552
32630
  const clean = await gitRun(r.path, ["clean", "-fd"], log2, "clean -fd");
32553
- if (!clean.ok) return clean;
32631
+ if (!clean.ok) {
32632
+ applyError = clean;
32633
+ return;
32634
+ }
32554
32635
  if (r.stashSha) {
32555
32636
  const ap = await gitRun(r.path, ["stash", "apply", r.stashSha], log2, "stash apply");
32556
- if (!ap.ok) return ap;
32637
+ if (!ap.ok) applyError = ap;
32557
32638
  }
32558
- }
32639
+ });
32640
+ if (applyError?.ok === false) return applyError;
32559
32641
  log2(`[snapshot] Restored pre-turn state for ${data.runId.slice(0, 8)}\u2026`);
32560
32642
  return { ok: true };
32561
32643
  }
@@ -32563,8 +32645,7 @@ function snapshotFilePath(agentCwd, runId) {
32563
32645
  return path14.join(snapshotsDirForCwd(agentCwd), `${runId}.json`);
32564
32646
  }
32565
32647
 
32566
- // src/git/session-git-queue.ts
32567
- var execFileAsync6 = promisify7(execFile7);
32648
+ // src/git/snapshot/session-git-queue.ts
32568
32649
  var MAX_FULL_FILE_TEXT_BYTES = 512 * 1024;
32569
32650
  async function readWorkspaceFileAsUtf8(absPath) {
32570
32651
  try {
@@ -32593,35 +32674,28 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
32593
32674
  return;
32594
32675
  }
32595
32676
  const multiRepo = data.repos.length > 1;
32596
- for (const repo of data.repos) {
32597
- if (!repo.stashSha) continue;
32677
+ await forEachWithGitYield(data.repos, async (repo) => {
32678
+ if (!repo.stashSha) return;
32598
32679
  let namesRaw;
32599
32680
  try {
32600
- const { stdout } = await execFileAsync6("git", ["diff", "--name-only", repo.stashSha], {
32601
- cwd: repo.path,
32602
- maxBuffer: 10 * 1024 * 1024
32603
- });
32681
+ const { stdout } = await execGitFile(["diff", "--name-only", repo.stashSha], { cwd: repo.path });
32604
32682
  namesRaw = stdout;
32605
32683
  } catch (e) {
32606
32684
  log2(
32607
32685
  `[session-git-queue] Git diff --name-only failed in ${repo.path}: ${e instanceof Error ? e.message : String(e)}`
32608
32686
  );
32609
- continue;
32687
+ return;
32610
32688
  }
32611
32689
  const lines = namesRaw.split("\n").map((l) => l.trim()).filter(Boolean);
32612
32690
  const slug = path15.basename(repo.path).replace(/[^\w.-]+/g, "_") || "repo";
32613
- for (const rel of lines) {
32614
- if (rel.includes("..")) continue;
32691
+ await forEachWithGitYield(lines, async (rel) => {
32692
+ if (rel.includes("..")) return;
32615
32693
  try {
32616
- const { stdout: patchContent } = await execFileAsync6(
32617
- "git",
32694
+ const { stdout: patchContent } = await execGitFile(
32618
32695
  ["diff", "--no-color", repo.stashSha, "--", rel],
32619
- {
32620
- cwd: repo.path,
32621
- maxBuffer: 50 * 1024 * 1024
32622
- }
32696
+ { cwd: repo.path }
32623
32697
  );
32624
- if (!patchContent.trim()) continue;
32698
+ if (!patchContent.trim()) return;
32625
32699
  const displayPath = multiRepo ? `${slug}/${rel}` : rel;
32626
32700
  const workspaceFilePath = path15.join(repo.path, rel);
32627
32701
  const newText = await readWorkspaceFileAsUtf8(workspaceFilePath);
@@ -32638,8 +32712,8 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
32638
32712
  `[session-git-queue] Git diff failed for ${rel}: ${e instanceof Error ? e.message : String(e)}`
32639
32713
  );
32640
32714
  }
32641
- }
32642
- }
32715
+ });
32716
+ });
32643
32717
  }
32644
32718
 
32645
32719
  // src/agents/acp/put-summarize-change-summaries.ts
@@ -32981,9 +33055,9 @@ __export(claude_code_acp_client_exports, {
32981
33055
  });
32982
33056
 
32983
33057
  // src/agents/acp/clients/detect-command-on-path.ts
32984
- import { execFile as execFile8 } from "node:child_process";
32985
- import { promisify as promisify8 } from "node:util";
32986
- var execFileAsync7 = promisify8(execFile8);
33058
+ import { execFile as execFile7 } from "node:child_process";
33059
+ import { promisify as promisify7 } from "node:util";
33060
+ var execFileAsync6 = promisify7(execFile7);
32987
33061
  var COMMAND_ON_PATH_PROBE_TIMEOUT_MS = 750;
32988
33062
  async function execFileShutdownAware(file2, args, timeoutMs) {
32989
33063
  if (isCliImmediateShutdownRequested()) throw new Error("shutdown");
@@ -32993,7 +33067,7 @@ async function execFileShutdownAware(file2, args, timeoutMs) {
32993
33067
  }, 50);
32994
33068
  shutdownPoll.unref?.();
32995
33069
  try {
32996
- await execFileAsync7(file2, args, { timeout: timeoutMs, signal: ac.signal });
33070
+ await execFileAsync6(file2, args, { timeout: timeoutMs, signal: ac.signal });
32997
33071
  } finally {
32998
33072
  clearInterval(shutdownPoll);
32999
33073
  }
@@ -33018,7 +33092,7 @@ async function execProbeShutdownAware(file2, args, timeoutMs) {
33018
33092
  }
33019
33093
 
33020
33094
  // src/agents/acp/clients/sdk/sdk-stdio-acp-client.ts
33021
- import { spawn as spawn2 } from "node:child_process";
33095
+ import { spawn as spawn3 } from "node:child_process";
33022
33096
  import { Readable, Writable } from "node:stream";
33023
33097
 
33024
33098
  // src/agents/acp/clients/agent-stderr-capture.ts
@@ -33613,7 +33687,7 @@ async function createSdkStdioAcpClient(options) {
33613
33687
  getActiveConfigOptions
33614
33688
  } = options;
33615
33689
  const isWindows = process.platform === "win32";
33616
- const child = spawn2(command[0], command.slice(1), {
33690
+ const child = spawn3(command[0], command.slice(1), {
33617
33691
  cwd,
33618
33692
  stdio: ["pipe", "pipe", "pipe"],
33619
33693
  env: process.env,
@@ -33826,7 +33900,7 @@ __export(cursor_acp_client_exports, {
33826
33900
  createCursorAcpClient: () => createCursorAcpClient,
33827
33901
  detectLocalAgentPresence: () => detectLocalAgentPresence3
33828
33902
  });
33829
- import { spawn as spawn3 } from "node:child_process";
33903
+ import { spawn as spawn4 } from "node:child_process";
33830
33904
  import * as readline from "node:readline";
33831
33905
 
33832
33906
  // src/agents/acp/format-session-update-kind-for-log.ts
@@ -33896,7 +33970,7 @@ async function createCursorAcpClient(options) {
33896
33970
  } = options;
33897
33971
  const dbgFs = process.env.BUILDAUTOMATON_DEBUG_ACP_FS === "1";
33898
33972
  const isWindows = process.platform === "win32";
33899
- const child = spawn3(command[0], command.slice(1), {
33973
+ const child = spawn4(command[0], command.slice(1), {
33900
33974
  cwd,
33901
33975
  stdio: ["pipe", "pipe", "pipe"],
33902
33976
  env: process.env,
@@ -35074,7 +35148,7 @@ async function ensureAcpClient(options) {
35074
35148
  if (!state.acpStartPromise) {
35075
35149
  let statOk = false;
35076
35150
  try {
35077
- const st = fs16.statSync(targetSessionParentPath);
35151
+ const st = await fs16.promises.stat(targetSessionParentPath);
35078
35152
  statOk = st.isDirectory();
35079
35153
  if (!statOk) {
35080
35154
  state.lastAcpStartError = `Agent cwd is not a directory: ${targetSessionParentPath}`;
@@ -35342,7 +35416,7 @@ import os8 from "node:os";
35342
35416
  import * as fs18 from "node:fs";
35343
35417
  import * as path21 from "node:path";
35344
35418
 
35345
- // src/git/worktree-add.ts
35419
+ // src/git/worktrees/worktree-add.ts
35346
35420
  async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch) {
35347
35421
  const mainGit = cliSimpleGit(mainRepoPath);
35348
35422
  await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
@@ -35447,7 +35521,7 @@ async function prepareNewSessionWorktrees(options) {
35447
35521
  };
35448
35522
  }
35449
35523
 
35450
- // src/git/rename-branch.ts
35524
+ // src/git/branches/rename-branch.ts
35451
35525
  async function gitRenameCurrentBranch(repoDir, newName) {
35452
35526
  const g = cliSimpleGit(repoDir);
35453
35527
  await g.raw(["branch", "-m", newName]);
@@ -35471,10 +35545,10 @@ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
35471
35545
  // src/worktrees/remove-session-worktrees.ts
35472
35546
  import * as fs21 from "node:fs";
35473
35547
 
35474
- // src/git/worktree-remove.ts
35548
+ // src/git/worktrees/worktree-remove.ts
35475
35549
  import * as fs20 from "node:fs";
35476
35550
 
35477
- // src/git/resolve-main-repo-from-git-file.ts
35551
+ // src/git/worktrees/resolve-main-repo-from-git-file.ts
35478
35552
  import * as fs19 from "node:fs";
35479
35553
  import * as path22 from "node:path";
35480
35554
  function resolveMainRepoFromWorktreeGitFile(wt) {
@@ -35488,7 +35562,7 @@ function resolveMainRepoFromWorktreeGitFile(wt) {
35488
35562
  return path22.dirname(gitDir);
35489
35563
  }
35490
35564
 
35491
- // src/git/worktree-remove.ts
35565
+ // src/git/worktrees/worktree-remove.ts
35492
35566
  async function gitWorktreeRemoveForce(worktreePath) {
35493
35567
  const mainRepo = resolveMainRepoFromWorktreeGitFile(worktreePath);
35494
35568
  if (mainRepo) {
@@ -35514,7 +35588,88 @@ async function removeSessionWorktrees(paths, log2) {
35514
35588
  }
35515
35589
  }
35516
35590
 
35517
- // src/git/working-directory/status/working-tree-status.ts
35591
+ // src/git/changes/lib/parse-git-status.ts
35592
+ function parseNameStatusLines(lines) {
35593
+ const m = /* @__PURE__ */ new Map();
35594
+ for (const line of lines) {
35595
+ if (!line.trim()) continue;
35596
+ const tabParts = line.split(" ");
35597
+ if (tabParts.length < 2) continue;
35598
+ const status = tabParts[0].trim();
35599
+ const code = status[0];
35600
+ if (code === "A") {
35601
+ m.set(tabParts[tabParts.length - 1], "added");
35602
+ } else if (code === "D") {
35603
+ m.set(tabParts[tabParts.length - 1], "removed");
35604
+ } else if (code === "R" || code === "C") {
35605
+ if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
35606
+ } else if (code === "M" || code === "U" || code === "T") {
35607
+ m.set(tabParts[tabParts.length - 1], "modified");
35608
+ }
35609
+ }
35610
+ return m;
35611
+ }
35612
+ function parseNumstatFirstLine(line) {
35613
+ const parts = line.split(" ");
35614
+ if (parts.length < 3) return null;
35615
+ const [a, d] = parts;
35616
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35617
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35618
+ return { additions, deletions };
35619
+ }
35620
+ function parseNumstat(lines) {
35621
+ const m = /* @__PURE__ */ new Map();
35622
+ for (const line of lines) {
35623
+ if (!line.trim()) continue;
35624
+ const parts = line.split(" ");
35625
+ if (parts.length < 3) continue;
35626
+ const [a, d, p] = parts;
35627
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35628
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35629
+ m.set(p, { additions, deletions });
35630
+ }
35631
+ return m;
35632
+ }
35633
+ async function numstatFromGitNoIndex(g, pathInRepo) {
35634
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35635
+ try {
35636
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
35637
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
35638
+ return parseNumstatFirstLine(first2);
35639
+ } catch {
35640
+ return null;
35641
+ }
35642
+ }
35643
+
35644
+ // src/git/changes/lib/working-tree-changed-path-count.ts
35645
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
35646
+ const kindByPath = parseNameStatusLines(nameStatusLines);
35647
+ const numByPath = parseNumstat(numstatLines);
35648
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
35649
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
35650
+ paths.add(p);
35651
+ }
35652
+ return paths.size;
35653
+ }
35654
+
35655
+ // src/git/changes/count-working-tree-changed-files.ts
35656
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
35657
+ return runGitTask(async () => {
35658
+ const g = cliSimpleGit(repoGitCwd);
35659
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
35660
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
35661
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
35662
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
35663
+ ]);
35664
+ return workingTreeChangedPathCount(
35665
+ String(nameStatusRaw).split("\n"),
35666
+ String(numstatRaw).split("\n"),
35667
+ String(untrackedRaw).split("\n")
35668
+ );
35669
+ });
35670
+ }
35671
+
35672
+ // src/git/commits/resolve-remote-tracking.ts
35518
35673
  async function tryConfigGet(g, key) {
35519
35674
  try {
35520
35675
  const out = await g.raw(["config", "--get", key]);
@@ -35597,30 +35752,6 @@ async function resolveBaseShaForUnpushedCommits(g) {
35597
35752
  if (!defaultRef) return null;
35598
35753
  return revParseSafe(g, defaultRef);
35599
35754
  }
35600
- function parseLogShaDateSubjectLines(raw) {
35601
- const out = [];
35602
- for (const line of String(raw).split("\n")) {
35603
- const l = line.trimEnd();
35604
- if (!l.trim()) continue;
35605
- const parts = l.split(" ");
35606
- if (parts.length < 3) continue;
35607
- const sha = parts[0].trim();
35608
- const committedAt = parts[1].trim();
35609
- const subject = parts.slice(2).join(" ").trim();
35610
- if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
35611
- out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
35612
- }
35613
- return out;
35614
- }
35615
- async function gitLogNotReachableFromBase(g, baseSha, headSha) {
35616
- if (baseSha === headSha) return [];
35617
- try {
35618
- const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
35619
- return parseLogShaDateSubjectLines(logOut);
35620
- } catch {
35621
- return [];
35622
- }
35623
- }
35624
35755
  async function commitsAheadOfRemoteTracking(repoDir) {
35625
35756
  const g = cliSimpleGit(repoDir);
35626
35757
  const headSha = await revParseSafe(g, "HEAD");
@@ -35635,44 +35766,41 @@ async function commitsAheadOfRemoteTracking(repoDir) {
35635
35766
  return 0;
35636
35767
  }
35637
35768
  }
35769
+
35770
+ // src/git/status/working-tree-status.ts
35638
35771
  async function getRepoWorkingTreeStatus(repoDir) {
35639
- const g = cliSimpleGit(repoDir);
35640
- const st = await g.status();
35641
- const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
35642
- const ahead = await commitsAheadOfRemoteTracking(repoDir);
35643
- return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0 };
35644
- }
35645
- async function listUnpushedCommits(repoDir) {
35646
- const g = cliSimpleGit(repoDir);
35647
- const headSha = await revParseSafe(g, "HEAD");
35648
- if (!headSha) return [];
35649
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
35650
- if (!baseSha) return [];
35651
- return gitLogNotReachableFromBase(g, baseSha, headSha);
35772
+ return runGitTask(async () => {
35773
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
35774
+ const hasUncommittedChanges = uncommittedFileCount > 0;
35775
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
35776
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
35777
+ });
35652
35778
  }
35653
35779
  async function aggregateSessionPathsWorkingTreeStatus(paths) {
35654
35780
  let hasUncommittedChanges = false;
35655
35781
  let hasUnpushedCommits = false;
35656
- for (const p of paths) {
35782
+ let uncommittedFileCount = 0;
35783
+ await forEachWithGitYield(paths, async (p) => {
35657
35784
  const s = await getRepoWorkingTreeStatus(p);
35785
+ uncommittedFileCount += s.uncommittedFileCount;
35658
35786
  if (s.hasUncommittedChanges) hasUncommittedChanges = true;
35659
35787
  if (s.hasUnpushedCommits) hasUnpushedCommits = true;
35660
- }
35661
- return { hasUncommittedChanges, hasUnpushedCommits };
35788
+ });
35789
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
35662
35790
  }
35663
35791
  async function pushAheadOfUpstreamForPaths(paths) {
35664
- for (const p of paths) {
35792
+ await forEachWithGitYield(paths, async (p) => {
35665
35793
  const g = cliSimpleGit(p);
35666
35794
  const ahead = await commitsAheadOfRemoteTracking(p);
35667
- if (ahead <= 0) continue;
35795
+ if (ahead <= 0) return;
35668
35796
  await g.push();
35669
- }
35797
+ });
35670
35798
  }
35671
35799
 
35672
- // src/git/working-directory/changes/types.ts
35800
+ // src/git/changes/types.ts
35673
35801
  var MAX_PATCH_CHARS = 35e4;
35674
35802
 
35675
- // src/git/working-directory/changes/repo-format.ts
35803
+ // src/git/changes/lib/repo-format.ts
35676
35804
  function posixJoinDirFile(dir, file2) {
35677
35805
  const d = dir === "." || dir === "" ? "" : dir.replace(/\\/g, "/").replace(/\/+$/, "");
35678
35806
  const f = file2.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -35726,63 +35854,80 @@ function formatRemoteDisplayLabel(remoteUrl) {
35726
35854
  return `origin \xB7 ${hostPath}`;
35727
35855
  }
35728
35856
 
35729
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
35857
+ // src/git/changes/get-working-tree-change-repo-details.ts
35730
35858
  import * as path24 from "node:path";
35731
35859
 
35732
- // src/git/working-directory/changes/parse-git-status.ts
35733
- function parseNameStatusLines(lines) {
35734
- const m = /* @__PURE__ */ new Map();
35735
- for (const line of lines) {
35736
- if (!line.trim()) continue;
35737
- const tabParts = line.split(" ");
35738
- if (tabParts.length < 2) continue;
35739
- const status = tabParts[0].trim();
35740
- const code = status[0];
35741
- if (code === "A") {
35742
- m.set(tabParts[tabParts.length - 1], "added");
35743
- } else if (code === "D") {
35744
- m.set(tabParts[tabParts.length - 1], "removed");
35745
- } else if (code === "R" || code === "C") {
35746
- if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
35747
- } else if (code === "M" || code === "U" || code === "T") {
35748
- m.set(tabParts[tabParts.length - 1], "modified");
35749
- }
35860
+ // src/git/commits/lib/parse-log-lines.ts
35861
+ function parseLogShaDateSubjectLines(raw) {
35862
+ const out = [];
35863
+ for (const line of String(raw).split("\n")) {
35864
+ const l = line.trimEnd();
35865
+ if (!l.trim()) continue;
35866
+ const parts = l.split(" ");
35867
+ if (parts.length < 3) continue;
35868
+ const sha = parts[0].trim();
35869
+ const committedAt = parts[1].trim();
35870
+ const subject = parts.slice(2).join(" ").trim();
35871
+ if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
35872
+ out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
35750
35873
  }
35751
- return m;
35752
- }
35753
- function parseNumstatFirstLine(line) {
35754
- const parts = line.split(" ");
35755
- if (parts.length < 3) return null;
35756
- const [a, d] = parts;
35757
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35758
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35759
- return { additions, deletions };
35874
+ return out;
35760
35875
  }
35761
- function parseNumstat(lines) {
35762
- const m = /* @__PURE__ */ new Map();
35763
- for (const line of lines) {
35764
- if (!line.trim()) continue;
35765
- const parts = line.split(" ");
35766
- if (parts.length < 3) continue;
35767
- const [a, d, p] = parts;
35768
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35769
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35770
- m.set(p, { additions, deletions });
35876
+
35877
+ // src/git/commits/list-unpushed-commits.ts
35878
+ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
35879
+ if (baseSha === headSha) return [];
35880
+ try {
35881
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
35882
+ return parseLogShaDateSubjectLines(logOut);
35883
+ } catch {
35884
+ return [];
35771
35885
  }
35772
- return m;
35773
35886
  }
35774
- async function numstatFromGitNoIndex(g, pathInRepo) {
35775
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35887
+ async function listUnpushedCommits(repoDir) {
35888
+ const g = cliSimpleGit(repoDir);
35889
+ const headSha = await revParseSafe(g, "HEAD");
35890
+ if (!headSha) return [];
35891
+ const baseSha = await resolveBaseShaForUnpushedCommits(g);
35892
+ if (!baseSha) return [];
35893
+ return gitLogNotReachableFromBase(g, baseSha, headSha);
35894
+ }
35895
+
35896
+ // src/git/commits/lib/sanitize-recent-commits-limit.ts
35897
+ var RECENT_COMMITS_LIMIT_MIN = 1;
35898
+ var RECENT_COMMITS_LIMIT_MAX = 50;
35899
+ var RECENT_COMMITS_LIMIT_DEFAULT = 2;
35900
+ function sanitizeRecentCommitsLimit(value) {
35901
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : Number.NaN;
35902
+ if (!Number.isFinite(n)) return RECENT_COMMITS_LIMIT_DEFAULT;
35903
+ return Math.min(RECENT_COMMITS_LIMIT_MAX, Math.max(RECENT_COMMITS_LIMIT_MIN, Math.trunc(n)));
35904
+ }
35905
+
35906
+ // src/git/commits/list-recent-commits.ts
35907
+ async function listRecentCommits(repoDir, limitInput) {
35908
+ const limit = sanitizeRecentCommitsLimit(limitInput);
35909
+ const g = cliSimpleGit(repoDir);
35910
+ const headSha = await revParseSafe(g, "HEAD");
35911
+ if (!headSha) return { commits: [], hasMore: false };
35912
+ const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
35776
35913
  try {
35777
- const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
35778
- const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
35779
- return parseNumstatFirstLine(first2);
35914
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
35915
+ const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
35916
+ ...c,
35917
+ needsPush: unpushedSet.has(c.sha)
35918
+ }));
35919
+ let hasMore = false;
35920
+ if (commits.length === limit) {
35921
+ const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
35922
+ hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
35923
+ }
35924
+ return { commits, hasMore };
35780
35925
  } catch {
35781
- return null;
35926
+ return { commits: [], hasMore: false };
35782
35927
  }
35783
35928
  }
35784
35929
 
35785
- // src/git/working-directory/changes/patch-truncate.ts
35930
+ // src/git/changes/lib/patch-truncate.ts
35786
35931
  function truncatePatch(s) {
35787
35932
  if (s.length <= MAX_PATCH_CHARS) return s;
35788
35933
  return `${s.slice(0, MAX_PATCH_CHARS)}
@@ -35790,7 +35935,7 @@ function truncatePatch(s) {
35790
35935
  \u2026 (diff truncated)`;
35791
35936
  }
35792
35937
 
35793
- // src/git/working-directory/changes/list-changed-files-for-commit.ts
35938
+ // src/git/commits/list-changed-files-for-commit.ts
35794
35939
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
35795
35940
  async function parentForCommitDiff(g, sha) {
35796
35941
  try {
@@ -35816,7 +35961,7 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35816
35961
  const paths = new Set([...kindByPath.keys(), ...numByPath.keys()].filter(Boolean));
35817
35962
  const rows = [];
35818
35963
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
35819
- for (const pathInRepo of paths) {
35964
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
35820
35965
  const relLauncher = posixJoinDirFile(normRel, pathInRepo.replace(/\\/g, "/"));
35821
35966
  const nums = numByPath.get(pathInRepo);
35822
35967
  let additions = nums?.additions ?? 0;
@@ -35828,8 +35973,8 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35828
35973
  else change = "modified";
35829
35974
  }
35830
35975
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
35831
- }
35832
- for (const row of rows) {
35976
+ });
35977
+ await forEachWithGitYield(rows, async (row) => {
35833
35978
  let pathInRepo;
35834
35979
  if (normRel === ".") {
35835
35980
  pathInRepo = row.pathRelLauncher;
@@ -35841,16 +35986,16 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35841
35986
  const raw = await g.raw(["diff", "-U20000", range, "--", pathInRepo]).catch(() => "");
35842
35987
  const t = String(raw).trim();
35843
35988
  row.patchContent = t ? truncatePatch(t) : void 0;
35844
- }
35989
+ });
35845
35990
  rows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
35846
35991
  return rows;
35847
35992
  }
35848
35993
 
35849
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
35994
+ // src/git/changes/list-changed-files-for-repo.ts
35850
35995
  import * as fs23 from "node:fs";
35851
35996
  import * as path23 from "node:path";
35852
35997
 
35853
- // src/git/working-directory/changes/count-lines.ts
35998
+ // src/git/changes/lib/count-lines.ts
35854
35999
  import { createReadStream } from "node:fs";
35855
36000
  import * as readline2 from "node:readline";
35856
36001
  async function countTextFileLines(filePath) {
@@ -35871,7 +36016,7 @@ async function countTextFileLines(filePath) {
35871
36016
  return lines;
35872
36017
  }
35873
36018
 
35874
- // src/git/working-directory/changes/hydrate-patch.ts
36019
+ // src/git/changes/hydrate-patch.ts
35875
36020
  import * as fs22 from "node:fs";
35876
36021
  var UNIFIED_HUNK_HEADER_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
35877
36022
  var MAX_HYDRATE_LINES_PER_GAP = 8e3;
@@ -35987,26 +36132,16 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
35987
36132
  return truncatePatch(out.join("\n"));
35988
36133
  }
35989
36134
 
35990
- // src/git/working-directory/changes/unified-diff-for-file.ts
36135
+ // src/git/changes/unified-diff-for-file.ts
35991
36136
  async function unifiedDiffForFile(repoCwd, pathInRepo, change) {
35992
36137
  const g = cliSimpleGit(repoCwd);
35993
- try {
35994
- let raw;
35995
- if (change === "added") {
35996
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35997
- raw = await g.raw(["diff", "--no-index", "--", devNull, pathInRepo]);
35998
- } else {
35999
- raw = await g.raw(["diff", "HEAD", "--", pathInRepo]);
36000
- }
36001
- const t = String(raw).trim();
36002
- if (!t) return void 0;
36003
- return truncatePatch(t);
36004
- } catch {
36005
- return void 0;
36006
- }
36138
+ const args = change === "added" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : change === "removed" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : ["diff", "--no-color", "HEAD", "--", pathInRepo];
36139
+ const raw = await g.raw([...args]).catch(() => "");
36140
+ const t = String(raw).trim();
36141
+ return t ? truncatePatch(t) : void 0;
36007
36142
  }
36008
36143
 
36009
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
36144
+ // src/git/changes/list-changed-files-for-repo.ts
36010
36145
  async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36011
36146
  const g = cliSimpleGit(repoGitCwd);
36012
36147
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
@@ -36020,7 +36155,7 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36020
36155
  const untracked = String(untrackedRaw).split("\n").map((s) => s.trim()).filter(Boolean);
36021
36156
  for (const p of untracked) paths.add(p);
36022
36157
  const rows = [];
36023
- for (const pathInRepo of paths) {
36158
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
36024
36159
  const relLauncher = posixJoinDirFile(repoRelPath, pathInRepo.replace(/\\/g, "/"));
36025
36160
  const repoFilePath = path23.join(repoGitCwd, pathInRepo);
36026
36161
  const nums = numByPath.get(pathInRepo);
@@ -36050,9 +36185,9 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36050
36185
  else change = "modified";
36051
36186
  }
36052
36187
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
36053
- }
36188
+ });
36054
36189
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
36055
- for (const row of rows) {
36190
+ await forEachWithGitYield(rows, async (row) => {
36056
36191
  let pathInRepo;
36057
36192
  if (normRel === ".") {
36058
36193
  pathInRepo = row.pathRelLauncher;
@@ -36067,11 +36202,11 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36067
36202
  patch = await hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, pathInRepo, row.change);
36068
36203
  }
36069
36204
  row.patchContent = patch;
36070
- }
36205
+ });
36071
36206
  return rows;
36072
36207
  }
36073
36208
 
36074
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
36209
+ // src/git/changes/get-working-tree-change-repo-details.ts
36075
36210
  function normRepoRel(p) {
36076
36211
  const x = p.replace(/\\/g, "/").trim();
36077
36212
  return x === "" ? "." : x;
@@ -36090,7 +36225,9 @@ async function getWorkingTreeChangeRepoDetails(options) {
36090
36225
  throw new Error("commit sha is required for commit changes");
36091
36226
  }
36092
36227
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
36093
- for (const target of options.commitTargetPaths) {
36228
+ for (let i = 0; i < options.commitTargetPaths.length; i++) {
36229
+ if (i > 0) await yieldToEventLoop2();
36230
+ const target = options.commitTargetPaths[i];
36094
36231
  const t = path24.resolve(target);
36095
36232
  if (!await isGitRepoDirectory(t)) continue;
36096
36233
  const g = cliSimpleGit(t);
@@ -36124,7 +36261,10 @@ async function getWorkingTreeChangeRepoDetails(options) {
36124
36261
  const files = basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
36125
36262
  const st = await g.status();
36126
36263
  const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
36127
- const unpushedCommits = await listUnpushedCommits(t);
36264
+ const [unpushedCommits, recentCommitList] = await Promise.all([
36265
+ listUnpushedCommits(t),
36266
+ listRecentCommits(t, options.recentCommitsLimit)
36267
+ ]);
36128
36268
  out.push({
36129
36269
  repoRelPath: norm,
36130
36270
  repoDisplayName,
@@ -36134,6 +36274,8 @@ async function getWorkingTreeChangeRepoDetails(options) {
36134
36274
  files,
36135
36275
  hasUncommittedChanges,
36136
36276
  unpushedCommits,
36277
+ recentCommits: recentCommitList.commits,
36278
+ recentCommitsHasMore: recentCommitList.hasMore,
36137
36279
  changesView: basis.kind === "commit" ? "commit" : "working",
36138
36280
  changesCommitSha: basis.kind === "commit" ? basis.sha.trim() : null
36139
36281
  });
@@ -36142,7 +36284,7 @@ async function getWorkingTreeChangeRepoDetails(options) {
36142
36284
  return out;
36143
36285
  }
36144
36286
 
36145
- // src/git/commit-and-push.ts
36287
+ // src/git/branches/commit-and-push.ts
36146
36288
  async function gitCommitAllIfDirty(repoDir, message, options) {
36147
36289
  const g = cliSimpleGit(repoDir);
36148
36290
  const st = await g.status();
@@ -36566,7 +36708,8 @@ var SessionWorktreeManager = class {
36566
36708
  sessionWorktreeRootPath: sessionWorkingTreeRelRoot,
36567
36709
  legacyRepoNestedSessionLayout: legacyNested,
36568
36710
  repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
36569
- basis: opts?.basis
36711
+ basis: opts?.basis,
36712
+ recentCommitsLimit: opts?.recentCommitsLimit
36570
36713
  });
36571
36714
  }
36572
36715
  async pushSessionUpstream(sessionId) {
@@ -37096,7 +37239,7 @@ function pipedStdoutStderrFor(attemptStdio) {
37096
37239
  }
37097
37240
 
37098
37241
  // src/dev-servers/manager/shell-spawn/try-spawn-piped-via-sh.ts
37099
- import { spawn as spawn4 } from "node:child_process";
37242
+ import { spawn as spawn5 } from "node:child_process";
37100
37243
  function trySpawnPipedViaSh(command, env, cwd, signal) {
37101
37244
  const attempts = [
37102
37245
  { stdio: [devNullReadFd(), "pipe", "pipe"], endStdin: false },
@@ -37117,9 +37260,9 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
37117
37260
  if (process.platform === "win32") {
37118
37261
  opts.windowsHide = true;
37119
37262
  const com = process.env.ComSpec || "cmd.exe";
37120
- proc = spawn4(com, ["/d", "/s", "/c", command], opts);
37263
+ proc = spawn5(com, ["/d", "/s", "/c", command], opts);
37121
37264
  } else {
37122
- proc = spawn4("/bin/sh", ["-c", command], opts);
37265
+ proc = spawn5("/bin/sh", ["-c", command], opts);
37123
37266
  }
37124
37267
  if (attempt.endStdin) {
37125
37268
  proc.stdin?.end();
@@ -37139,7 +37282,7 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
37139
37282
  }
37140
37283
 
37141
37284
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-true-piped.ts
37142
- import { spawn as spawn5 } from "node:child_process";
37285
+ import { spawn as spawn6 } from "node:child_process";
37143
37286
  function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37144
37287
  try {
37145
37288
  const opts = {
@@ -37152,7 +37295,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37152
37295
  if (process.platform === "win32") {
37153
37296
  opts.windowsHide = true;
37154
37297
  }
37155
- return spawn5(command, opts);
37298
+ return spawn6(command, opts);
37156
37299
  } catch (e) {
37157
37300
  if (isSpawnEbadf(e)) return null;
37158
37301
  throw e;
@@ -37160,7 +37303,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37160
37303
  }
37161
37304
 
37162
37305
  // src/dev-servers/manager/shell-spawn/try-spawn-merged-log-file.ts
37163
- import { spawn as spawn6 } from "node:child_process";
37306
+ import { spawn as spawn7 } from "node:child_process";
37164
37307
  import fs29 from "node:fs";
37165
37308
  import { tmpdir } from "node:os";
37166
37309
  import path32 from "node:path";
@@ -37178,7 +37321,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37178
37321
  try {
37179
37322
  let proc;
37180
37323
  if (process.platform === "win32") {
37181
- proc = spawn6(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
37324
+ proc = spawn7(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
37182
37325
  env,
37183
37326
  cwd,
37184
37327
  stdio,
@@ -37186,7 +37329,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37186
37329
  ...signal ? { signal } : {}
37187
37330
  });
37188
37331
  } else {
37189
- proc = spawn6("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
37332
+ proc = spawn7("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
37190
37333
  }
37191
37334
  fs29.closeSync(logFd);
37192
37335
  return {
@@ -37207,7 +37350,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37207
37350
  }
37208
37351
 
37209
37352
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-script-log-redirect.ts
37210
- import { spawn as spawn7 } from "node:child_process";
37353
+ import { spawn as spawn8 } from "node:child_process";
37211
37354
  import fs30 from "node:fs";
37212
37355
  import { tmpdir as tmpdir2 } from "node:os";
37213
37356
  import path33 from "node:path";
@@ -37230,7 +37373,7 @@ cd ${shSingleQuote(cwd)}
37230
37373
  /bin/sh ${shSingleQuote(innerPath)} >>${shSingleQuote(logPath)} 2>&1
37231
37374
  `
37232
37375
  );
37233
- const proc = spawn7("/bin/sh", [runnerPath], {
37376
+ const proc = spawn8("/bin/sh", [runnerPath], {
37234
37377
  env,
37235
37378
  cwd: tmpRoot,
37236
37379
  stdio: "ignore",
@@ -37262,7 +37405,7 @@ CD /D ${q(cwd)}\r
37262
37405
  ${command} >> ${q(logPath)} 2>&1\r
37263
37406
  `
37264
37407
  );
37265
- const proc = spawn7(com, ["/d", "/s", "/c", q(runnerPath)], {
37408
+ const proc = spawn8(com, ["/d", "/s", "/c", q(runnerPath)], {
37266
37409
  env,
37267
37410
  cwd: tmpRoot,
37268
37411
  stdio: "ignore",
@@ -37283,7 +37426,7 @@ ${command} >> ${q(logPath)} 2>&1\r
37283
37426
  }
37284
37427
 
37285
37428
  // src/dev-servers/manager/shell-spawn/try-spawn-inherit.ts
37286
- import { spawn as spawn8 } from "node:child_process";
37429
+ import { spawn as spawn9 } from "node:child_process";
37287
37430
  function trySpawnInheritStdio(command, env, cwd, signal) {
37288
37431
  const opts = {
37289
37432
  env,
@@ -37295,9 +37438,9 @@ function trySpawnInheritStdio(command, env, cwd, signal) {
37295
37438
  if (process.platform === "win32") {
37296
37439
  opts.windowsHide = true;
37297
37440
  const com = process.env.ComSpec || "cmd.exe";
37298
- proc = spawn8(com, ["/d", "/s", "/c", command], opts);
37441
+ proc = spawn9(com, ["/d", "/s", "/c", command], opts);
37299
37442
  } else {
37300
- proc = spawn8("/bin/sh", ["-c", command], opts);
37443
+ proc = spawn9("/bin/sh", ["-c", command], opts);
37301
37444
  }
37302
37445
  return { proc, pipedStdoutStderr: false };
37303
37446
  }
@@ -38027,11 +38170,13 @@ function connectFirehose(options) {
38027
38170
  if (Buffer.isBuffer(raw) && tryConsumeBinaryProxyBody(raw, deps)) {
38028
38171
  return;
38029
38172
  }
38030
- try {
38031
- const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
38032
- dispatchFirehoseJsonMessage(JSON.parse(text), deps);
38033
- } catch {
38034
- }
38173
+ setImmediate(() => {
38174
+ try {
38175
+ const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
38176
+ dispatchFirehoseJsonMessage(JSON.parse(text), deps);
38177
+ } catch {
38178
+ }
38179
+ });
38035
38180
  });
38036
38181
  ws.on("close", (code, reason) => {
38037
38182
  disposeClientPing();
@@ -38547,7 +38692,9 @@ async function runLocalRevertBeforeQueuedPrompt(next, deps) {
38547
38692
  const tid = typeof pl.snapshotRevertTurnId === "string" && pl.snapshotRevertTurnId.trim() !== "" ? pl.snapshotRevertTurnId.trim() : next.turnId;
38548
38693
  const agentBase = deps.sessionWorktreeManager.getSessionWorktreeRootForSession(sid) ?? getBridgeRoot();
38549
38694
  const file2 = snapshotFilePath(agentBase, tid);
38550
- if (!fs32.existsSync(file2)) {
38695
+ try {
38696
+ await fs32.promises.access(file2, fs32.constants.F_OK);
38697
+ } catch {
38551
38698
  deps.log(
38552
38699
  `[Queue] requeued_with_revert: no pre-turn snapshot for ${tid.slice(0, 8)}\u2026; continuing without revert.`
38553
38700
  );
@@ -38721,12 +38868,12 @@ function createBridgePromptSenders(deps, getWs) {
38721
38868
  }
38722
38869
 
38723
38870
  // src/agents/acp/from-bridge/bridge-prompt-preamble.ts
38724
- import { execFile as execFile9 } from "node:child_process";
38725
- import { promisify as promisify9 } from "node:util";
38726
- var execFileAsync8 = promisify9(execFile9);
38871
+ import { execFile as execFile8 } from "node:child_process";
38872
+ import { promisify as promisify8 } from "node:util";
38873
+ var execFileAsync7 = promisify8(execFile8);
38727
38874
  async function readGitBranch(cwd) {
38728
38875
  try {
38729
- const { stdout } = await execFileAsync8("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
38876
+ const { stdout } = await execFileAsync7("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
38730
38877
  const b = stdout.trim();
38731
38878
  return b || null;
38732
38879
  } catch {
@@ -38974,7 +39121,7 @@ var handleSessionRequestResponseMessage = (msg, deps) => {
38974
39121
  };
38975
39122
 
38976
39123
  // src/skills/preview.ts
38977
- import { spawn as spawn9 } from "node:child_process";
39124
+ import { spawn as spawn10 } from "node:child_process";
38978
39125
  var PREVIEW_API_BASE_PATH = "/__preview";
38979
39126
  var PREVIEW_SECRET_HEADER = "X-Preview-Secret";
38980
39127
  var DEFAULT_PORT = 3e3;
@@ -39053,7 +39200,7 @@ var previewSkill = {
39053
39200
  const parts = command.split(/\s+/);
39054
39201
  const exe = parts[0];
39055
39202
  const args = parts.slice(1);
39056
- previewProcess = spawn9(isWindows && exe === "npm" ? "npm.cmd" : exe, args, {
39203
+ previewProcess = spawn10(isWindows && exe === "npm" ? "npm.cmd" : exe, args, {
39057
39204
  cwd: getBridgeRoot(),
39058
39205
  stdio: ["ignore", "pipe", "pipe"],
39059
39206
  env: {
@@ -39610,7 +39757,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39610
39757
  reply({
39611
39758
  ok: true,
39612
39759
  hasUncommittedChanges: r.hasUncommittedChanges,
39613
- hasUnpushedCommits: r.hasUnpushedCommits
39760
+ hasUnpushedCommits: r.hasUnpushedCommits,
39761
+ uncommittedFileCount: r.uncommittedFileCount
39614
39762
  });
39615
39763
  return;
39616
39764
  }
@@ -39624,7 +39772,12 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39624
39772
  return;
39625
39773
  }
39626
39774
  }
39627
- const opts = repoRel && view === "commit" && commitSha ? { repoRelPath: repoRel, basis: { kind: "commit", sha: commitSha } } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" } } : void 0;
39775
+ const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
39776
+ const opts = repoRel && view === "commit" && commitSha ? {
39777
+ repoRelPath: repoRel,
39778
+ basis: { kind: "commit", sha: commitSha },
39779
+ recentCommitsLimit
39780
+ } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
39628
39781
  const repos = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts);
39629
39782
  reply({
39630
39783
  ok: true,
@@ -39642,7 +39795,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39642
39795
  reply({
39643
39796
  ok: true,
39644
39797
  hasUncommittedChanges: st2.hasUncommittedChanges,
39645
- hasUnpushedCommits: st2.hasUnpushedCommits
39798
+ hasUnpushedCommits: st2.hasUnpushedCommits,
39799
+ uncommittedFileCount: st2.uncommittedFileCount
39646
39800
  });
39647
39801
  return;
39648
39802
  }
@@ -39667,7 +39821,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39667
39821
  reply({
39668
39822
  ok: true,
39669
39823
  hasUncommittedChanges: st.hasUncommittedChanges,
39670
- hasUnpushedCommits: st.hasUnpushedCommits
39824
+ hasUnpushedCommits: st.hasUnpushedCommits,
39825
+ uncommittedFileCount: st.uncommittedFileCount
39671
39826
  });
39672
39827
  } catch (e) {
39673
39828
  reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -39710,7 +39865,9 @@ var handleRevertTurnSnapshotMessage = (msg, deps) => {
39710
39865
  if (!s) return;
39711
39866
  const agentBase = sessionWorktreeManager.getSessionWorktreeRootForSession(sessionId) ?? getBridgeRoot();
39712
39867
  const file2 = snapshotFilePath(agentBase, turnId);
39713
- if (!fs36.existsSync(file2)) {
39868
+ try {
39869
+ await fs36.promises.access(file2, fs36.constants.F_OK);
39870
+ } catch {
39714
39871
  sendWsMessage(s, {
39715
39872
  type: "revert_turn_snapshot_result",
39716
39873
  id,
@@ -39829,9 +39986,7 @@ function handleBridgeMessage(data, deps) {
39829
39986
  if (!deps.getWs()) return;
39830
39987
  const msg = parseApiToBridgeMessage(normalizeInboundBridgeWebSocketJson(data), deps.log);
39831
39988
  if (!msg) return;
39832
- setImmediate(() => {
39833
- dispatchBridgeMessage(msg, deps);
39834
- });
39989
+ dispatchBridgeMessage(msg, deps);
39835
39990
  }
39836
39991
 
39837
39992
  // src/auth/refresh-bridge-tokens.ts
@@ -40397,25 +40552,27 @@ async function createBridgeConnection(options) {
40397
40552
  }
40398
40553
  function sendAgentCapabilitiesToBridge(info) {
40399
40554
  if (!Array.isArray(info.configOptions) || info.configOptions.length === 0) return;
40400
- let changed = false;
40401
- try {
40402
- changed = withCliSqliteSync(
40403
- (db) => upsertCliAgentCapabilityCache(db, {
40404
- workspaceId,
40405
- agentType: info.agentType,
40406
- configOptions: info.configOptions
40407
- })
40408
- );
40409
- } catch (e) {
40410
- if (e instanceof CliSqliteInterrupted) return;
40411
- }
40412
- if (!changed) return;
40413
- const socket = getWs();
40414
- if (!socket || socket.readyState !== wrapper_default.OPEN) return;
40415
- sendWsMessage(socket, {
40416
- type: "agent_capabilities",
40417
- agentType: info.agentType,
40418
- configOptions: info.configOptions
40555
+ setImmediate(() => {
40556
+ let changed = false;
40557
+ try {
40558
+ changed = withCliSqliteSync(
40559
+ (db) => upsertCliAgentCapabilityCache(db, {
40560
+ workspaceId,
40561
+ agentType: info.agentType,
40562
+ configOptions: info.configOptions
40563
+ })
40564
+ );
40565
+ } catch (e) {
40566
+ if (e instanceof CliSqliteInterrupted) return;
40567
+ }
40568
+ if (!changed) return;
40569
+ const socket = getWs();
40570
+ if (!socket || socket.readyState !== wrapper_default.OPEN) return;
40571
+ sendWsMessage(socket, {
40572
+ type: "agent_capabilities",
40573
+ agentType: info.agentType,
40574
+ configOptions: info.configOptions
40575
+ });
40419
40576
  });
40420
40577
  }
40421
40578
  const worktreesRootPath = options.worktreesRootPath ?? defaultWorktreesRootPath();
@@ -40567,6 +40724,7 @@ async function runConnectedBridge(options, restartWithoutAuth) {
40567
40724
  let bridgeClose = null;
40568
40725
  const onSignal = (kind) => {
40569
40726
  requestCliImmediateShutdown();
40727
+ abortActiveGitChildProcesses();
40570
40728
  cleanupKeyCommand?.();
40571
40729
  logImmediate(
40572
40730
  kind === "interrupt" ? "Keyboard interrupt (Ctrl+C) \u2014 stopping\u2026" : "Stop requested \u2014 shutting down\u2026"