zapdev 1.9.0 → 1.10.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 +2 -2
  2. package/dist/cli.js +78 -84
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -124,7 +124,7 @@ 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.) |
127
+ | `-t, --type <type>` | Force an exact lowercase Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
128
128
  | `-p, --push` | Skip push confirmation; still ask for each destination 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 |
@@ -144,7 +144,7 @@ In a terminal, Git and its hooks display live logs and support native prompts, e
144
144
 
145
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.
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
+ 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.
148
148
 
149
149
  Without a TTY, zapdev commits all prepared repositories automatically but cannot push; `--push` reports an error and leaves commits local.
150
150
 
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.10.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,8 +2317,8 @@ 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();
@@ -2366,23 +2337,21 @@ async function getPushRemote(repo, branch) {
2366
2337
  if (upstream) return remotes.includes(upstream) ? upstream : null;
2367
2338
  return remotes.length === 1 ? remotes[0] : null;
2368
2339
  }
2369
- async function push(repo, remote, branch, onHook) {
2340
+ async function push(repo, remote, branch, onHook, prefix, onNativeOutput) {
2370
2341
  await git(["check-ref-format", "--branch", branch], repo);
2371
- await git(["push", "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook);
2342
+ await git(prefix ? ["subtree", "push", `--prefix=${prefix}`, remote, branch] : ["push", "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook, onNativeOutput);
2372
2343
  }
2373
- async function hasUnpushedCommits(repo, remote, branch, onHook, prefix) {
2344
+ async function hasUnpushedCommits(repo, remote, branch, onHook, prefix, onNativeOutput) {
2374
2345
  if (await tryGit(["rev-parse", "--verify", "HEAD"], repo) === null) return false;
2375
2346
  await git(["check-ref-format", "--branch", branch], repo);
2376
2347
  const source = prefix ? (await git(["subtree", "split", `--prefix=${prefix}`, "--quiet", "HEAD"], repo)).trim() : "HEAD";
2377
2348
  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);
2349
+ const refs = new Set((await git(["ls-remote", "--", remote, ref, "HEAD"], repo, onHook, onNativeOutput)).trim().split("\n").map((line) => line.split(" ")[1]));
2350
+ const target = refs.has(ref) ? ref : refs.has("HEAD") ? "HEAD" : null;
2351
+ if (!target) return true;
2352
+ await git(["fetch", "--quiet", "--", remote, target], repo, onHook, onNativeOutput);
2380
2353
  return Boolean((await git(["rev-list", "--max-count=1", `FETCH_HEAD..${source}`], repo)).trim());
2381
2354
  }
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
2355
  async function findRepos(path) {
2387
2356
  const root = await repoRoot(path);
2388
2357
  if (root) return [root];
@@ -2424,6 +2393,21 @@ var commit_message_default = "You are zapdev, a CLI tool that creates commits qu
2424
2393
  // src/prompts/index.ts
2425
2394
  var COMMIT_SYSTEM_PROMPT = commit_message_default.trim();
2426
2395
 
2396
+ // src/lib/commit-message.ts
2397
+ function applyCommitType(systemPrompt, type) {
2398
+ return `${systemPrompt}
2399
+
2400
+ The type MUST be exactly "${type}".`;
2401
+ }
2402
+ var MAX_DIFF_CHARS = 12e3;
2403
+ function truncateDiff(diff, max = MAX_DIFF_CHARS) {
2404
+ return diff.length > max ? diff.slice(0, max) : diff;
2405
+ }
2406
+ function sanitizeCommitMessage(raw) {
2407
+ const firstLine = raw.split("\n", 1)[0] ?? "";
2408
+ return firstLine.replace(/[`"]/g, "").trim();
2409
+ }
2410
+
2427
2411
  // src/lib/llm.ts
2428
2412
  var REQUEST_TIMEOUT_MS = 25e3;
2429
2413
  async function generateCommitMessage(diff, config, type) {
@@ -2463,17 +2447,11 @@ async function generateCommitMessage(diff, config, type) {
2463
2447
  return sanitizeCommitMessage(content);
2464
2448
  }
2465
2449
  function describeRequestError(error) {
2466
- if (errorName(error) === "TimeoutError") {
2450
+ if (error instanceof Error && error.name === "TimeoutError") {
2467
2451
  return `LLM did not answer within ${REQUEST_TIMEOUT_MS / 1e3}s.`;
2468
2452
  }
2469
2453
  return "Could not complete the LLM request. Check URL and endpoint availability.";
2470
2454
  }
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
2455
  function serverError(body) {
2478
2456
  if (!body || typeof body !== "object" || !("error" in body)) return null;
2479
2457
  const error = body.error;
@@ -2491,6 +2469,28 @@ function messageContent(body) {
2491
2469
  return typeof message.content === "string" ? message.content : null;
2492
2470
  }
2493
2471
 
2472
+ // src/types/commit.ts
2473
+ var COMMIT_TYPES = [
2474
+ "feat",
2475
+ "fix",
2476
+ "chore",
2477
+ "docs",
2478
+ "style",
2479
+ "refactor",
2480
+ "perf",
2481
+ "test",
2482
+ "build",
2483
+ "ci",
2484
+ "revert"
2485
+ ];
2486
+
2487
+ // src/commands/native-output.ts
2488
+ import { styleText as styleText3 } from "node:util";
2489
+ var reportNativeOutput = (phase) => {
2490
+ const symbol2 = styleText3("gray", phase === "start" ? S_BAR_END : S_BAR_START);
2491
+ log.message(phase === "start" ? symbol2 : ["", symbol2], { withGuide: false, spacing: 0 });
2492
+ };
2493
+
2494
2494
  // src/commands/commit.ts
2495
2495
  var commitCommand = defineCommand({
2496
2496
  meta: {
@@ -2511,7 +2511,9 @@ var commitCommand = defineCommand({
2511
2511
  description: "Override $ZD_EFFORT, sent as reasoning_effort."
2512
2512
  },
2513
2513
  type: {
2514
- type: "string",
2514
+ type: "enum",
2515
+ options: [...COMMIT_TYPES],
2516
+ required: false,
2515
2517
  alias: "t",
2516
2518
  description: `Force the Conventional Commits type (${COMMIT_TYPES.join(", ")}).`
2517
2519
  },
@@ -2533,14 +2535,6 @@ var commitCommand = defineCommand({
2533
2535
  },
2534
2536
  async run({ args }) {
2535
2537
  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
2538
  let config;
2545
2539
  let subtrees;
2546
2540
  try {
@@ -2580,12 +2574,12 @@ var commitCommand = defineCommand({
2580
2574
  const { repo } = repository;
2581
2575
  const { branch, hasChanges } = await getRepoStatus(repo);
2582
2576
  repository.label = `${basename(repo)} (${branch})`;
2583
- repository.pendingLabel = hasChanges ? styleText3(["bold", "underline"], repository.label) : repository.label;
2577
+ repository.pendingLabel = hasChanges ? styleText4(["bold", "underline"], repository.label) : repository.label;
2584
2578
  if (!args.staged) await stageAll(repo);
2585
2579
  const diff = await getStagedDiff(repo);
2586
2580
  if (!diff.trim()) return null;
2587
2581
  if (scan) await scanStagedChanges(repo);
2588
- const message = await generateCommitMessage(diff, config, type);
2582
+ const message = await generateCommitMessage(diff, config, args.type);
2589
2583
  if (!message) throw new Error("The model returned an empty message.");
2590
2584
  return { ...repository, message };
2591
2585
  }));
@@ -2608,19 +2602,19 @@ var commitCommand = defineCommand({
2608
2602
  cancel("Cancelled (changes left staged).");
2609
2603
  return;
2610
2604
  }
2611
- const committed = [];
2605
+ const toSend = [];
2612
2606
  for (const draft of selected) {
2613
2607
  log.info(`${draft.label}: committing`);
2614
2608
  try {
2615
- await commit(draft.repo, draft.message, reportHooks(draft.label));
2616
- committed.push(draft);
2609
+ await commit(draft.repo, draft.message, reportHooks(draft.label), reportNativeOutput);
2610
+ toSend.push(draft);
2617
2611
  log.success(`${draft.label}: committed ${draft.message}`);
2618
2612
  } catch (error) {
2619
2613
  reportGitFailure(`${draft.pendingLabel}: commit failed`, error);
2620
2614
  process.exitCode = 1;
2621
2615
  }
2622
2616
  }
2623
- const toSend = [...committed, ...unchanged];
2617
+ toSend.push(...unchanged);
2624
2618
  if (toSend.length === 0) return;
2625
2619
  let shouldPush = Boolean(args.push);
2626
2620
  if (!shouldPush && interactive) {
@@ -2641,8 +2635,7 @@ var commitCommand = defineCommand({
2641
2635
  return;
2642
2636
  }
2643
2637
  let previousBranchInput = "";
2644
- for (const repository of toSend) {
2645
- const { repo, label } = repository;
2638
+ for (const { repo, label } of toSend) {
2646
2639
  try {
2647
2640
  const current = await currentBranch(repo);
2648
2641
  const destinations = publishSubtreeMode ? Object.entries(subtrees) : [["", await getPushRemote(repo, current)]];
@@ -2651,7 +2644,8 @@ var commitCommand = defineCommand({
2651
2644
  log.warn(`${label}: no remote or ambiguous remote choice. Skipping push; commit remains local.`);
2652
2645
  continue;
2653
2646
  }
2654
- if (!await hasUnpushedCommits(repo, remote, previousBranchInput || current, reportHooks(label), prefix || void 0)) {
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)) {
2655
2649
  log.info(`${label}: no unpushed commits${prefix ? ` in ${prefix}` : ""}. Skipping.`);
2656
2650
  continue;
2657
2651
  }
@@ -2666,8 +2660,7 @@ var commitCommand = defineCommand({
2666
2660
  previousBranchInput = answer.trim();
2667
2661
  const branch = previousBranchInput || current;
2668
2662
  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));
2663
+ await push(repo, remote, branch, reportHooks(label), prefix || void 0, reportNativeOutput);
2671
2664
  log.success(`${label}: pushed to ${remote}/${branch}`);
2672
2665
  }
2673
2666
  } catch (error) {
@@ -2686,11 +2679,11 @@ function reportGitFailure(message, error) {
2686
2679
  log.error(error instanceof GitOutputError ? message : `${message}: ${errorMessage(error)}`);
2687
2680
  }
2688
2681
  function reportHooks(label) {
2689
- const options = { secondarySymbol: "" };
2682
+ const options = { secondarySymbol: "", withGuide: false };
2690
2683
  return (event) => {
2691
2684
  const hook = `${label}: ${event.name}`;
2692
2685
  if (event.phase === "start") {
2693
- log.step(styleText3("bold", hook), options);
2686
+ log.step(styleText4("bold", hook), options);
2694
2687
  } else {
2695
2688
  if (event.exitCode === 0) log.success(`${hook}: completed`, options);
2696
2689
  else log.error(`${hook}: failed (exit ${event.exitCode})`, options);
@@ -2784,7 +2777,7 @@ async function initializeSubtrees(directory, sources, options = {}) {
2784
2777
  const imports = [];
2785
2778
  for (const source of sources) {
2786
2779
  options.onProgress?.(`Detecting default branch for ${source.name}`);
2787
- const refs = await git(["ls-remote", "--symref", "--", source.url, "HEAD"], repo);
2780
+ const refs = await git(["ls-remote", "--symref", "--", source.url, "HEAD"], repo, void 0, options.onNativeOutput);
2788
2781
  const branch = /^ref: refs\/heads\/(.+)\tHEAD$/m.exec(refs)?.[1];
2789
2782
  if (!branch) options.onWarning?.(`Could not detect default branch for ${source.name}; using main.`);
2790
2783
  imports.push({ ...source, branch: branch ?? "main" });
@@ -2795,12 +2788,12 @@ async function initializeSubtrees(directory, sources, options = {}) {
2795
2788
  `);
2796
2789
  await git(["add", "--", "zapdev.json"], repo);
2797
2790
  if (initial || (await getStagedDiff(repo)).trim()) {
2798
- await commit(repo, initial ? "chore: init" : "chore: configure subtrees");
2791
+ await commit(repo, initial ? "chore: init" : "chore: configure subtrees", void 0, options.onNativeOutput);
2799
2792
  }
2800
2793
  for (const [name, url] of missingRemotes) await git(["remote", "add", name, url], repo);
2801
2794
  for (const { name, branch } of imports) {
2802
2795
  options.onProgress?.(`Adding ${name} (${branch})`);
2803
- await git(["subtree", "add", `--prefix=${name}`, name, branch, "--squash"], repo);
2796
+ await git(["subtree", "add", `--prefix=${name}`, name, branch, "--squash"], repo, void 0, options.onNativeOutput);
2804
2797
  }
2805
2798
  }
2806
2799
  async function exists(path) {
@@ -2826,7 +2819,8 @@ var subtreeInitCommand = defineCommand({
2826
2819
  await initializeSubtrees(args.directory, sources, {
2827
2820
  origin: args.origin,
2828
2821
  onProgress: log.info,
2829
- onWarning: log.warn
2822
+ onWarning: log.warn,
2823
+ onNativeOutput: reportNativeOutput
2830
2824
  });
2831
2825
  if (interactive) outro("Subtrees ready.");
2832
2826
  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.10.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": {