zapdev 1.12.0 → 1.13.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 +181 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -145,11 +145,11 @@ Preparation and commit failures are reported per repository while the others con
145
145
 
146
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. Missing upstreams, divergent history, or conflicting local changes stop the entire flow before staging; no rebase, merge commit, or automatic stash is performed. Earlier successful pulls remain applied.
147
147
 
148
- 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`.
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`.
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 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>`. Missing or ambiguous remotes are skipped with a warning. Pushes 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, pushing is attempted. Already published history is skipped. 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 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>`. Missing or ambiguous remotes are skipped with a warning. 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.
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,6 +830,7 @@ function isUnicodeSupported() {
830
830
  }
831
831
  var unicode = isUnicodeSupported();
832
832
  var isCI = () => process.env.CI === "true";
833
+ var isTTY = (o2) => o2.isTTY === true;
833
834
  var unicodeOr = (o2, e2) => unicode ? o2 : e2;
834
835
  var S_STEP_ACTIVE = unicodeOr("\u25C6", "*");
835
836
  var S_STEP_CANCEL = unicodeOr("\u25A0", "x");
@@ -1075,7 +1076,7 @@ var spinner = ({
1075
1076
  const x2 = getColumns(n4), k2 = I3?.styleFrame ?? W, g2 = (e2) => {
1076
1077
  const r3 = e2 > 1 ? O2 ?? settings.messages.error : G ?? settings.messages.cancel;
1077
1078
  S2 = e2 === 1, d2 && (a3(r3, e2), S2 && typeof h3 == "function" && h3());
1078
- }, f2 = () => g2(2), i3 = () => g2(1), A = () => {
1079
+ }, f2 = () => g2(2), i3 = () => g2(1), A2 = () => {
1079
1080
  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);
1080
1081
  }, H = () => {
1081
1082
  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);
@@ -1096,7 +1097,7 @@ var spinner = ({
1096
1097
  d2 = true, M = block({ output: n4 }), s2 = C2(e2), w2 = performance.now(), N2 && n4.write(`${styleText2("gray", S_BAR)}
1097
1098
  `);
1098
1099
  let r3 = 0, t3 = 0;
1099
- A(), T2 = setInterval(() => {
1100
+ A2(), T2 = setInterval(() => {
1100
1101
  if (u4 && s2 === p2)
1101
1102
  return;
1102
1103
  y2(), p2 = s2;
@@ -1224,6 +1225,123 @@ ${b2}
1224
1225
  }).prompt();
1225
1226
  };
1226
1227
  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
+ };
1227
1345
  var text = (e2) => new n2({
1228
1346
  validate: e2.validate,
1229
1347
  placeholder: e2.placeholder,
@@ -1736,7 +1854,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
1736
1854
  // package.json
1737
1855
  var package_default = {
1738
1856
  name: "zapdev",
1739
- version: "1.12.0",
1857
+ version: "1.13.0",
1740
1858
  description: "Fast, performant and lightweight git chores and dev tools using your terminal and an OpenAI-compatible LLM endpoint.",
1741
1859
  type: "module",
1742
1860
  bin: {
@@ -1796,7 +1914,7 @@ var package_default = {
1796
1914
 
1797
1915
  // src/commands/commit.ts
1798
1916
  import { basename } from "node:path";
1799
- import { styleText as styleText3 } from "node:util";
1917
+ import { styleText as styleText4 } from "node:util";
1800
1918
 
1801
1919
  // src/lib/config.ts
1802
1920
  function resolveHookTimeout(env = process.env) {
@@ -2208,18 +2326,23 @@ function createHookReporter(onHook, onTimeout, timeout) {
2208
2326
  }
2209
2327
 
2210
2328
  // src/lib/git.ts
2211
- async function git(args, cwd, onHook) {
2329
+ async function git(args, cwd, onHook, onOutput) {
2212
2330
  const timeout = resolveHookTimeout();
2213
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && ["commit", "pull", "push", "fetch", "ls-remote"].includes(args[0] ?? ""));
2331
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && ["commit", "pull", "push"].includes(args[0] ?? ""));
2214
2332
  const terminalState = interactive && process.platform !== "win32" ? execFileSync("stty", ["-g"], { stdio: ["inherit", "pipe", "ignore"], encoding: "utf8" }).trim() : void 0;
2215
2333
  const child = R2("git", args, {
2216
2334
  nodeOptions: {
2217
2335
  cwd,
2218
2336
  detached: !interactive && process.platform !== "win32",
2219
2337
  env: { ...process.env, GIT_TRACE2_EVENT: "3" },
2220
- stdio: interactive ? ["inherit", args[0] === "ls-remote" ? "pipe" : "inherit", "inherit", "pipe"] : ["pipe", "pipe", "pipe", "pipe"]
2338
+ stdio: interactive ? ["inherit", onOutput ? "pipe" : "inherit", onOutput ? "pipe" : "inherit", "pipe"] : ["pipe", "pipe", "pipe", "pipe"]
2221
2339
  }
2222
2340
  });
2341
+ if (onOutput) {
2342
+ for (const stream of ["stdout", "stderr"]) {
2343
+ child.process?.[stream]?.setEncoding("utf8").on("data", (chunk) => onOutput(chunk, stream));
2344
+ }
2345
+ }
2223
2346
  let timeoutError;
2224
2347
  let hookFailureReported = false;
2225
2348
  const { report, close } = createHookReporter((event) => {
@@ -2247,7 +2370,7 @@ async function git(args, cwd, onHook) {
2247
2370
  if (timeoutError) throw timeoutError;
2248
2371
  if (result.exitCode !== 0) {
2249
2372
  const message = result.stderr.trim() || `git ${args.join(" ")} failed`;
2250
- if (interactive) throw new GitOutputError(message, hookFailureReported);
2373
+ if (interactive || onOutput) throw new GitOutputError(message, hookFailureReported);
2251
2374
  throw new Error(message);
2252
2375
  }
2253
2376
  return result.stdout;
@@ -2297,8 +2420,8 @@ async function getStagedDiffStats(repo) {
2297
2420
  }
2298
2421
  return stats;
2299
2422
  }
2300
- async function commit(repo, message, onHook) {
2301
- await git(["commit", "--quiet", "-m", message], repo, onHook);
2423
+ async function commit(repo, message, onHook, onOutput) {
2424
+ await git(["commit", "--quiet", "-m", message], repo, onHook, onOutput);
2302
2425
  }
2303
2426
  async function currentBranch(repo) {
2304
2427
  return (await git(["symbolic-ref", "--short", "HEAD"], repo)).trim();
@@ -2323,19 +2446,9 @@ async function getPushRemote(repo, branch) {
2323
2446
  if (upstream) return remotes.includes(upstream) ? upstream : null;
2324
2447
  return remotes.length === 1 ? remotes[0] : null;
2325
2448
  }
2326
- async function push(repo, remote, branch, onHook) {
2327
- await git(["check-ref-format", "--branch", branch], repo);
2328
- await git(["push", "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook);
2329
- }
2330
- async function hasUnpushedCommits(repo, remote, branch, onHook) {
2331
- if (await tryGit(["rev-parse", "--verify", "HEAD"], repo) === null) return false;
2449
+ async function push(repo, remote, branch, onHook, onOutput) {
2332
2450
  await git(["check-ref-format", "--branch", branch], repo);
2333
- const ref = `refs/heads/${branch}`;
2334
- const refs = new Set((await git(["ls-remote", "--", remote, ref, "HEAD"], repo, onHook)).trim().split("\n").map((line) => line.split(" ")[1]));
2335
- const target = refs.has(ref) ? ref : refs.has("HEAD") ? "HEAD" : null;
2336
- if (!target) return true;
2337
- await git(["fetch", "--quiet", "--", remote, target], repo, onHook);
2338
- return Boolean((await git(["rev-list", "--max-count=1", "FETCH_HEAD..HEAD"], repo)).trim());
2451
+ await git(["push", "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook, onOutput);
2339
2452
  }
2340
2453
  async function findRepos(path) {
2341
2454
  const root = await repoRoot(path);
@@ -2469,6 +2582,42 @@ var COMMIT_TYPES = [
2469
2582
  "revert"
2470
2583
  ];
2471
2584
 
2585
+ // src/commands/git-task.ts
2586
+ import { styleText as styleText3 } from "node:util";
2587
+ async function runGitTask(label, operation, successMessage, run) {
2588
+ const title = `${label}: ${operation}`;
2589
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2590
+ let task;
2591
+ const onOutput = (chunk, stream) => {
2592
+ if (!interactive) {
2593
+ process[stream].write(chunk);
2594
+ return;
2595
+ }
2596
+ task ??= taskLog({ title, limit: 10, retainLog: true });
2597
+ task.message(chunk, { raw: true });
2598
+ };
2599
+ const onHook = (event) => {
2600
+ const hook = `${label}: ${event.name}`;
2601
+ const message = event.phase === "start" ? hook : `${hook}: ${event.exitCode === 0 ? "completed" : `failed (exit ${event.exitCode})`}`;
2602
+ if (task) task.message(message);
2603
+ else if (event.phase === "start") log.step(styleText3("bold", message));
2604
+ else if (event.exitCode === 0) log.success(message);
2605
+ else log.error(message);
2606
+ };
2607
+ log.info(title);
2608
+ try {
2609
+ await run(onHook, onOutput);
2610
+ if (task) task.success(successMessage, { showLog: true });
2611
+ else log.success(successMessage);
2612
+ return true;
2613
+ } catch (error) {
2614
+ const message = `${label}: ${operation} failed${error instanceof GitOutputError ? "" : `: ${errorMessage(error)}`}`;
2615
+ if (task) task.error(message);
2616
+ else if (!(error instanceof GitOutputError && error.hookFailureReported)) log.error(message);
2617
+ return false;
2618
+ }
2619
+ }
2620
+
2472
2621
  // src/commands/commit.ts
2473
2622
  var commitCommand = defineCommand({
2474
2623
  meta: {
@@ -2540,11 +2689,7 @@ var commitCommand = defineCommand({
2540
2689
  if (args.pull) {
2541
2690
  for (const repo of repos) {
2542
2691
  const label = basename(repo);
2543
- try {
2544
- log.info(`${label}: pulling`);
2545
- await git(["pull", "--ff-only", "--no-rebase", "--no-autostash"], repo, reportHooks(label));
2546
- } catch (error) {
2547
- reportGitFailure(`${label}: pull failed`, error);
2692
+ if (!await runGitTask(label, "pull", `${label}: pulled`, (onHook, onOutput) => git(["pull", "--ff-only", "--no-rebase", "--no-autostash"], repo, onHook, onOutput))) {
2548
2693
  process.exitCode = 1;
2549
2694
  return;
2550
2695
  }
@@ -2566,7 +2711,7 @@ var commitCommand = defineCommand({
2566
2711
  const { repo } = repository;
2567
2712
  const { branch, hasChanges } = await getRepoStatus(repo);
2568
2713
  repository.label = `${basename(repo)} (${branch})`;
2569
- repository.pendingLabel = hasChanges ? styleText3("bold", repository.label) : repository.label;
2714
+ repository.pendingLabel = hasChanges ? styleText4("bold", repository.label) : repository.label;
2570
2715
  if (!args.staged) await stageAll(repo);
2571
2716
  const diff = await getStagedDiff(repo);
2572
2717
  if (!diff.trim()) return null;
@@ -2615,13 +2760,14 @@ var commitCommand = defineCommand({
2615
2760
  if (branch !== current) {
2616
2761
  await switchBranch(draft.repo, branch);
2617
2762
  draft.label = `${basename(draft.repo)} (${branch})`;
2618
- draft.pendingLabel = styleText3("bold", draft.label);
2763
+ draft.pendingLabel = styleText4("bold", draft.label);
2619
2764
  }
2620
2765
  }
2621
- log.info(`${draft.label}: committing`);
2622
- await commit(draft.repo, draft.message, reportHooks(draft.label));
2766
+ if (!await runGitTask(draft.label, "commit", `${draft.label}: committed ${draft.message}`, (onHook, onOutput) => commit(draft.repo, draft.message, onHook, onOutput))) {
2767
+ process.exitCode = 1;
2768
+ continue;
2769
+ }
2623
2770
  toSend.push(draft);
2624
- log.success(`${draft.label}: committed ${draft.message}`);
2625
2771
  } catch (error) {
2626
2772
  reportGitFailure(`${draft.pendingLabel}: commit failed`, error);
2627
2773
  process.exitCode = 1;
@@ -2657,17 +2803,7 @@ var commitCommand = defineCommand({
2657
2803
  }
2658
2804
  if (shouldPush) {
2659
2805
  for (const { repo, label, branch, remote } of destinations) {
2660
- try {
2661
- log.info(`${label}: checking unpublished commits (${remote}/${branch}).`);
2662
- if (!await hasUnpushedCommits(repo, remote, branch, reportHooks(label))) {
2663
- log.info(`${label}: no unpushed commits. Skipping.`);
2664
- continue;
2665
- }
2666
- log.info(`${label}: pushing ${remote}/${branch}`);
2667
- await push(repo, remote, branch, reportHooks(label));
2668
- log.success(`${label}: pushed to ${remote}/${branch}`);
2669
- } catch (error) {
2670
- reportGitFailure(`${label}: push failed`, error);
2806
+ if (!await runGitTask(label, "push", `${label}: pushed to ${remote}/${branch}`, (onHook, onOutput) => push(repo, remote, branch, onHook, onOutput))) {
2671
2807
  process.exitCode = 1;
2672
2808
  outro("Sending stopped after a Git failure.");
2673
2809
  return;
@@ -2681,21 +2817,10 @@ function reportGitFailure(message, error) {
2681
2817
  if (error instanceof GitOutputError && error.hookFailureReported) return;
2682
2818
  log.error(error instanceof GitOutputError ? message : `${message}: ${errorMessage(error)}`);
2683
2819
  }
2684
- function reportHooks(label) {
2685
- return (event) => {
2686
- const hook = `${label}: ${event.name}`;
2687
- if (event.phase === "start") {
2688
- log.step(styleText3("bold", hook));
2689
- } else {
2690
- if (event.exitCode === 0) log.success(`${hook}: completed`);
2691
- else log.error(`${hook}: failed (exit ${event.exitCode})`);
2692
- }
2693
- };
2694
- }
2695
2820
  async function reviewMessages(drafts, canPrompt) {
2696
2821
  while (true) {
2697
2822
  for (const { stats, pendingLabel, message } of drafts) {
2698
- log.message(`(${styleText3("green", `+${stats.additions}`)} ${styleText3("red", `-${stats.deletions}`)}) ${pendingLabel}: ${message}`);
2823
+ log.message(`(${styleText4("green", `+${stats.additions}`)} ${styleText4("red", `-${stats.deletions}`)}) ${pendingLabel}: ${message}`);
2699
2824
  }
2700
2825
  if (!canPrompt) return drafts;
2701
2826
  const action = await select({
@@ -2728,7 +2853,7 @@ async function reviewMessages(drafts, canPrompt) {
2728
2853
  }
2729
2854
 
2730
2855
  // src/cli.ts
2731
- updateSettings({ withGuide: false });
2856
+ updateSettings({ withGuide: true });
2732
2857
  var main = defineCommand({
2733
2858
  meta: {
2734
2859
  name: "zapdev",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.12.0",
3
+ "version": "1.13.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": {