zapdev 1.10.0 → 1.11.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 +11 -11
  2. package/dist/cli.js +34 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -113,7 +113,7 @@ Stages all changes, scans them with Gitleaks when installed, generates Conventio
113
113
  - **Unified review:** generates messages in parallel, then presents every message with its repository name and branch. Repositories with pending changes are shown in bold and underlined when terminal styling is enabled.
114
114
  - **Actions:** commit all, commit only a named repository, edit a named repository's message, or cancel. Editing returns to the review menu.
115
115
 
116
- Repositories with no new changes can still send existing commits. Unselected or cancelled changes remain staged; failed or unselected commit drafts are not pushed. Push confirmation is asked once, followed by a destination input for each eligible repository or subtree.
116
+ 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.
117
117
 
118
118
  ```bash
119
119
  zapdev commit
@@ -125,7 +125,7 @@ zapdev commit
125
125
  | `--model <model>` | Override the model |
126
126
  | `--effort <effort>` | Override the reasoning effort |
127
127
  | `-t, --type <type>` | Force an exact lowercase Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
128
- | `-p, --push` | Skip push confirmation; still ask for each destination branch |
128
+ | `-p, --push` | Skip push confirmation and push the current branch |
129
129
  | `-s, --staged` | Commit only changes that are already staged |
130
130
  | `-y, --yes` | Skip commit review; still confirm push unless `--push` is set |
131
131
 
@@ -133,7 +133,7 @@ zapdev commit
133
133
  zapdev commit -t feat # force the type
134
134
  zapdev commit --staged # leave unstaged changes untouched
135
135
  zapdev commit -y # commit automatically, then ask before pushing
136
- zapdev commit -yp # commit automatically, then enter each push destination
136
+ zapdev commit -yp # skip review and push confirmation; choose a branch if protected
137
137
  ```
138
138
 
139
139
  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,11 +142,11 @@ Preparation and commit failures are reported per repository while the others con
142
142
 
143
143
  In a terminal, Git and its hooks display live logs and support native prompts, even with `--yes` (which skips commit review, not native Git prompts). Git controls hook stdin; interactive hooks should read from the terminal, for example `/dev/tty`. Each hook has a 60-second deadline, including time spent answering prompts; override it with `ZD_HOOK_TIMEOUT=120 zapdev commit`.
144
144
 
145
- Every push asks for a destination branch, even with `--yes --push`. In both modes, prompts start empty and then reuse the previous input across repositories in this run. Press Enter to accept the prefilled name; an empty or cleared input uses the current branch and leaves the next prompt empty. Pushing to `main` is allowed in both modes. Classic pushes use the current branch's upstream remote, or the only configured remote if there is no upstream, and run `git push <remote> HEAD:refs/heads/<input>` without changing local branches or their upstreams. The destination need not exist locally. When no remote can be selected unambiguously, the repository's push is skipped with a warning, without a branch prompt. Git rejections stop the command: no rebase, merge, retry, or force-push.
145
+ 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.
146
146
 
147
- Before each destination input, both modes check the presumed remote branch: the previous input, or the current branch when empty. If that branch is missing, the check falls back to the remote's default branch (`HEAD`). Only unpublished local commits trigger a destination input; if neither reference exists, zapdev also asks. Classic mode compares HEAD; subtree mode compares its extracted history. This check cannot anticipate a different destination you would type next. Repositories without new changes do not trigger an LLM call or a new commit.
147
+ Pushes always target a remote branch with the current local branch's name, with no destination input. Classic pushes use the current branch's upstream remote, or the only configured remote if there is no upstream, and run `git push <remote> HEAD:refs/heads/<current-branch>`. Ambiguous remotes are skipped with a warning. Both modes check for unpublished commits against that remote branch, falling back to the remote's default branch (`HEAD`) if it is missing. If neither reference exists, publication is attempted. Classic mode compares HEAD; subtree mode compares its extracted history. Already published history is skipped. No rebase, merge, retry, or force-push.
148
148
 
149
- Without a TTY, zapdev commits all prepared repositories automatically but cannot push; `--push` reports an error and leaves commits local.
149
+ 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.
150
150
 
151
151
  #### Subtree publication
152
152
 
@@ -167,15 +167,15 @@ Place an optional `zapdev.json` in the directory where you launch zapdev:
167
167
 
168
168
  - **Scope:** only the launch directory's file is read, not parents or child repositories. Its mapping applies to newly committed repositories and repositories without new changes.
169
169
  - **Configuration:** only `zapdev.json` selects subtree mode; no subtree CLI flag. Missing or empty `subtrees` uses classic push. Invalid configuration stops the command before staging. Replace the removed `isSubtree` option with an explicit mapping.
170
- - **Destinations:** only mapped folders are published, using their configured remotes; folder and remote names need not match. Each changed subtree asks for its own destination branch, initially empty, then prefilled with the previous input across repositories in this run. Press Enter to reuse it, or leave the input empty to use the current branch. `main` is allowed; detached HEAD is refused.
171
- - **Publication:** check the presumed destination branch using the shared unpublished-commit rule above, then run `git subtree push --prefix=<folder> <remote> <input>`. Already published history is skipped without prompting; uncommitted changes are excluded.
170
+ - **Destinations:** only mapped folders are published, using their configured remotes; folder and remote names need not match. All subtrees use the parent repository's current branch name. The parent itself is not pushed. Detached HEAD is refused.
171
+ - **Publication:** check each subtree using the shared unpublished-commit rule above, then run `git subtree push --prefix=<folder> <remote> <current-branch>`. Subtrees without unpublished commits are skipped without creating remote branches; uncommitted changes are excluded.
172
172
  - **Failures:** stop all remaining publications, leaving earlier publications intact. No automatic recovery.
173
173
 
174
- Interactive runs still ask for confirmation, including with `--yes`; `--push` skips only that confirmation, never the destination inputs. Publication requires a TTY.
174
+ Interactive runs still ask for publication confirmation, including with `--yes`; `--push` skips that confirmation. The commit and branch-selection flow is identical to classic mode.
175
175
 
176
176
  ```bash
177
177
  zapdev commit # with subtrees configured, ask to publish after committing
178
- zapdev commit -yp # enter a destination for each changed subtree
178
+ zapdev commit -yp # publish changed subtrees on the parent's current branch
179
179
  ```
180
180
 
181
181
  ### `zapdev subtree-init`
@@ -202,7 +202,7 @@ zapdev subtree-init my-project \
202
202
 
203
203
  ### Zed IDE
204
204
 
205
- For a faster review and commit workflow, review and stage changes from Zed's Git panel, then run `zapdev commit -syp` from a task. The command commits only staged changes, skips commit review and push confirmation, then asks for the push destination.
205
+ For a faster review and commit workflow, review and stage changes from Zed's Git panel, then run `zapdev commit -syp` from a task. The command commits only staged changes, skips commit review and push confirmation, and asks for a branch before committing only if the current branch is protected.
206
206
 
207
207
  Add the following tasks to `.zed/tasks.json`:
208
208
 
package/dist/cli.js CHANGED
@@ -554,7 +554,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
554
554
  // package.json
555
555
  var package_default = {
556
556
  name: "zapdev",
557
- version: "1.10.0",
557
+ version: "1.11.0",
558
558
  description: "Fast, performant and lightweight git chores and dev tools using your terminal and an OpenAI-compatible LLM endpoint.",
559
559
  type: "module",
560
560
  bin: {
@@ -2323,6 +2323,12 @@ async function commit(repo, message, onHook, onNativeOutput) {
2323
2323
  async function currentBranch(repo) {
2324
2324
  return (await git(["symbolic-ref", "--short", "HEAD"], repo)).trim();
2325
2325
  }
2326
+ async function switchBranch(repo, branch) {
2327
+ await git(["check-ref-format", "--branch", branch], repo);
2328
+ await git(["check-ref-format", `refs/heads/${branch}`], repo);
2329
+ const exists2 = await tryGit(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], repo) !== null;
2330
+ await git(exists2 ? ["switch", "--no-guess", "--", branch] : ["switch", "--no-track", "-c", branch], repo);
2331
+ }
2326
2332
  async function getRepoStatus(repo) {
2327
2333
  const lines = (await git(["status", "--porcelain=v2", "--branch", "--untracked-files=normal"], repo)).trim().split("\n");
2328
2334
  const branch = lines.find((line) => line.startsWith("# branch.head "))?.slice(14) ?? "HEAD";
@@ -2520,7 +2526,7 @@ var commitCommand = defineCommand({
2520
2526
  push: {
2521
2527
  type: "boolean",
2522
2528
  alias: "p",
2523
- description: "Skip push confirmation; still ask for each destination branch."
2529
+ description: "Skip push confirmation and push the current branch."
2524
2530
  },
2525
2531
  staged: {
2526
2532
  type: "boolean",
@@ -2603,9 +2609,29 @@ var commitCommand = defineCommand({
2603
2609
  return;
2604
2610
  }
2605
2611
  const toSend = [];
2612
+ let previousBranchInput = "";
2606
2613
  for (const draft of selected) {
2607
- log.info(`${draft.label}: committing`);
2608
2614
  try {
2615
+ const current = await currentBranch(draft.repo);
2616
+ if (/^(main|master|principal|dev|development)$/.test(current)) {
2617
+ if (!interactive) throw new Error("Committing on a protected branch requires a terminal to choose a branch. Changes remain staged.");
2618
+ const answer = await text({
2619
+ message: `${basename(draft.repo)} (${current}): branch to commit to`,
2620
+ initialValue: previousBranchInput
2621
+ });
2622
+ if (isCancel(answer)) {
2623
+ cancel("Committing cancelled. Changes remain staged; earlier commits stay local.");
2624
+ return;
2625
+ }
2626
+ previousBranchInput = answer.trim();
2627
+ const branch = previousBranchInput || current;
2628
+ if (branch !== current) {
2629
+ await switchBranch(draft.repo, branch);
2630
+ draft.label = `${basename(draft.repo)} (${branch})`;
2631
+ draft.pendingLabel = styleText4(["bold", "underline"], draft.label);
2632
+ }
2633
+ }
2634
+ log.info(`${draft.label}: committing`);
2609
2635
  await commit(draft.repo, draft.message, reportHooks(draft.label), reportNativeOutput);
2610
2636
  toSend.push(draft);
2611
2637
  log.success(`${draft.label}: committed ${draft.message}`);
@@ -2629,12 +2655,6 @@ var commitCommand = defineCommand({
2629
2655
  shouldPush = answer;
2630
2656
  }
2631
2657
  if (shouldPush) {
2632
- if (!interactive) {
2633
- log.error("Pushing requires a terminal to choose each destination branch. Commits remain local.");
2634
- process.exitCode = 1;
2635
- return;
2636
- }
2637
- let previousBranchInput = "";
2638
2658
  for (const { repo, label } of toSend) {
2639
2659
  try {
2640
2660
  const current = await currentBranch(repo);
@@ -2644,24 +2664,14 @@ var commitCommand = defineCommand({
2644
2664
  log.warn(`${label}: no remote or ambiguous remote choice. Skipping push; commit remains local.`);
2645
2665
  continue;
2646
2666
  }
2647
- log.info(`${label}: checking unpublished commits${prefix ? ` in ${prefix}` : ""} (${remote}/${previousBranchInput || current}).`);
2648
- if (!await hasUnpushedCommits(repo, remote, previousBranchInput || current, reportHooks(label), prefix || void 0, reportNativeOutput)) {
2667
+ log.info(`${label}: checking unpublished commits${prefix ? ` in ${prefix}` : ""} (${remote}/${current}).`);
2668
+ if (!await hasUnpushedCommits(repo, remote, current, reportHooks(label), prefix || void 0, reportNativeOutput)) {
2649
2669
  log.info(`${label}: no unpushed commits${prefix ? ` in ${prefix}` : ""}. Skipping.`);
2650
2670
  continue;
2651
2671
  }
2652
- const answer = await text({
2653
- message: `${label}: branch to push ${publishSubtreeMode ? `${prefix} to ` : "to "}${remote}`,
2654
- initialValue: previousBranchInput
2655
- });
2656
- if (isCancel(answer)) {
2657
- outro("Sending cancelled. Remaining commits stay local.");
2658
- return;
2659
- }
2660
- previousBranchInput = answer.trim();
2661
- const branch = previousBranchInput || current;
2662
- log.info(`${label}: pushing ${publishSubtreeMode ? `${prefix} \u2192 ` : ""}${remote}/${branch}`);
2663
- await push(repo, remote, branch, reportHooks(label), prefix || void 0, reportNativeOutput);
2664
- log.success(`${label}: pushed to ${remote}/${branch}`);
2672
+ log.info(`${label}: pushing ${publishSubtreeMode ? `${prefix} \u2192 ` : ""}${remote}/${current}`);
2673
+ await push(repo, remote, current, reportHooks(label), prefix || void 0, reportNativeOutput);
2674
+ log.success(`${label}: pushed to ${remote}/${current}`);
2665
2675
  }
2666
2676
  } catch (error) {
2667
2677
  reportGitFailure(`${label}: ${publishSubtreeMode ? "publication" : "push"} failed`, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.10.0",
3
+ "version": "1.11.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": {