zapdev 1.17.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -5
  2. package/dist/cli.js +121 -290
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -114,7 +114,7 @@ Stages all changes, scans them with Gitleaks when installed, generates Conventio
114
114
  - **Unified review:** generates messages in parallel, then presents every message with its repository name and branch. Repositories with pending changes are shown in bold when terminal styling is enabled.
115
115
  - **Actions:** commit all, commit only a named repository, edit a named repository's message, or cancel. Editing returns to the review menu.
116
116
 
117
- Only repositories successfully committed during the current run are offered for push or pushed with `--push`. Unselected or cancelled changes remain staged; failed or unselected commit drafts are not pushed. The branch is chosen before committing, never when pushing. Push confirmation is asked once, only for repositories with a configured upstream remote; it is omitted when none have one.
117
+ Only repositories successfully committed during the current run are offered for push or pushed with `--push`. Unselected or cancelled changes remain staged; failed or unselected commit drafts are not pushed. The branch is chosen before committing, never when pushing. Push confirmation is asked once, only for repositories with a resolved push remote; it is omitted when none have one.
118
118
 
119
119
  ```bash
120
120
  zapdev commit
@@ -139,17 +139,17 @@ zapdev commit -yp # skip review and push confirmation; choose a branch
139
139
  zapdev commit --pull # pull first, then follow the normal commit flow
140
140
  ```
141
141
 
142
- Before contacting the LLM endpoint, zapdev runs `gitleaks git --staged --verbose` in each changed repository when Gitleaks is installed. A failed scan skips that repository without sending its diff; when Gitleaks is absent, the scan is skipped. The staged diff is sent to the configured endpoint, which may be remote.
142
+ Before contacting the LLM endpoint, zapdev runs `gitleaks git --staged --verbose` in each changed repository when Gitleaks is installed. A failed scan never sends that repository's diff to the LLM and stops the flow before any commit; when Gitleaks is absent, the scan is skipped. The staged diff is sent to the configured endpoint, which may be remote.
143
143
 
144
- Preparation and commit failures are reported per repository while the others continue. A push failure stops all remaining sends. Any failure produces a nonzero exit code.
144
+ Any error stops the flow with a nonzero exit code. Parallel work already in flight finishes before exiting; later steps never start. Earlier successful pulls or commits remain applied.
145
145
 
146
146
  With `--pull`, only repositories with local changes are pulled, in parallel. With `--staged`, only repositories with a staged diff are pulled. All pulls must finish successfully before preparation and message generation start in parallel. Git uses each current branch's configured upstream. Repositories without a configured upstream remote are skipped with a warning, even if they have a single remote. Divergent history, a missing upstream branch, or conflicting local changes stop the flow before staging, after all in-flight pulls finish; no rebase, merge commit, or automatic stash is performed. Successful pulls remain applied.
147
147
 
148
- In a terminal, commit, pull, and push output is streamed into Clack task logs; parallel pulls share one log grouped by repository. Silent commits and pushes create no output block. Git keeps terminal input, but stdout/stderr are pipes: hooks requiring a TTY on those streams are not supported. Git controls hook stdin; hooks can read from `/dev/tty`, although prompts written there appear outside the blocks. `--yes` skips commit review, not native prompts. Without a terminal, stdout/stderr are streamed directly. Each hook has a 60-second deadline, including time spent answering prompts; override it with `ZD_HOOK_TIMEOUT=120 zapdev commit`.
148
+ Each step uses one spinner: start, wait, stop. Normal Git and hook output is captured without live task logs; failure diagnostics are displayed once. Without a terminal, steps print plain progress messages instead. Git keeps terminal input, but interactive hooks must read and write through `/dev/tty`; prompts on captured stdout/stderr are not displayed. Each hook retains a 60-second deadline; override it with `ZD_HOOK_TIMEOUT=120 zapdev commit`. `--yes` skips commit review, not native prompts.
149
149
 
150
150
  Immediately before each selected commit, zapdev asks for a local branch only when the current name exactly matches `main`, `master`, `principal`, `dev`, or `development`, including with `--yes`. The first prompt starts empty; subsequent prompts reuse the previous input across repositories. Press Enter to accept the prefilled name; an empty or cleared input keeps the current branch and leaves the next prompt empty. A different name switches to the existing local branch or creates it from HEAD, without forcing or discarding pending changes. Other branches and repositories with nothing to commit never trigger this prompt. Cancelling stops the remaining commits and all pushes; earlier commits remain local.
151
151
 
152
- Pushes always target a remote branch with the current local branch's name, with no destination input. They use only the current branch's configured upstream remote and run `git push <remote> HEAD:refs/heads/<current-branch>`. Repositories without a configured upstream remote are skipped with a warning, even if they have a single remote. Pushes run directly, without a preliminary fetch or history comparison; Git handles up-to-date branches and rejects non-fast-forward updates. No rebase, merge, retry, or force-push.
152
+ Pushes always target a remote branch with the current local branch's name, with no destination input. The remote is the branch's configured upstream remote, otherwise `origin`, otherwise the only configured remote. With no remote or multiple remotes and no `origin`, push is skipped with a warning. The first successful push sets the upstream when none exists; existing upstreams are preserved. Pushes run directly, without a preliminary fetch or history comparison; Git handles up-to-date branches and rejects non-fast-forward updates. No rebase, merge, retry, or force-push.
153
153
 
154
154
  Without a TTY, zapdev commits prepared repositories on nonprotected branches automatically; protected-branch commits are refused and changes remain staged. `--push` can publish without a TTY; without it, commits stay local. Repositories without new changes do not trigger an LLM call, branch prompt, or new commit.
155
155
 
package/dist/cli.js CHANGED
@@ -830,7 +830,6 @@ function isUnicodeSupported() {
830
830
  }
831
831
  var unicode = isUnicodeSupported();
832
832
  var isCI = () => process.env.CI === "true";
833
- var isTTY = (o2) => o2.isTTY === true;
834
833
  var unicodeOr = (o2, e2) => unicode ? o2 : e2;
835
834
  var S_STEP_ACTIVE = unicodeOr("\u25C6", "*");
836
835
  var S_STEP_CANCEL = unicodeOr("\u25A0", "x");
@@ -1076,7 +1075,7 @@ var spinner = ({
1076
1075
  const x2 = getColumns(n4), k2 = I3?.styleFrame ?? W, g2 = (e2) => {
1077
1076
  const r3 = e2 > 1 ? O2 ?? settings.messages.error : G ?? settings.messages.cancel;
1078
1077
  S2 = e2 === 1, d2 && (a3(r3, e2), S2 && typeof h3 == "function" && h3());
1079
- }, f2 = () => g2(2), i3 = () => g2(1), A2 = () => {
1078
+ }, f2 = () => g2(2), i3 = () => g2(1), A = () => {
1080
1079
  process.on("uncaughtExceptionMonitor", f2), process.on("unhandledRejection", f2), process.on("SIGINT", i3), process.on("SIGTERM", i3), process.on("exit", g2), m2 && m2.addEventListener("abort", i3);
1081
1080
  }, H = () => {
1082
1081
  process.removeListener("uncaughtExceptionMonitor", f2), process.removeListener("unhandledRejection", f2), process.removeListener("SIGINT", i3), process.removeListener("SIGTERM", i3), process.removeListener("exit", g2), m2 && m2.removeEventListener("abort", i3);
@@ -1097,7 +1096,7 @@ var spinner = ({
1097
1096
  d2 = true, M = block({ output: n4 }), s2 = C2(e2), w2 = performance.now(), N2 && n4.write(`${styleText2("gray", S_BAR)}
1098
1097
  `);
1099
1098
  let r3 = 0, t3 = 0;
1100
- A2(), T2 = setInterval(() => {
1099
+ A(), T2 = setInterval(() => {
1101
1100
  if (u4 && s2 === p2)
1102
1101
  return;
1103
1102
  y2(), p2 = s2;
@@ -1225,123 +1224,6 @@ ${b2}
1225
1224
  }).prompt();
1226
1225
  };
1227
1226
  var i = `${styleText2("gray", S_BAR)} `;
1228
- var A = (l3) => l3.replace(/\x1b\[(?:\d+;)*\d*[ABCDEFGHfJKSTsu]|\x1b\[(s|u)/g, "");
1229
- var taskLog = (l3) => {
1230
- const r3 = l3.output ?? process.stdout, O2 = getColumns(r3), i3 = styleText2("gray", S_BAR), p2 = l3.spacing ?? 1, k2 = 3, m2 = l3.retainLog === true, d2 = !isCI() && isTTY(r3);
1231
- r3.write(`${i3}
1232
- `), r3.write(`${styleText2("green", S_STEP_SUBMIT)} ${l3.title}
1233
- `);
1234
- for (let e2 = 0; e2 < p2; e2++)
1235
- r3.write(`${i3}
1236
- `);
1237
- const n4 = [
1238
- {
1239
- value: "",
1240
- full: ""
1241
- }
1242
- ];
1243
- let v2 = false;
1244
- const f2 = (e2) => {
1245
- if (n4.length === 0)
1246
- return;
1247
- let s2 = 0;
1248
- e2 && (s2 += p2 + 2);
1249
- for (const t3 of n4) {
1250
- const { value: o2, result: a3 } = t3;
1251
- let g2 = a3?.message ?? o2;
1252
- if (g2.length === 0)
1253
- continue;
1254
- a3 === void 0 && t3.header !== void 0 && t3.header !== "" && (g2 += `
1255
- ${t3.header}`);
1256
- const E2 = g2.split(`
1257
- `).reduce((b2, w2) => w2 === "" ? b2 + 1 : b2 + Math.ceil((w2.length + k2) / O2), 0);
1258
- s2 += E2;
1259
- }
1260
- s2 > 0 && (s2 += 1, r3.write(import_sisteransi2.erase.lines(s2)));
1261
- }, h3 = (e2, s2, t3) => {
1262
- const o2 = t3 ? `${e2.full}
1263
- ${e2.value}` : e2.value;
1264
- e2.header !== void 0 && e2.header !== "" && log.message(
1265
- e2.header.split(`
1266
- `).map((a3) => styleText2("bold", a3)),
1267
- {
1268
- output: r3,
1269
- secondarySymbol: i3,
1270
- symbol: i3,
1271
- spacing: 0
1272
- }
1273
- ), log.message(
1274
- o2.split(`
1275
- `).map((a3) => styleText2("dim", a3)),
1276
- {
1277
- output: r3,
1278
- secondarySymbol: i3,
1279
- symbol: i3,
1280
- spacing: s2 ?? p2
1281
- }
1282
- );
1283
- }, T2 = () => {
1284
- for (const e2 of n4) {
1285
- const { header: s2, value: t3, full: o2 } = e2;
1286
- (s2 === void 0 || s2.length === 0) && t3.length === 0 || h3(e2, void 0, m2 === true && o2.length > 0);
1287
- }
1288
- }, L = (e2, s2, t3) => {
1289
- if (f2(false), (t3?.raw !== true || !v2) && e2.value !== "" && (e2.value += `
1290
- `), e2.value += A(s2), v2 = t3?.raw === true, l3.limit !== void 0) {
1291
- const o2 = e2.value.split(`
1292
- `), a3 = o2.length - l3.limit;
1293
- if (a3 > 0) {
1294
- const g2 = o2.splice(0, a3);
1295
- m2 && (e2.full += (e2.full === "" ? "" : `
1296
- `) + g2.join(`
1297
- `));
1298
- }
1299
- e2.value = o2.join(`
1300
- `);
1301
- }
1302
- d2 && y2();
1303
- }, y2 = () => {
1304
- for (const e2 of n4)
1305
- e2.result ? e2.result.status === "error" ? log.error(e2.result.message, { output: r3, secondarySymbol: i3, spacing: 0 }) : log.success(e2.result.message, { output: r3, secondarySymbol: i3, spacing: 0 }) : e2.value !== "" && h3(e2, 0);
1306
- }, B = (e2, s2) => {
1307
- f2(false), e2.result = s2, d2 && y2();
1308
- };
1309
- return {
1310
- message(e2, s2) {
1311
- L(n4[0], e2, s2);
1312
- },
1313
- group(e2) {
1314
- const s2 = {
1315
- header: e2,
1316
- value: "",
1317
- full: ""
1318
- };
1319
- return n4.push(s2), {
1320
- message(t3, o2) {
1321
- L(s2, t3, o2);
1322
- },
1323
- error(t3) {
1324
- B(s2, {
1325
- status: "error",
1326
- message: t3
1327
- });
1328
- },
1329
- success(t3) {
1330
- B(s2, {
1331
- status: "success",
1332
- message: t3
1333
- });
1334
- }
1335
- };
1336
- },
1337
- error(e2, s2) {
1338
- f2(true), log.error(e2, { output: r3, secondarySymbol: i3, spacing: 1 }), s2?.showLog !== false && T2(), n4.splice(1, n4.length - 1), n4[0].value = "", n4[0].full = "";
1339
- },
1340
- success(e2, s2) {
1341
- f2(true), log.success(e2, { output: r3, secondarySymbol: i3, spacing: 1 }), s2?.showLog === true && T2(), n4.splice(1, n4.length - 1), n4[0].value = "", n4[0].full = "";
1342
- }
1343
- };
1344
- };
1345
1227
  var text = (e2) => new n2({
1346
1228
  validate: e2.validate,
1347
1229
  placeholder: e2.placeholder,
@@ -1854,7 +1736,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
1854
1736
  // package.json
1855
1737
  var package_default = {
1856
1738
  name: "zapdev",
1857
- version: "1.17.0",
1739
+ version: "2.0.0",
1858
1740
  description: "Fast, performant and lightweight git chores and dev tools using your terminal and an OpenAI-compatible LLM endpoint.",
1859
1741
  type: "module",
1860
1742
  bin: {
@@ -1914,7 +1796,7 @@ var package_default = {
1914
1796
 
1915
1797
  // src/commands/commit.ts
1916
1798
  import { basename } from "node:path";
1917
- import { styleText as styleText4 } from "node:util";
1799
+ import { styleText as styleText3 } from "node:util";
1918
1800
 
1919
1801
  // src/lib/config.ts
1920
1802
  function resolveHookTimeout(env = process.env) {
@@ -2441,13 +2323,24 @@ async function getRepoStatus(repo) {
2441
2323
  };
2442
2324
  }
2443
2325
  async function getUpstreamRemote(repo, branch) {
2444
- const remotes = (await git(["remote"], repo)).trim().split("\n").filter(Boolean);
2326
+ const remotes = await getRemotes(repo);
2445
2327
  const upstream = (await git(["for-each-ref", "--format=%(upstream:remotename)", `refs/heads/${branch}`], repo)).trim();
2446
2328
  return remotes.includes(upstream) ? upstream : null;
2447
2329
  }
2330
+ async function getPushRemote(repo, branch) {
2331
+ const upstream = await getUpstreamRemote(repo, branch);
2332
+ if (upstream) return upstream;
2333
+ const remotes = await getRemotes(repo);
2334
+ return remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : null;
2335
+ }
2336
+ async function getRemotes(repo) {
2337
+ return (await git(["remote"], repo)).trim().split("\n").filter(Boolean);
2338
+ }
2448
2339
  async function push(repo, remote, branch, onHook, onOutput) {
2449
2340
  await git(["check-ref-format", "--branch", branch], repo);
2450
- await git(["push", "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook, onOutput);
2341
+ const current = await currentBranch(repo);
2342
+ const upstream = (await git(["for-each-ref", "--format=%(upstream)", `refs/heads/${current}`], repo)).trim();
2343
+ await git(["push", ...!upstream ? ["--set-upstream"] : [], "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook, onOutput);
2451
2344
  }
2452
2345
  async function findRepos(path) {
2453
2346
  const root = await repoRoot(path);
@@ -2582,54 +2475,23 @@ var COMMIT_TYPES = [
2582
2475
  ];
2583
2476
 
2584
2477
  // src/commands/git-task.ts
2585
- import { styleText as styleText3 } from "node:util";
2586
- async function runGitTask(label, operation, successMessage, run, group) {
2587
- const title = `${label}: ${operation}`;
2478
+ async function runTask(title, successMessage, run, withGuide = true) {
2588
2479
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2589
- const loader = interactive && !group ? spinner({ withGuide: false }) : void 0;
2590
- const startLoading = () => {
2591
- if (!loader) return;
2592
- const wasRaw = Boolean(process.stdin.isRaw);
2593
- loader.start(title);
2480
+ const loader = interactive ? spinner({ withGuide }) : void 0;
2481
+ const wasRaw = Boolean(process.stdin.isRaw);
2482
+ loader?.start(title);
2483
+ if (loader) {
2594
2484
  process.stdin.setRawMode?.(wasRaw);
2595
2485
  process.stdin.pause();
2596
- };
2597
- let task = group;
2598
- const onOutput = (chunk, stream) => {
2599
- if (!interactive) {
2600
- process[stream].write(chunk);
2601
- return;
2602
- }
2603
- if (!task) loader?.clear();
2604
- task ??= taskLog({ title: `${title}: running\u2026`, limit: 10, retainLog: true });
2605
- task.message(chunk, { raw: true });
2606
- };
2607
- const onHook = (event) => {
2608
- loader?.clear();
2609
- if (event.phase === "start" || event.exitCode !== 0) {
2610
- const hook = `${label}: ${event.name}`;
2611
- const message = event.phase === "start" ? `${hook}: running\u2026` : `${hook}: failed (exit ${event.exitCode})`;
2612
- if (task) task.message(message);
2613
- else if (event.phase === "start") log.step(styleText3("bold", message));
2614
- else log.error(message);
2615
- }
2616
- if (event.phase === "exit" && !task) startLoading();
2617
- };
2618
- if (!interactive) log.info(title);
2619
- startLoading();
2486
+ } else log.info(title);
2620
2487
  try {
2621
- await run(onHook, onOutput);
2622
- loader?.clear();
2623
- if (task) task.success(successMessage, { showLog: true });
2624
- else log.success(successMessage);
2625
- return true;
2488
+ const result = await run(() => void 0);
2489
+ if (loader) loader.stop(successMessage);
2490
+ else log.success(successMessage, { spacing: 0 });
2491
+ return result;
2626
2492
  } catch (error) {
2627
2493
  loader?.clear();
2628
- const message = `${label}: ${operation} failed${error instanceof GitOutputError ? "" : `: ${errorMessage(error)}`}`;
2629
- group?.message(message);
2630
- if (task) task.error(message);
2631
- else if (!(error instanceof GitOutputError && error.hookFailureReported)) log.error(message);
2632
- return false;
2494
+ throw new Error(`${title} failed: ${errorMessage(error)}`, { cause: error });
2633
2495
  }
2634
2496
  }
2635
2497
 
@@ -2690,94 +2552,79 @@ var commitCommand = defineCommand({
2690
2552
  effort: args.effort
2691
2553
  });
2692
2554
  } catch (error) {
2693
- log.error(errorMessage(error));
2555
+ log.error(errorMessage(error), { spacing: 0 });
2694
2556
  process.exitCode = 1;
2695
2557
  return;
2696
2558
  }
2697
2559
  if (interactive) intro("zapdev commit");
2698
- const repos = await findRepos(process.cwd());
2699
- if (repos.length === 0) {
2700
- log.warn("No git repository found here or in direct children.");
2701
- if (interactive) outro("Nothing to do.");
2702
- return;
2703
- }
2704
- const loader = interactive ? spinner() : void 0;
2705
- if (args.pull) {
2706
- loader?.start("Pulling repositories in parallel");
2707
- let pullLog;
2708
- const pulls = await Promise.allSettled(repos.map(async (repo) => {
2709
- const label = basename(repo);
2710
- const hasChanges = args.staged ? Boolean((await getStagedDiff(repo)).trim()) : (await getRepoStatus(repo)).hasChanges;
2711
- if (!hasChanges) return true;
2712
- const branch = await currentBranch(repo);
2713
- if (!await getUpstreamRemote(repo, branch)) {
2714
- return `${label}: no configured upstream remote. Skipping pull.`;
2560
+ try {
2561
+ const repos = await findRepos(process.cwd());
2562
+ if (repos.length === 0) {
2563
+ log.warn("No git repository found here or in direct children.");
2564
+ if (interactive) outro("Nothing to do.");
2565
+ return;
2566
+ }
2567
+ if (args.pull) {
2568
+ const pulls = await runTask("Pulling repositories in parallel", "Repositories pulled", async (onOutput) => {
2569
+ const results2 = await Promise.allSettled(repos.map(async (repo) => {
2570
+ const label = basename(repo);
2571
+ const hasChanges = args.staged ? Boolean((await getStagedDiff(repo)).trim()) : (await getRepoStatus(repo)).hasChanges;
2572
+ if (!hasChanges) return null;
2573
+ const branch = await currentBranch(repo);
2574
+ if (!await getUpstreamRemote(repo, branch)) {
2575
+ return `${label}: no configured upstream remote. Skipping pull.`;
2576
+ }
2577
+ await git(["pull", "--ff-only", "--no-rebase", "--no-autostash"], repo, void 0, onOutput);
2578
+ return null;
2579
+ }));
2580
+ for (const [index, result] of results2.entries()) {
2581
+ if (result.status === "rejected") throw new Error(`${basename(repos[index])}: ${errorMessage(result.reason)}`);
2582
+ }
2583
+ return results2;
2584
+ });
2585
+ for (const result of pulls) {
2586
+ if (result.status === "fulfilled" && result.value) log.warn(result.value, { spacing: 0 });
2715
2587
  }
2716
- if (interactive) {
2717
- loader?.clear();
2718
- pullLog ??= taskLog({ title: "Pulling repositories in parallel", limit: 10, retainLog: true });
2588
+ }
2589
+ const repositories = repos.map((repo) => ({ repo, label: basename(repo), pendingLabel: basename(repo) }));
2590
+ const { results, scan } = await runTask("Preparing repositories and generating commit messages in parallel", "Repositories prepared", async () => {
2591
+ const scan2 = await hasGitleaks();
2592
+ const results2 = await Promise.allSettled(repositories.map(async (repository) => {
2593
+ const { repo } = repository;
2594
+ const { branch, hasChanges } = await getRepoStatus(repo);
2595
+ repository.label = `${basename(repo)} (${branch})`;
2596
+ repository.pendingLabel = hasChanges ? styleText3("bold", repository.label) : repository.label;
2597
+ if (!args.staged) await stageAll(repo);
2598
+ const diff = await getStagedDiff(repo);
2599
+ if (!diff.trim()) return null;
2600
+ const stats = await getStagedDiffStats(repo);
2601
+ if (scan2) await scanStagedChanges(repo);
2602
+ const message = await generateCommitMessage(diff, config, args.type);
2603
+ if (!message) throw new Error("The model returned an empty message.");
2604
+ return { ...repository, message, stats };
2605
+ }));
2606
+ for (const [index, result] of results2.entries()) {
2607
+ if (result.status === "rejected") throw new Error(`${repositories[index].pendingLabel}: ${errorMessage(result.reason)}`);
2608
+ }
2609
+ return { results: results2, scan: scan2 };
2610
+ });
2611
+ if (!scan) log.info("Gitleaks not found, skipping secret scan.", { spacing: 0 });
2612
+ const drafts = [];
2613
+ for (const [index, result] of results.entries()) {
2614
+ if (result.status === "fulfilled") {
2615
+ if (result.value) drafts.push(result.value);
2616
+ else log.info(`${repositories[index].pendingLabel}: nothing to commit.`, { spacing: 0 });
2719
2617
  }
2720
- return runGitTask(label, "pull", `${label}: pulled`, (onHook, onOutput) => git(["pull", "--ff-only", "--no-rebase", "--no-autostash"], repo, onHook, onOutput), pullLog?.group(label));
2721
- }));
2722
- loader?.clear();
2723
- const failed = pulls.some((result) => result.status === "rejected" || result.value === false);
2724
- if (failed) pullLog?.error("Pulling failed");
2725
- else pullLog?.success("Repositories pulled", { showLog: true });
2726
- for (const [index, result] of pulls.entries()) {
2727
- if (result.status === "rejected") reportGitFailure(`${basename(repos[index])}: pull failed`, result.reason);
2728
- else if (typeof result.value === "string") log.warn(result.value);
2729
2618
  }
2730
- if (failed) {
2731
- process.exitCode = 1;
2619
+ const selected = drafts.length ? await reviewMessages(drafts, interactive && !args.yes) : [];
2620
+ if (!selected) {
2621
+ cancel("Cancelled (changes left staged).");
2732
2622
  return;
2733
2623
  }
2734
- }
2735
- let scan;
2736
- try {
2737
- scan = await hasGitleaks();
2738
- } catch (error) {
2739
- log.error(`Gitleaks check failed: ${errorMessage(error)}`);
2740
- process.exitCode = 1;
2741
- return;
2742
- }
2743
- if (!scan) log.info("Gitleaks not found, skipping secret scan.");
2744
- loader?.start("Preparing repositories and generating commit messages in parallel");
2745
- const repositories = repos.map((repo) => ({ repo, label: basename(repo), pendingLabel: basename(repo) }));
2746
- const results = await Promise.allSettled(repositories.map(async (repository) => {
2747
- const { repo } = repository;
2748
- const { branch, hasChanges } = await getRepoStatus(repo);
2749
- repository.label = `${basename(repo)} (${branch})`;
2750
- repository.pendingLabel = hasChanges ? styleText4("bold", repository.label) : repository.label;
2751
- if (!args.staged) await stageAll(repo);
2752
- const diff = await getStagedDiff(repo);
2753
- if (!diff.trim()) return null;
2754
- const stats = await getStagedDiffStats(repo);
2755
- if (scan) await scanStagedChanges(repo);
2756
- const message = await generateCommitMessage(diff, config, args.type);
2757
- if (!message) throw new Error("The model returned an empty message.");
2758
- return { ...repository, message, stats };
2759
- }));
2760
- loader?.stop("Repositories prepared");
2761
- const drafts = [];
2762
- for (const [index, result] of results.entries()) {
2763
- if (result.status === "rejected") {
2764
- log.error(`${repositories[index].pendingLabel}: ${errorMessage(result.reason)}`);
2765
- process.exitCode = 1;
2766
- } else if (result.value) {
2767
- drafts.push(result.value);
2768
- } else {
2769
- log.info(`${repositories[index].pendingLabel}: nothing to commit.`);
2770
- }
2771
- }
2772
- const selected = drafts.length ? await reviewMessages(drafts, interactive && !args.yes) : [];
2773
- if (!selected) {
2774
- cancel("Cancelled (changes left staged).");
2775
- return;
2776
- }
2777
- const toSend = [];
2778
- let previousBranchInput = "";
2779
- for (const draft of selected) {
2780
- try {
2624
+ const toSend = [];
2625
+ let previousBranchInput = "";
2626
+ for (const draft of selected) {
2627
+ let withGuide = true;
2781
2628
  const current = await currentBranch(draft.repo);
2782
2629
  if (/^(main|master|principal|dev|development)$/.test(current)) {
2783
2630
  if (!interactive) throw new Error("Committing on a protected branch requires a terminal to choose a branch. Changes remain staged.");
@@ -2790,70 +2637,54 @@ var commitCommand = defineCommand({
2790
2637
  return;
2791
2638
  }
2792
2639
  previousBranchInput = answer.trim();
2640
+ withGuide = Boolean(previousBranchInput);
2793
2641
  const branch = previousBranchInput || current;
2794
2642
  if (branch !== current) {
2795
2643
  await switchBranch(draft.repo, branch);
2796
2644
  draft.label = `${basename(draft.repo)} (${branch})`;
2797
- draft.pendingLabel = styleText4("bold", draft.label);
2645
+ draft.pendingLabel = styleText3("bold", draft.label);
2798
2646
  }
2799
2647
  }
2800
- if (!await runGitTask(draft.label, "commit", `${draft.label}: committed ${draft.message}`, (onHook, onOutput) => commit(draft.repo, draft.message, onHook, onOutput))) {
2801
- process.exitCode = 1;
2802
- continue;
2803
- }
2648
+ await runTask(`${draft.label}: commit`, `${draft.label}: committed ${draft.message}`, (onOutput) => commit(draft.repo, draft.message, void 0, onOutput), withGuide);
2804
2649
  toSend.push(draft);
2805
- } catch (error) {
2806
- reportGitFailure(`${draft.pendingLabel}: commit failed`, error);
2807
- process.exitCode = 1;
2808
2650
  }
2809
- }
2810
- if (toSend.length === 0) return;
2811
- if (!interactive && !args.push) return;
2812
- const destinations = [];
2813
- for (const { repo, label } of toSend) {
2814
- try {
2651
+ if (toSend.length === 0) return;
2652
+ if (!interactive && !args.push) return;
2653
+ const destinations = [];
2654
+ for (const { repo, label } of toSend) {
2815
2655
  const branch = await currentBranch(repo);
2816
- const remote = await getUpstreamRemote(repo, branch);
2656
+ const remote = await getPushRemote(repo, branch);
2817
2657
  if (remote) destinations.push({ repo, label, branch, remote });
2818
- else log.warn(`${label}: no configured upstream remote. Skipping push; commit remains local.`);
2819
- } catch (error) {
2820
- reportGitFailure(`${label}: push failed`, error);
2821
- process.exitCode = 1;
2822
- return;
2658
+ else log.warn(`${label}: no unambiguous push remote. Skipping push; commit remains local.`, { spacing: 0 });
2823
2659
  }
2824
- }
2825
- let shouldPush = Boolean(args.push);
2826
- if (!shouldPush && interactive && destinations.length) {
2827
- const answer = await confirm({
2828
- message: `Push ${destinations.map(({ label }) => label).join(", ")}?`,
2829
- initialValue: false
2830
- });
2831
- if (isCancel(answer)) {
2832
- outro("Committed. Not pushed.");
2833
- return;
2834
- }
2835
- shouldPush = answer;
2836
- }
2837
- if (shouldPush) {
2838
- for (const { repo, label, branch, remote } of destinations) {
2839
- if (!await runGitTask(label, "push", `${label}: pushed to ${remote}/${branch}`, (onHook, onOutput) => push(repo, remote, branch, onHook, onOutput))) {
2840
- process.exitCode = 1;
2841
- outro("Sending stopped after a Git failure.");
2660
+ let shouldPush = Boolean(args.push);
2661
+ if (!shouldPush && interactive && destinations.length) {
2662
+ const answer = await confirm({
2663
+ message: `Push ${destinations.map(({ label }) => label).join(", ")}?`,
2664
+ initialValue: false
2665
+ });
2666
+ if (isCancel(answer)) {
2667
+ cancel("Committed. Not pushed.");
2842
2668
  return;
2843
2669
  }
2670
+ shouldPush = answer;
2844
2671
  }
2672
+ if (shouldPush) {
2673
+ for (const { repo, label, branch, remote } of destinations) {
2674
+ await runTask(`${label}: push`, `${label}: pushed to ${remote}/${branch}`, (onOutput) => push(repo, remote, branch, void 0, onOutput));
2675
+ }
2676
+ }
2677
+ if (interactive) outro("Done.");
2678
+ } catch (error) {
2679
+ log.error(errorMessage(error), { spacing: 0 });
2680
+ process.exitCode = 1;
2845
2681
  }
2846
- if (interactive) outro(process.exitCode ? "Finished with errors." : "Done.");
2847
2682
  }
2848
2683
  });
2849
- function reportGitFailure(message, error) {
2850
- if (error instanceof GitOutputError && error.hookFailureReported) return;
2851
- log.error(error instanceof GitOutputError ? message : `${message}: ${errorMessage(error)}`);
2852
- }
2853
2684
  async function reviewMessages(drafts, canPrompt) {
2854
2685
  while (true) {
2855
- for (const { stats, pendingLabel, message } of drafts) {
2856
- log.message(`(${styleText4("green", `+${stats.additions}`)} ${styleText4("red", `-${stats.deletions}`)}) ${pendingLabel}: ${message}`);
2686
+ for (const [index, { stats, pendingLabel, message }] of drafts.entries()) {
2687
+ log.message(`(${styleText3("green", `+${stats.additions}`)} ${styleText3("red", `-${stats.deletions}`)}) ${pendingLabel}: ${message}`, { spacing: index === 0 ? 1 : 0 });
2857
2688
  }
2858
2689
  if (!canPrompt) return drafts;
2859
2690
  const action = await select({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.17.0",
3
+ "version": "2.0.0",
4
4
  "description": "Fast, performant and lightweight git chores and dev tools using your terminal and an OpenAI-compatible LLM endpoint.",
5
5
  "type": "module",
6
6
  "bin": {