zapdev 1.1.0 → 1.2.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 +52 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -94,7 +94,7 @@ Stages all changes, scans them with Gitleaks when installed, generates Conventio
94
94
 
95
95
  - **Inside a Git repository:** uses that repository only, including when launched from a subdirectory. Does not inspect child repositories.
96
96
  - **Outside a Git repository:** processes only direct child repositories. No recursive search; `node_modules` is excluded.
97
- - **Unified review:** generates messages in parallel, then presents every message with its repository name.
97
+ - **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.
98
98
  - **Actions:** commit all, commit only a named repository, edit a named repository's message, or cancel. Editing returns to the review menu.
99
99
 
100
100
  Repositories with nothing to commit are skipped. Unselected or cancelled changes remain staged. Push confirmation is asked once for the successfully committed repositories.
@@ -120,7 +120,7 @@ zapdev commit -t feat # force the type
120
120
  zapdev commit --staged # leave unstaged changes untouched
121
121
  ```
122
122
 
123
- Before contacting the LLM endpoint, zapdev runs `gitleaks git --staged` 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.
123
+ 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.
124
124
 
125
125
  Failures are reported per repository while the others continue. Any failure produces a nonzero exit code.
126
126
 
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.1.0",
557
+ version: "1.2.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,6 +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
618
 
618
619
  // node_modules/@clack/core/dist/index.mjs
619
620
  import { styleText } from "node:util";
@@ -2190,6 +2191,14 @@ async function commit(repo, message) {
2190
2191
  async function currentBranch(repo) {
2191
2192
  return (await git(["symbolic-ref", "--short", "HEAD"], repo)).trim();
2192
2193
  }
2194
+ async function getRepoStatus(repo) {
2195
+ const lines = (await git(["status", "--porcelain=v2", "--branch", "--untracked-files=normal"], repo)).trim().split("\n");
2196
+ const branch = lines.find((line) => line.startsWith("# branch.head "))?.slice(14) ?? "HEAD";
2197
+ return {
2198
+ branch: branch === "(detached)" ? "detached HEAD" : branch,
2199
+ hasChanges: lines.some((line) => !line.startsWith("#"))
2200
+ };
2201
+ }
2193
2202
  async function hasUpstream(repo) {
2194
2203
  const result = await tryGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", UPSTREAM_REF], repo);
2195
2204
  return result !== null;
@@ -2244,7 +2253,7 @@ async function hasGitleaks() {
2244
2253
  }
2245
2254
  }
2246
2255
  async function scanStagedChanges(repo) {
2247
- const result = await R2("gitleaks", ["git", "--staged"], {
2256
+ const result = await R2("gitleaks", ["git", "--staged", "--verbose"], {
2248
2257
  nodePath: false,
2249
2258
  nodeOptions: { cwd: repo }
2250
2259
  });
@@ -2422,25 +2431,30 @@ var commitCommand = defineCommand({
2422
2431
  if (!scan) log.info("Gitleaks not found, skipping secret scan.");
2423
2432
  const loader = interactive ? spinner() : void 0;
2424
2433
  loader?.start("Preparing repositories and generating commit messages in parallel");
2425
- const results = await Promise.allSettled(repos.map(async (repo) => {
2434
+ const repositories = repos.map((repo) => ({ repo, label: basename(repo), pendingLabel: basename(repo) }));
2435
+ const results = await Promise.allSettled(repositories.map(async (repository) => {
2436
+ const { repo } = repository;
2437
+ const { branch, hasChanges } = await getRepoStatus(repo);
2438
+ repository.label = `${basename(repo)} (${branch})`;
2439
+ repository.pendingLabel = hasChanges ? styleText3("bold", repository.label) : repository.label;
2426
2440
  if (!args.staged) await stageAll(repo);
2427
2441
  const diff = await getStagedDiff(repo);
2428
2442
  if (!diff.trim()) return null;
2429
2443
  if (scan) await scanStagedChanges(repo);
2430
2444
  const message = await generateCommitMessage(diff, config, type);
2431
2445
  if (!message) throw new Error("The model returned an empty message.");
2432
- return { repo, message };
2446
+ return { ...repository, message };
2433
2447
  }));
2434
2448
  loader?.stop("Repositories prepared");
2435
2449
  const drafts = [];
2436
2450
  for (const [index, result] of results.entries()) {
2437
2451
  if (result.status === "rejected") {
2438
- log.error(`${basename(repos[index])}: ${errorMessage(result.reason)}`);
2452
+ log.error(`${repositories[index].pendingLabel}: ${errorMessage(result.reason)}`);
2439
2453
  process.exitCode = 1;
2440
2454
  } else if (result.value) {
2441
2455
  drafts.push(result.value);
2442
2456
  } else {
2443
- log.info(`${basename(repos[index])}: nothing to commit.`);
2457
+ log.info(`${repositories[index].pendingLabel}: nothing to commit.`);
2444
2458
  }
2445
2459
  }
2446
2460
  if (drafts.length === 0) {
@@ -2457,9 +2471,9 @@ var commitCommand = defineCommand({
2457
2471
  try {
2458
2472
  await commit(draft.repo, draft.message);
2459
2473
  committed.push(draft);
2460
- log.success(`${basename(draft.repo)}: committed ${draft.message}`);
2474
+ log.success(`${draft.label}: committed ${draft.message}`);
2461
2475
  } catch (error) {
2462
- log.error(`${basename(draft.repo)}: commit failed: ${errorMessage(error)}`);
2476
+ log.error(`${draft.pendingLabel}: commit failed: ${errorMessage(error)}`);
2463
2477
  process.exitCode = 1;
2464
2478
  }
2465
2479
  }
@@ -2467,7 +2481,7 @@ var commitCommand = defineCommand({
2467
2481
  let shouldPush = Boolean(args.push);
2468
2482
  if (!shouldPush && interactive && !args.yes) {
2469
2483
  const answer = await confirm({
2470
- message: `Push ${committed.map(({ repo }) => basename(repo)).join(", ")}?`,
2484
+ message: `Push ${committed.map(({ label }) => label).join(", ")}?`,
2471
2485
  initialValue: false
2472
2486
  });
2473
2487
  if (isCancel(answer)) {
@@ -2477,17 +2491,17 @@ var commitCommand = defineCommand({
2477
2491
  shouldPush = answer;
2478
2492
  }
2479
2493
  if (shouldPush) {
2480
- for (const { repo } of committed) {
2494
+ for (const repository of committed) {
2481
2495
  try {
2482
2496
  const pushed = await pushOptimistic(
2483
- repo,
2497
+ repository,
2484
2498
  interactive,
2485
2499
  interactive && !args.yes,
2486
2500
  syncStrategy
2487
2501
  );
2488
2502
  if (!pushed) process.exitCode = 1;
2489
2503
  } catch (error) {
2490
- log.error(`${basename(repo)}: push failed: ${errorMessage(error)}`);
2504
+ log.error(`${repository.label}: push failed: ${errorMessage(error)}`);
2491
2505
  process.exitCode = 1;
2492
2506
  }
2493
2507
  }
@@ -2497,7 +2511,7 @@ var commitCommand = defineCommand({
2497
2511
  });
2498
2512
  async function reviewMessages(drafts, canPrompt) {
2499
2513
  while (true) {
2500
- for (const draft of drafts) log.message(`${basename(draft.repo)}: ${draft.message}`);
2514
+ for (const draft of drafts) log.message(`${draft.pendingLabel}: ${draft.message}`);
2501
2515
  if (!canPrompt) return drafts;
2502
2516
  const action = await select({
2503
2517
  message: "Action",
@@ -2506,11 +2520,11 @@ async function reviewMessages(drafts, canPrompt) {
2506
2520
  { value: "all", label: drafts.length > 1 ? "Commit all" : "Commit" },
2507
2521
  ...drafts.length > 1 ? drafts.map((draft) => ({
2508
2522
  value: { action: "commit", draft },
2509
- label: `Commit only "${basename(draft.repo)}"`
2523
+ label: `Commit only "${draft.pendingLabel}"`
2510
2524
  })) : [],
2511
2525
  ...drafts.map((draft) => ({
2512
2526
  value: { action: "edit", draft },
2513
- label: `Edit message for "${basename(draft.repo)}"`
2527
+ label: `Edit message for "${draft.pendingLabel}"`
2514
2528
  })),
2515
2529
  { value: "cancel", label: "Cancel" }
2516
2530
  ]
@@ -2519,7 +2533,7 @@ async function reviewMessages(drafts, canPrompt) {
2519
2533
  if (action === "all") return drafts;
2520
2534
  if (action.action === "commit") return [action.draft];
2521
2535
  const edited = await text({
2522
- message: `Edit message for "${basename(action.draft.repo)}"`,
2536
+ message: `Edit message for "${action.draft.pendingLabel}"`,
2523
2537
  initialValue: action.draft.message,
2524
2538
  validate: (value) => value?.trim() ? void 0 : "Message cannot be empty."
2525
2539
  });
@@ -2527,43 +2541,44 @@ async function reviewMessages(drafts, canPrompt) {
2527
2541
  action.draft.message = edited.trim();
2528
2542
  }
2529
2543
  }
2530
- async function syncWithUpstream(repo, strategy, interactive) {
2544
+ async function syncWithUpstream({ repo, label }, strategy, interactive) {
2531
2545
  const loader = interactive ? spinner() : void 0;
2532
- const label = strategy === "rebase" ? "Rebase" : "Merge";
2533
- loader?.start(`${basename(repo)}: pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`);
2546
+ const actionLabel = strategy === "rebase" ? "Rebase" : "Merge";
2547
+ loader?.start(`${label}: pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`);
2534
2548
  try {
2535
2549
  await (strategy === "rebase" ? pullRebase(repo) : pullMerge(repo));
2536
2550
  loader?.stop(strategy === "rebase" ? "\u2713 Rebased on upstream" : "\u2713 Merged upstream");
2537
2551
  return true;
2538
2552
  } catch (error) {
2539
- loader?.error(`${label} failed`);
2540
- log.error(`${basename(repo)}: ${label} failed (resolve conflicts, then push): ${errorMessage(error)}`);
2553
+ loader?.error(`${actionLabel} failed`);
2554
+ log.error(`${label}: ${actionLabel} failed (resolve conflicts, then push): ${errorMessage(error)}`);
2541
2555
  return false;
2542
2556
  }
2543
2557
  }
2544
- async function pushOptimistic(repo, interactive, canPrompt, strategy) {
2558
+ async function pushOptimistic(repository, interactive, canPrompt, strategy) {
2559
+ const { repo, label } = repository;
2545
2560
  const [upstream, branch] = await Promise.all([hasUpstream(repo), currentBranch(repo)]);
2546
2561
  const doPush = () => upstream ? push(repo) : pushSetUpstream(repo, branch);
2547
- const first = await tryPush(repo, interactive, doPush);
2562
+ const first = await tryPush(repository, interactive, doPush);
2548
2563
  if (first.ok) return true;
2549
- if (upstream && await isBehind(repo, interactive)) {
2550
- const syncStrategy = strategy ?? await chooseSyncStrategy(repo, canPrompt);
2551
- if (!syncStrategy || !await syncWithUpstream(repo, syncStrategy, interactive)) return false;
2552
- const retry = await tryPush(repo, interactive, doPush);
2564
+ if (upstream && await isBehind(repository, interactive)) {
2565
+ const syncStrategy = strategy ?? await chooseSyncStrategy(repository, canPrompt);
2566
+ if (!syncStrategy || !await syncWithUpstream(repository, syncStrategy, interactive)) return false;
2567
+ const retry = await tryPush(repository, interactive, doPush);
2553
2568
  if (retry.ok) return true;
2554
- log.error(`${basename(repo)}: push failed: ${errorMessage(retry.error)}`);
2569
+ log.error(`${label}: push failed: ${errorMessage(retry.error)}`);
2555
2570
  return false;
2556
2571
  }
2557
- log.error(`${basename(repo)}: push failed: ${errorMessage(first.error)}`);
2572
+ log.error(`${label}: push failed: ${errorMessage(first.error)}`);
2558
2573
  return false;
2559
2574
  }
2560
- async function chooseSyncStrategy(repo, interactive) {
2575
+ async function chooseSyncStrategy({ label }, interactive) {
2561
2576
  if (!interactive) {
2562
- log.error(`${basename(repo)}: branch is behind upstream. Re-run with --rebase or --merge.`);
2577
+ log.error(`${label}: branch is behind upstream. Re-run with --rebase or --merge.`);
2563
2578
  return null;
2564
2579
  }
2565
2580
  const action = await select({
2566
- message: `${basename(repo)}: branch is behind upstream. How should zapdev sync it?`,
2581
+ message: `${label}: branch is behind upstream. How should zapdev sync it?`,
2567
2582
  options: [
2568
2583
  { value: "rebase", label: "Rebase" },
2569
2584
  { value: "merge", label: "Merge" },
@@ -2576,9 +2591,9 @@ async function chooseSyncStrategy(repo, interactive) {
2576
2591
  }
2577
2592
  return action;
2578
2593
  }
2579
- async function tryPush(repo, interactive, doPush) {
2594
+ async function tryPush({ label }, interactive, doPush) {
2580
2595
  const loader = interactive ? spinner() : void 0;
2581
- loader?.start(`${basename(repo)}: pushing`);
2596
+ loader?.start(`${label}: pushing`);
2582
2597
  try {
2583
2598
  await doPush();
2584
2599
  loader?.stop("\u2713 Pushed");
@@ -2588,9 +2603,9 @@ async function tryPush(repo, interactive, doPush) {
2588
2603
  return { ok: false, error };
2589
2604
  }
2590
2605
  }
2591
- async function isBehind(repo, interactive) {
2606
+ async function isBehind({ repo, label }, interactive) {
2592
2607
  const loader = interactive ? spinner() : void 0;
2593
- loader?.start(`${basename(repo)}: checking upstream`);
2608
+ loader?.start(`${label}: checking upstream`);
2594
2609
  try {
2595
2610
  await fetchRemote(repo);
2596
2611
  const behind = await behindCount(repo);
@@ -2600,7 +2615,7 @@ async function isBehind(repo, interactive) {
2600
2615
  return behind > 0;
2601
2616
  } catch (error) {
2602
2617
  loader?.error("Could not check upstream");
2603
- log.warn(`${basename(repo)}: could not check upstream: ${errorMessage(error)}`);
2618
+ log.warn(`${label}: could not check upstream: ${errorMessage(error)}`);
2604
2619
  return false;
2605
2620
  }
2606
2621
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.1.0",
3
+ "version": "1.2.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": {