zapdev 1.15.0 → 1.17.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 +37 -28
  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
- Repositories with no new changes can still send existing commits. 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 configured upstream remote; it is omitted when none have one.
118
118
 
119
119
  ```bash
120
120
  zapdev commit
@@ -127,7 +127,7 @@ zapdev commit
127
127
  | `--effort <effort>` | Override the reasoning effort |
128
128
  | `-t, --type <type>` | Force an exact lowercase Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
129
129
  | `-p, --push` | Skip push confirmation and push the current branch |
130
- | `--pull` | Pull fast-forward updates in repositories with an upstream remote before staging or message generation |
130
+ | `--pull` | Pull fast-forward updates only in repositories with changes to commit and an upstream remote |
131
131
  | `-s, --staged` | Commit only changes that are already staged |
132
132
  | `-y, --yes` | Skip commit review; still confirm push unless `--push` is set |
133
133
 
@@ -143,13 +143,13 @@ Before contacting the LLM endpoint, zapdev runs `gitleaks git --staged --verbose
143
143
 
144
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.
145
145
 
146
- With `--pull`, repositories are pulled one at a time before any staging, secret scan, or message generation. 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 entire flow before staging; no rebase, merge commit, or automatic stash is performed. Earlier successful pulls remain applied.
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; silent commands 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
+ 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`.
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. A missing destination branch can be created even without new changes. 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. 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.
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
@@ -1854,7 +1854,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
1854
1854
  // package.json
1855
1855
  var package_default = {
1856
1856
  name: "zapdev",
1857
- version: "1.15.0",
1857
+ version: "1.17.0",
1858
1858
  description: "Fast, performant and lightweight git chores and dev tools using your terminal and an OpenAI-compatible LLM endpoint.",
1859
1859
  type: "module",
1860
1860
  bin: {
@@ -2583,10 +2583,10 @@ var COMMIT_TYPES = [
2583
2583
 
2584
2584
  // src/commands/git-task.ts
2585
2585
  import { styleText as styleText3 } from "node:util";
2586
- async function runGitTask(label, operation, successMessage, run) {
2586
+ async function runGitTask(label, operation, successMessage, run, group) {
2587
2587
  const title = `${label}: ${operation}`;
2588
2588
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2589
- const loader = interactive ? spinner({ withGuide: false }) : void 0;
2589
+ const loader = interactive && !group ? spinner({ withGuide: false }) : void 0;
2590
2590
  const startLoading = () => {
2591
2591
  if (!loader) return;
2592
2592
  const wasRaw = Boolean(process.stdin.isRaw);
@@ -2594,7 +2594,7 @@ async function runGitTask(label, operation, successMessage, run) {
2594
2594
  process.stdin.setRawMode?.(wasRaw);
2595
2595
  process.stdin.pause();
2596
2596
  };
2597
- let task;
2597
+ let task = group;
2598
2598
  const onOutput = (chunk, stream) => {
2599
2599
  if (!interactive) {
2600
2600
  process[stream].write(chunk);
@@ -2606,12 +2606,13 @@ async function runGitTask(label, operation, successMessage, run) {
2606
2606
  };
2607
2607
  const onHook = (event) => {
2608
2608
  loader?.clear();
2609
- const hook = `${label}: ${event.name}`;
2610
- const message = event.phase === "start" ? `${hook}: running\u2026` : `${hook}: ${event.exitCode === 0 ? "completed" : `failed (exit ${event.exitCode})`}`;
2611
- if (task) task.message(message);
2612
- else if (event.phase === "start") log.step(styleText3("bold", message));
2613
- else if (event.exitCode === 0) log.success(message);
2614
- else log.error(message);
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
+ }
2615
2616
  if (event.phase === "exit" && !task) startLoading();
2616
2617
  };
2617
2618
  if (!interactive) log.info(title);
@@ -2625,6 +2626,7 @@ async function runGitTask(label, operation, successMessage, run) {
2625
2626
  } catch (error) {
2626
2627
  loader?.clear();
2627
2628
  const message = `${label}: ${operation} failed${error instanceof GitOutputError ? "" : `: ${errorMessage(error)}`}`;
2629
+ group?.message(message);
2628
2630
  if (task) task.error(message);
2629
2631
  else if (!(error instanceof GitOutputError && error.hookFailureReported)) log.error(message);
2630
2632
  return false;
@@ -2699,24 +2701,35 @@ var commitCommand = defineCommand({
2699
2701
  if (interactive) outro("Nothing to do.");
2700
2702
  return;
2701
2703
  }
2704
+ const loader = interactive ? spinner() : void 0;
2702
2705
  if (args.pull) {
2703
- for (const repo of repos) {
2706
+ loader?.start("Pulling repositories in parallel");
2707
+ let pullLog;
2708
+ const pulls = await Promise.allSettled(repos.map(async (repo) => {
2704
2709
  const label = basename(repo);
2705
- try {
2706
- const branch = await currentBranch(repo);
2707
- if (!await getUpstreamRemote(repo, branch)) {
2708
- log.warn(`${label}: no configured upstream remote. Skipping pull.`);
2709
- continue;
2710
- }
2711
- } catch (error) {
2712
- reportGitFailure(`${label}: pull failed`, error);
2713
- process.exitCode = 1;
2714
- return;
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.`;
2715
2715
  }
2716
- if (!await runGitTask(label, "pull", `${label}: pulled`, (onHook, onOutput) => git(["pull", "--ff-only", "--no-rebase", "--no-autostash"], repo, onHook, onOutput))) {
2717
- process.exitCode = 1;
2718
- return;
2716
+ if (interactive) {
2717
+ loader?.clear();
2718
+ pullLog ??= taskLog({ title: "Pulling repositories in parallel", limit: 10, retainLog: true });
2719
2719
  }
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
+ }
2730
+ if (failed) {
2731
+ process.exitCode = 1;
2732
+ return;
2720
2733
  }
2721
2734
  }
2722
2735
  let scan;
@@ -2728,7 +2741,6 @@ var commitCommand = defineCommand({
2728
2741
  return;
2729
2742
  }
2730
2743
  if (!scan) log.info("Gitleaks not found, skipping secret scan.");
2731
- const loader = interactive ? spinner() : void 0;
2732
2744
  loader?.start("Preparing repositories and generating commit messages in parallel");
2733
2745
  const repositories = repos.map((repo) => ({ repo, label: basename(repo), pendingLabel: basename(repo) }));
2734
2746
  const results = await Promise.allSettled(repositories.map(async (repository) => {
@@ -2747,7 +2759,6 @@ var commitCommand = defineCommand({
2747
2759
  }));
2748
2760
  loader?.stop("Repositories prepared");
2749
2761
  const drafts = [];
2750
- const unchanged = [];
2751
2762
  for (const [index, result] of results.entries()) {
2752
2763
  if (result.status === "rejected") {
2753
2764
  log.error(`${repositories[index].pendingLabel}: ${errorMessage(result.reason)}`);
@@ -2756,7 +2767,6 @@ var commitCommand = defineCommand({
2756
2767
  drafts.push(result.value);
2757
2768
  } else {
2758
2769
  log.info(`${repositories[index].pendingLabel}: nothing to commit.`);
2759
- unchanged.push(repositories[index]);
2760
2770
  }
2761
2771
  }
2762
2772
  const selected = drafts.length ? await reviewMessages(drafts, interactive && !args.yes) : [];
@@ -2797,7 +2807,6 @@ var commitCommand = defineCommand({
2797
2807
  process.exitCode = 1;
2798
2808
  }
2799
2809
  }
2800
- toSend.push(...unchanged);
2801
2810
  if (toSend.length === 0) return;
2802
2811
  if (!interactive && !args.push) return;
2803
2812
  const destinations = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.15.0",
3
+ "version": "1.17.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": {