zapdev 1.9.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 +12 -12
  2. package/dist/cli.js +108 -104
  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
@@ -124,8 +124,8 @@ zapdev commit
124
124
  | `--url <url>` | Override the complete Chat Completions endpoint |
125
125
  | `--model <model>` | Override the model |
126
126
  | `--effort <effort>` | Override the reasoning effort |
127
- | `-t, --type <type>` | Force the Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
128
- | `-p, --push` | Skip push confirmation; still ask for each destination branch |
127
+ | `-t, --type <type>` | Force an exact lowercase Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
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 the remote branch is missing or local commits remain unpublished, zapdev asks for a destination; otherwise it skips the send. 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.9.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: {
@@ -614,7 +614,7 @@ var package_default = {
614
614
 
615
615
  // src/commands/commit.ts
616
616
  import { basename } from "node:path";
617
- import { styleText as styleText3 } from "node:util";
617
+ import { styleText as styleText4 } from "node:util";
618
618
 
619
619
  // node_modules/@clack/core/dist/index.mjs
620
620
  import { styleText } from "node:util";
@@ -1780,40 +1780,6 @@ ${r3}
1780
1780
  }
1781
1781
  }).prompt();
1782
1782
 
1783
- // src/types/commit.ts
1784
- var COMMIT_TYPES = [
1785
- "feat",
1786
- "fix",
1787
- "chore",
1788
- "docs",
1789
- "style",
1790
- "refactor",
1791
- "perf",
1792
- "test",
1793
- "build",
1794
- "ci",
1795
- "revert"
1796
- ];
1797
-
1798
- // src/lib/commit-message.ts
1799
- function normalizeCommitType(input) {
1800
- const normalized = input.trim().toLowerCase();
1801
- return COMMIT_TYPES.includes(normalized) ? normalized : null;
1802
- }
1803
- function applyCommitType(systemPrompt, type) {
1804
- return `${systemPrompt}
1805
-
1806
- The type MUST be exactly "${type}".`;
1807
- }
1808
- var MAX_DIFF_CHARS = 12e3;
1809
- function truncateDiff(diff, max = MAX_DIFF_CHARS) {
1810
- return diff.length > max ? diff.slice(0, max) : diff;
1811
- }
1812
- function sanitizeCommitMessage(raw) {
1813
- const firstLine = raw.split("\n", 1)[0] ?? "";
1814
- return firstLine.replace(/[`"]/g, "").trim();
1815
- }
1816
-
1817
1783
  // src/lib/config.ts
1818
1784
  import { readFile } from "node:fs/promises";
1819
1785
  import { isAbsolute, join } from "node:path";
@@ -2267,16 +2233,17 @@ function createHookReporter(onHook, onTimeout, timeout) {
2267
2233
  }
2268
2234
 
2269
2235
  // src/lib/git.ts
2270
- async function git(args, cwd, onHook) {
2236
+ async function git(args, cwd, onHook, onNativeOutput) {
2271
2237
  const timeout = resolveHookTimeout();
2272
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && ["commit", "push", "subtree", "fetch"].includes(args[0] ?? "") && !(args[0] === "subtree" && args[1] === "split"));
2238
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && ["commit", "push", "subtree", "fetch", "ls-remote"].includes(args[0] ?? "") && !(args[0] === "subtree" && args[1] === "split"));
2273
2239
  const terminalState = interactive && process.platform !== "win32" ? execFileSync("stty", ["-g"], { stdio: ["inherit", "pipe", "ignore"], encoding: "utf8" }).trim() : void 0;
2240
+ if (interactive) onNativeOutput?.("start");
2274
2241
  const child = R2("git", args, {
2275
2242
  nodeOptions: {
2276
2243
  cwd,
2277
2244
  detached: !interactive && process.platform !== "win32",
2278
2245
  env: { ...process.env, GIT_TRACE2_EVENT: "3" },
2279
- stdio: interactive ? ["inherit", "inherit", "inherit", "pipe"] : ["pipe", "pipe", "pipe", "pipe"]
2246
+ stdio: interactive ? ["inherit", args[0] === "ls-remote" ? "pipe" : "inherit", "inherit", "pipe"] : ["pipe", "pipe", "pipe", "pipe"]
2280
2247
  }
2281
2248
  });
2282
2249
  let timeoutError;
@@ -2313,7 +2280,11 @@ async function git(args, cwd, onHook) {
2313
2280
  } finally {
2314
2281
  reader?.close();
2315
2282
  close();
2316
- if (terminalState) execFileSync("stty", [terminalState], { stdio: ["inherit", "ignore", "ignore"] });
2283
+ try {
2284
+ if (terminalState) execFileSync("stty", [terminalState], { stdio: ["inherit", "ignore", "ignore"] });
2285
+ } finally {
2286
+ if (interactive) onNativeOutput?.("exit");
2287
+ }
2317
2288
  }
2318
2289
  }
2319
2290
  function killProcessTree(pid) {
@@ -2346,12 +2317,18 @@ async function stageAll(repo) {
2346
2317
  async function getStagedDiff(repo) {
2347
2318
  return git(["diff", "--cached"], repo);
2348
2319
  }
2349
- async function commit(repo, message, onHook) {
2350
- await git(["commit", "--quiet", "-m", message], repo, onHook);
2320
+ async function commit(repo, message, onHook, onNativeOutput) {
2321
+ await git(["commit", "--quiet", "-m", message], repo, onHook, onNativeOutput);
2351
2322
  }
2352
2323
  async function currentBranch(repo) {
2353
2324
  return (await git(["symbolic-ref", "--short", "HEAD"], repo)).trim();
2354
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
+ }
2355
2332
  async function getRepoStatus(repo) {
2356
2333
  const lines = (await git(["status", "--porcelain=v2", "--branch", "--untracked-files=normal"], repo)).trim().split("\n");
2357
2334
  const branch = lines.find((line) => line.startsWith("# branch.head "))?.slice(14) ?? "HEAD";
@@ -2366,23 +2343,21 @@ async function getPushRemote(repo, branch) {
2366
2343
  if (upstream) return remotes.includes(upstream) ? upstream : null;
2367
2344
  return remotes.length === 1 ? remotes[0] : null;
2368
2345
  }
2369
- async function push(repo, remote, branch, onHook) {
2346
+ async function push(repo, remote, branch, onHook, prefix, onNativeOutput) {
2370
2347
  await git(["check-ref-format", "--branch", branch], repo);
2371
- await git(["push", "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook);
2348
+ await git(prefix ? ["subtree", "push", `--prefix=${prefix}`, remote, branch] : ["push", "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook, onNativeOutput);
2372
2349
  }
2373
- async function hasUnpushedCommits(repo, remote, branch, onHook, prefix) {
2350
+ async function hasUnpushedCommits(repo, remote, branch, onHook, prefix, onNativeOutput) {
2374
2351
  if (await tryGit(["rev-parse", "--verify", "HEAD"], repo) === null) return false;
2375
2352
  await git(["check-ref-format", "--branch", branch], repo);
2376
2353
  const source = prefix ? (await git(["subtree", "split", `--prefix=${prefix}`, "--quiet", "HEAD"], repo)).trim() : "HEAD";
2377
2354
  const ref = `refs/heads/${branch}`;
2378
- if (!(await git(["ls-remote", "--heads", "--", remote, ref], repo)).trim()) return true;
2379
- await git(["fetch", "--quiet", "--", remote, ref], repo, onHook);
2355
+ const refs = new Set((await git(["ls-remote", "--", remote, ref, "HEAD"], repo, onHook, onNativeOutput)).trim().split("\n").map((line) => line.split(" ")[1]));
2356
+ const target = refs.has(ref) ? ref : refs.has("HEAD") ? "HEAD" : null;
2357
+ if (!target) return true;
2358
+ await git(["fetch", "--quiet", "--", remote, target], repo, onHook, onNativeOutput);
2380
2359
  return Boolean((await git(["rev-list", "--max-count=1", `FETCH_HEAD..${source}`], repo)).trim());
2381
2360
  }
2382
- async function publishSubtree(repo, prefix, remote, branch, onHook) {
2383
- await git(["check-ref-format", "--branch", branch], repo);
2384
- await git(["subtree", "push", `--prefix=${prefix}`, remote, branch], repo, onHook);
2385
- }
2386
2361
  async function findRepos(path) {
2387
2362
  const root = await repoRoot(path);
2388
2363
  if (root) return [root];
@@ -2424,6 +2399,21 @@ var commit_message_default = "You are zapdev, a CLI tool that creates commits qu
2424
2399
  // src/prompts/index.ts
2425
2400
  var COMMIT_SYSTEM_PROMPT = commit_message_default.trim();
2426
2401
 
2402
+ // src/lib/commit-message.ts
2403
+ function applyCommitType(systemPrompt, type) {
2404
+ return `${systemPrompt}
2405
+
2406
+ The type MUST be exactly "${type}".`;
2407
+ }
2408
+ var MAX_DIFF_CHARS = 12e3;
2409
+ function truncateDiff(diff, max = MAX_DIFF_CHARS) {
2410
+ return diff.length > max ? diff.slice(0, max) : diff;
2411
+ }
2412
+ function sanitizeCommitMessage(raw) {
2413
+ const firstLine = raw.split("\n", 1)[0] ?? "";
2414
+ return firstLine.replace(/[`"]/g, "").trim();
2415
+ }
2416
+
2427
2417
  // src/lib/llm.ts
2428
2418
  var REQUEST_TIMEOUT_MS = 25e3;
2429
2419
  async function generateCommitMessage(diff, config, type) {
@@ -2463,17 +2453,11 @@ async function generateCommitMessage(diff, config, type) {
2463
2453
  return sanitizeCommitMessage(content);
2464
2454
  }
2465
2455
  function describeRequestError(error) {
2466
- if (errorName(error) === "TimeoutError") {
2456
+ if (error instanceof Error && error.name === "TimeoutError") {
2467
2457
  return `LLM did not answer within ${REQUEST_TIMEOUT_MS / 1e3}s.`;
2468
2458
  }
2469
2459
  return "Could not complete the LLM request. Check URL and endpoint availability.";
2470
2460
  }
2471
- function errorName(error) {
2472
- if (error && typeof error === "object" && "name" in error && typeof error.name === "string") {
2473
- return error.name;
2474
- }
2475
- return null;
2476
- }
2477
2461
  function serverError(body) {
2478
2462
  if (!body || typeof body !== "object" || !("error" in body)) return null;
2479
2463
  const error = body.error;
@@ -2491,6 +2475,28 @@ function messageContent(body) {
2491
2475
  return typeof message.content === "string" ? message.content : null;
2492
2476
  }
2493
2477
 
2478
+ // src/types/commit.ts
2479
+ var COMMIT_TYPES = [
2480
+ "feat",
2481
+ "fix",
2482
+ "chore",
2483
+ "docs",
2484
+ "style",
2485
+ "refactor",
2486
+ "perf",
2487
+ "test",
2488
+ "build",
2489
+ "ci",
2490
+ "revert"
2491
+ ];
2492
+
2493
+ // src/commands/native-output.ts
2494
+ import { styleText as styleText3 } from "node:util";
2495
+ var reportNativeOutput = (phase) => {
2496
+ const symbol2 = styleText3("gray", phase === "start" ? S_BAR_END : S_BAR_START);
2497
+ log.message(phase === "start" ? symbol2 : ["", symbol2], { withGuide: false, spacing: 0 });
2498
+ };
2499
+
2494
2500
  // src/commands/commit.ts
2495
2501
  var commitCommand = defineCommand({
2496
2502
  meta: {
@@ -2511,14 +2517,16 @@ var commitCommand = defineCommand({
2511
2517
  description: "Override $ZD_EFFORT, sent as reasoning_effort."
2512
2518
  },
2513
2519
  type: {
2514
- type: "string",
2520
+ type: "enum",
2521
+ options: [...COMMIT_TYPES],
2522
+ required: false,
2515
2523
  alias: "t",
2516
2524
  description: `Force the Conventional Commits type (${COMMIT_TYPES.join(", ")}).`
2517
2525
  },
2518
2526
  push: {
2519
2527
  type: "boolean",
2520
2528
  alias: "p",
2521
- description: "Skip push confirmation; still ask for each destination branch."
2529
+ description: "Skip push confirmation and push the current branch."
2522
2530
  },
2523
2531
  staged: {
2524
2532
  type: "boolean",
@@ -2533,14 +2541,6 @@ var commitCommand = defineCommand({
2533
2541
  },
2534
2542
  async run({ args }) {
2535
2543
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2536
- const type = args.type ? normalizeCommitType(args.type) : void 0;
2537
- if (type === null) {
2538
- log.error(
2539
- `Invalid type "${args.type}". Valid types: ${COMMIT_TYPES.join(", ")}.`
2540
- );
2541
- process.exitCode = 1;
2542
- return;
2543
- }
2544
2544
  let config;
2545
2545
  let subtrees;
2546
2546
  try {
@@ -2580,12 +2580,12 @@ var commitCommand = defineCommand({
2580
2580
  const { repo } = repository;
2581
2581
  const { branch, hasChanges } = await getRepoStatus(repo);
2582
2582
  repository.label = `${basename(repo)} (${branch})`;
2583
- repository.pendingLabel = hasChanges ? styleText3(["bold", "underline"], repository.label) : repository.label;
2583
+ repository.pendingLabel = hasChanges ? styleText4(["bold", "underline"], repository.label) : repository.label;
2584
2584
  if (!args.staged) await stageAll(repo);
2585
2585
  const diff = await getStagedDiff(repo);
2586
2586
  if (!diff.trim()) return null;
2587
2587
  if (scan) await scanStagedChanges(repo);
2588
- const message = await generateCommitMessage(diff, config, type);
2588
+ const message = await generateCommitMessage(diff, config, args.type);
2589
2589
  if (!message) throw new Error("The model returned an empty message.");
2590
2590
  return { ...repository, message };
2591
2591
  }));
@@ -2608,19 +2608,39 @@ var commitCommand = defineCommand({
2608
2608
  cancel("Cancelled (changes left staged).");
2609
2609
  return;
2610
2610
  }
2611
- const committed = [];
2611
+ const toSend = [];
2612
+ let previousBranchInput = "";
2612
2613
  for (const draft of selected) {
2613
- log.info(`${draft.label}: committing`);
2614
2614
  try {
2615
- await commit(draft.repo, draft.message, reportHooks(draft.label));
2616
- committed.push(draft);
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`);
2635
+ await commit(draft.repo, draft.message, reportHooks(draft.label), reportNativeOutput);
2636
+ toSend.push(draft);
2617
2637
  log.success(`${draft.label}: committed ${draft.message}`);
2618
2638
  } catch (error) {
2619
2639
  reportGitFailure(`${draft.pendingLabel}: commit failed`, error);
2620
2640
  process.exitCode = 1;
2621
2641
  }
2622
2642
  }
2623
- const toSend = [...committed, ...unchanged];
2643
+ toSend.push(...unchanged);
2624
2644
  if (toSend.length === 0) return;
2625
2645
  let shouldPush = Boolean(args.push);
2626
2646
  if (!shouldPush && interactive) {
@@ -2635,14 +2655,7 @@ var commitCommand = defineCommand({
2635
2655
  shouldPush = answer;
2636
2656
  }
2637
2657
  if (shouldPush) {
2638
- if (!interactive) {
2639
- log.error("Pushing requires a terminal to choose each destination branch. Commits remain local.");
2640
- process.exitCode = 1;
2641
- return;
2642
- }
2643
- let previousBranchInput = "";
2644
- for (const repository of toSend) {
2645
- const { repo, label } = repository;
2658
+ for (const { repo, label } of toSend) {
2646
2659
  try {
2647
2660
  const current = await currentBranch(repo);
2648
2661
  const destinations = publishSubtreeMode ? Object.entries(subtrees) : [["", await getPushRemote(repo, current)]];
@@ -2651,24 +2664,14 @@ var commitCommand = defineCommand({
2651
2664
  log.warn(`${label}: no remote or ambiguous remote choice. Skipping push; commit remains local.`);
2652
2665
  continue;
2653
2666
  }
2654
- if (!await hasUnpushedCommits(repo, remote, previousBranchInput || current, reportHooks(label), prefix || void 0)) {
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)) {
2655
2669
  log.info(`${label}: no unpushed commits${prefix ? ` in ${prefix}` : ""}. Skipping.`);
2656
2670
  continue;
2657
2671
  }
2658
- const answer = await text({
2659
- message: `${label}: branch to push ${publishSubtreeMode ? `${prefix} to ` : "to "}${remote}`,
2660
- initialValue: previousBranchInput
2661
- });
2662
- if (isCancel(answer)) {
2663
- outro("Sending cancelled. Remaining commits stay local.");
2664
- return;
2665
- }
2666
- previousBranchInput = answer.trim();
2667
- const branch = previousBranchInput || current;
2668
- log.info(`${label}: pushing ${publishSubtreeMode ? `${prefix} \u2192 ` : ""}${remote}/${branch}`);
2669
- if (publishSubtreeMode) await publishSubtree(repo, prefix, remote, branch, reportHooks(label));
2670
- else await push(repo, remote, branch, reportHooks(label));
2671
- 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}`);
2672
2675
  }
2673
2676
  } catch (error) {
2674
2677
  reportGitFailure(`${label}: ${publishSubtreeMode ? "publication" : "push"} failed`, error);
@@ -2686,11 +2689,11 @@ function reportGitFailure(message, error) {
2686
2689
  log.error(error instanceof GitOutputError ? message : `${message}: ${errorMessage(error)}`);
2687
2690
  }
2688
2691
  function reportHooks(label) {
2689
- const options = { secondarySymbol: "" };
2692
+ const options = { secondarySymbol: "", withGuide: false };
2690
2693
  return (event) => {
2691
2694
  const hook = `${label}: ${event.name}`;
2692
2695
  if (event.phase === "start") {
2693
- log.step(styleText3("bold", hook), options);
2696
+ log.step(styleText4("bold", hook), options);
2694
2697
  } else {
2695
2698
  if (event.exitCode === 0) log.success(`${hook}: completed`, options);
2696
2699
  else log.error(`${hook}: failed (exit ${event.exitCode})`, options);
@@ -2784,7 +2787,7 @@ async function initializeSubtrees(directory, sources, options = {}) {
2784
2787
  const imports = [];
2785
2788
  for (const source of sources) {
2786
2789
  options.onProgress?.(`Detecting default branch for ${source.name}`);
2787
- const refs = await git(["ls-remote", "--symref", "--", source.url, "HEAD"], repo);
2790
+ const refs = await git(["ls-remote", "--symref", "--", source.url, "HEAD"], repo, void 0, options.onNativeOutput);
2788
2791
  const branch = /^ref: refs\/heads\/(.+)\tHEAD$/m.exec(refs)?.[1];
2789
2792
  if (!branch) options.onWarning?.(`Could not detect default branch for ${source.name}; using main.`);
2790
2793
  imports.push({ ...source, branch: branch ?? "main" });
@@ -2795,12 +2798,12 @@ async function initializeSubtrees(directory, sources, options = {}) {
2795
2798
  `);
2796
2799
  await git(["add", "--", "zapdev.json"], repo);
2797
2800
  if (initial || (await getStagedDiff(repo)).trim()) {
2798
- await commit(repo, initial ? "chore: init" : "chore: configure subtrees");
2801
+ await commit(repo, initial ? "chore: init" : "chore: configure subtrees", void 0, options.onNativeOutput);
2799
2802
  }
2800
2803
  for (const [name, url] of missingRemotes) await git(["remote", "add", name, url], repo);
2801
2804
  for (const { name, branch } of imports) {
2802
2805
  options.onProgress?.(`Adding ${name} (${branch})`);
2803
- await git(["subtree", "add", `--prefix=${name}`, name, branch, "--squash"], repo);
2806
+ await git(["subtree", "add", `--prefix=${name}`, name, branch, "--squash"], repo, void 0, options.onNativeOutput);
2804
2807
  }
2805
2808
  }
2806
2809
  async function exists(path) {
@@ -2826,7 +2829,8 @@ var subtreeInitCommand = defineCommand({
2826
2829
  await initializeSubtrees(args.directory, sources, {
2827
2830
  origin: args.origin,
2828
2831
  onProgress: log.info,
2829
- onWarning: log.warn
2832
+ onWarning: log.warn,
2833
+ onNativeOutput: reportNativeOutput
2830
2834
  });
2831
2835
  if (interactive) outro("Subtrees ready.");
2832
2836
  else log.info("Subtrees ready.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.9.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": {