zapdev 1.2.0 → 1.4.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 +22 -7
  2. package/dist/cli.js +81 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,3 +1,12 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mvagnon/zapdev/main/.github/assets/preview.gif" alt="zapdev CLI demonstration">
3
+ </p>
4
+
5
+ ---
6
+
7
+ [![portfolio](https://img.shields.io/website?url=https%3A%2F%2Fmvagnon.dev&up_message=Visit&label=Portfolio&color=%23007fff)](https://mvagnon.dev)
8
+ [![bymeacoffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-Support-yellow?logo=buymeacoffee)](https://buymeacoffee.com/mvagnon)
9
+
1
10
  # zapdev
2
11
 
3
12
  ## Project Introduction
@@ -28,10 +37,10 @@ flowchart LR
28
37
 
29
38
  ## Environment Variables
30
39
 
31
- | Variable | Required | Description |
32
- | -------- | -------- | ----------- |
33
- | `ZD_URL` | For `commit` | Complete HTTP(S) Chat Completions endpoint, including its path |
34
- | `ZD_MODEL` | For `commit` | Model identifier supported by the endpoint |
40
+ | Variable | Required | Description |
41
+ | ----------- | ------------ | --------------------------------------------------------------------------------------------------- |
42
+ | `ZD_URL` | For `commit` | Complete HTTP(S) Chat Completions endpoint, including its path |
43
+ | `ZD_MODEL` | For `commit` | Model identifier supported by the endpoint |
35
44
  | `ZD_EFFORT` | For `commit` | Sent as `reasoning_effort`; use a value supported by your model, such as `low`, `medium`, or `high` |
36
45
 
37
46
  There are no defaults, automatic provider detection, or backup models. CLI flags override these variables. Legacy `OLLAMA_*` variables are no longer read.
@@ -55,6 +64,12 @@ Install zapdev globally for daily use:
55
64
  npm install -g zapdev
56
65
  ```
57
66
 
67
+ or
68
+
69
+ ```bash
70
+ bun install -g zapdev
71
+ ```
72
+
58
73
  Configure your endpoint and model before running `commit` (replace these example values):
59
74
 
60
75
  ```bash
@@ -94,7 +109,7 @@ Stages all changes, scans them with Gitleaks when installed, generates Conventio
94
109
 
95
110
  - **Inside a Git repository:** uses that repository only, including when launched from a subdirectory. Does not inspect child repositories.
96
111
  - **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 and branch. Repositories with pending changes are shown in bold when terminal styling is enabled.
112
+ - **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.
98
113
  - **Actions:** commit all, commit only a named repository, edit a named repository's message, or cancel. Editing returns to the review menu.
99
114
 
100
115
  Repositories with nothing to commit are skipped. Unselected or cancelled changes remain staged. Push confirmation is asked once for the successfully committed repositories.
@@ -106,8 +121,8 @@ zapdev commit
106
121
  | Flag | Description |
107
122
  | ------------------- | ----------------------------------------------------------------- |
108
123
  | `--url <url>` | Override the complete Chat Completions endpoint |
109
- | `--model <model>` | Override the model |
110
- | `--effort <effort>` | Override the reasoning effort |
124
+ | `--model <model>` | Override the model |
125
+ | `--effort <effort>` | Override the reasoning effort |
111
126
  | `-t, --type <type>` | Force the Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
112
127
  | `-p, --push` | Push after committing without asking |
113
128
  | `-s, --staged` | Commit only changes that are already staged |
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.2.0",
557
+ version: "1.4.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: {
@@ -1839,6 +1839,8 @@ function resolveConfig(env = process.env, overrides = {}) {
1839
1839
  // src/lib/git.ts
1840
1840
  import { readdir } from "node:fs/promises";
1841
1841
  import { join } from "node:path";
1842
+ import { createInterface as createInterface2 } from "node:readline";
1843
+ import { Readable } from "node:stream";
1842
1844
 
1843
1845
  // node_modules/tinyexec/dist/main.mjs
1844
1846
  import { spawn as e, spawnSync as t2 } from "node:child_process";
@@ -2166,10 +2168,43 @@ var R2 = (e2, t3, n4) => {
2166
2168
  };
2167
2169
  var z = R2;
2168
2170
 
2171
+ // src/lib/git-hooks.ts
2172
+ function createHookReporter(onHook) {
2173
+ const hooks = /* @__PURE__ */ new Map();
2174
+ return (line) => {
2175
+ let event;
2176
+ try {
2177
+ const value = JSON.parse(line);
2178
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2179
+ event = value;
2180
+ } catch {
2181
+ return;
2182
+ }
2183
+ if (typeof event.sid !== "string" || typeof event.child_id !== "number") return;
2184
+ const key = `${event.sid}:${event.child_id}`;
2185
+ if (event.event === "child_start" && event.child_class === "hook" && typeof event.hook_name === "string") {
2186
+ hooks.set(key, event.hook_name);
2187
+ onHook({ name: event.hook_name, phase: "start" });
2188
+ } else if (event.event === "child_exit" && typeof event.code === "number") {
2189
+ const name = hooks.get(key);
2190
+ if (name === void 0) return;
2191
+ hooks.delete(key);
2192
+ onHook({ name, phase: "exit", exitCode: event.code });
2193
+ }
2194
+ };
2195
+ }
2196
+
2169
2197
  // src/lib/git.ts
2170
2198
  var UPSTREAM_REF = "@{upstream}";
2171
- async function git(args, cwd) {
2172
- const result = await R2("git", args, { nodeOptions: { cwd } });
2199
+ async function git(args, cwd, onHook) {
2200
+ const child = R2("git", args, {
2201
+ nodeOptions: onHook ? { cwd, env: { ...process.env, GIT_TRACE2_EVENT: "3" }, stdio: ["pipe", "pipe", "pipe", "pipe"] } : { cwd }
2202
+ });
2203
+ const trace = child.process?.stdio[3];
2204
+ if (onHook && trace instanceof Readable) {
2205
+ createInterface2({ input: trace }).on("line", createHookReporter(onHook));
2206
+ }
2207
+ const result = await child;
2173
2208
  if (result.exitCode !== 0) {
2174
2209
  throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed`);
2175
2210
  }
@@ -2185,8 +2220,8 @@ async function stageAll(repo) {
2185
2220
  async function getStagedDiff(repo) {
2186
2221
  return git(["diff", "--cached"], repo);
2187
2222
  }
2188
- async function commit(repo, message) {
2189
- await git(["commit", "-m", message], repo);
2223
+ async function commit(repo, message, onHook) {
2224
+ await git(["commit", "-m", message], repo, onHook);
2190
2225
  }
2191
2226
  async function currentBranch(repo) {
2192
2227
  return (await git(["symbolic-ref", "--short", "HEAD"], repo)).trim();
@@ -2203,20 +2238,20 @@ async function hasUpstream(repo) {
2203
2238
  const result = await tryGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", UPSTREAM_REF], repo);
2204
2239
  return result !== null;
2205
2240
  }
2206
- async function push(repo) {
2207
- await git(["push"], repo);
2241
+ async function push(repo, onHook) {
2242
+ await git(["push"], repo, onHook);
2208
2243
  }
2209
- async function pushSetUpstream(repo, branch) {
2210
- await git(["push", "-u", "origin", branch], repo);
2244
+ async function pushSetUpstream(repo, branch, onHook) {
2245
+ await git(["push", "-u", "origin", branch], repo, onHook);
2211
2246
  }
2212
- async function pullRebase(repo) {
2213
- await git(["pull", "--rebase"], repo);
2247
+ async function pullRebase(repo, onHook) {
2248
+ await git(["pull", "--rebase"], repo, onHook);
2214
2249
  }
2215
- async function pullMerge(repo) {
2216
- await git(["pull", "--no-rebase", "--no-edit"], repo);
2250
+ async function pullMerge(repo, onHook) {
2251
+ await git(["pull", "--no-rebase", "--no-edit"], repo, onHook);
2217
2252
  }
2218
- async function fetchRemote(repo) {
2219
- await git(["fetch"], repo);
2253
+ async function fetchRemote(repo, onHook) {
2254
+ await git(["fetch"], repo, onHook);
2220
2255
  }
2221
2256
  async function behindCount(repo) {
2222
2257
  const out = await tryGit(["rev-list", "--count", `HEAD..${UPSTREAM_REF}`], repo);
@@ -2436,7 +2471,7 @@ var commitCommand = defineCommand({
2436
2471
  const { repo } = repository;
2437
2472
  const { branch, hasChanges } = await getRepoStatus(repo);
2438
2473
  repository.label = `${basename(repo)} (${branch})`;
2439
- repository.pendingLabel = hasChanges ? styleText3("bold", repository.label) : repository.label;
2474
+ repository.pendingLabel = hasChanges ? styleText3(["bold", "underline"], repository.label) : repository.label;
2440
2475
  if (!args.staged) await stageAll(repo);
2441
2476
  const diff = await getStagedDiff(repo);
2442
2477
  if (!diff.trim()) return null;
@@ -2468,11 +2503,16 @@ var commitCommand = defineCommand({
2468
2503
  }
2469
2504
  const committed = [];
2470
2505
  for (const draft of selected) {
2506
+ const loader2 = interactive ? spinner() : void 0;
2507
+ const message = `${draft.label}: committing`;
2508
+ loader2?.start(message);
2471
2509
  try {
2472
- await commit(draft.repo, draft.message);
2510
+ await commit(draft.repo, draft.message, reportHooks(draft.label, loader2, message));
2511
+ loader2?.clear();
2473
2512
  committed.push(draft);
2474
2513
  log.success(`${draft.label}: committed ${draft.message}`);
2475
2514
  } catch (error) {
2515
+ loader2?.clear();
2476
2516
  log.error(`${draft.pendingLabel}: commit failed: ${errorMessage(error)}`);
2477
2517
  process.exitCode = 1;
2478
2518
  }
@@ -2509,6 +2549,20 @@ var commitCommand = defineCommand({
2509
2549
  if (interactive) outro(process.exitCode ? "Finished with errors." : "Done.");
2510
2550
  }
2511
2551
  });
2552
+ function reportHooks(label, loader, message) {
2553
+ return (event) => {
2554
+ const hook = `${label}: ${event.name}`;
2555
+ if (event.phase === "start") {
2556
+ if (loader) loader.message(`${hook} running\u2026`);
2557
+ else log.info(`${hook} running\u2026`);
2558
+ } else {
2559
+ loader?.clear();
2560
+ if (event.exitCode === 0) log.success(`${hook} \u2713`);
2561
+ else log.error(`${hook} \u2717 (exit ${event.exitCode})`);
2562
+ loader?.start(message);
2563
+ }
2564
+ };
2565
+ }
2512
2566
  async function reviewMessages(drafts, canPrompt) {
2513
2567
  while (true) {
2514
2568
  for (const draft of drafts) log.message(`${draft.pendingLabel}: ${draft.message}`);
@@ -2544,9 +2598,10 @@ async function reviewMessages(drafts, canPrompt) {
2544
2598
  async function syncWithUpstream({ repo, label }, strategy, interactive) {
2545
2599
  const loader = interactive ? spinner() : void 0;
2546
2600
  const actionLabel = strategy === "rebase" ? "Rebase" : "Merge";
2547
- loader?.start(`${label}: pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`);
2601
+ const message = `${label}: pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`;
2602
+ loader?.start(message);
2548
2603
  try {
2549
- await (strategy === "rebase" ? pullRebase(repo) : pullMerge(repo));
2604
+ await (strategy === "rebase" ? pullRebase : pullMerge)(repo, reportHooks(label, loader, message));
2550
2605
  loader?.stop(strategy === "rebase" ? "\u2713 Rebased on upstream" : "\u2713 Merged upstream");
2551
2606
  return true;
2552
2607
  } catch (error) {
@@ -2558,7 +2613,7 @@ async function syncWithUpstream({ repo, label }, strategy, interactive) {
2558
2613
  async function pushOptimistic(repository, interactive, canPrompt, strategy) {
2559
2614
  const { repo, label } = repository;
2560
2615
  const [upstream, branch] = await Promise.all([hasUpstream(repo), currentBranch(repo)]);
2561
- const doPush = () => upstream ? push(repo) : pushSetUpstream(repo, branch);
2616
+ const doPush = (onHook) => upstream ? push(repo, onHook) : pushSetUpstream(repo, branch, onHook);
2562
2617
  const first = await tryPush(repository, interactive, doPush);
2563
2618
  if (first.ok) return true;
2564
2619
  if (upstream && await isBehind(repository, interactive)) {
@@ -2593,9 +2648,10 @@ async function chooseSyncStrategy({ label }, interactive) {
2593
2648
  }
2594
2649
  async function tryPush({ label }, interactive, doPush) {
2595
2650
  const loader = interactive ? spinner() : void 0;
2596
- loader?.start(`${label}: pushing`);
2651
+ const message = `${label}: pushing`;
2652
+ loader?.start(message);
2597
2653
  try {
2598
- await doPush();
2654
+ await doPush(reportHooks(label, loader, message));
2599
2655
  loader?.stop("\u2713 Pushed");
2600
2656
  return { ok: true };
2601
2657
  } catch (error) {
@@ -2605,9 +2661,10 @@ async function tryPush({ label }, interactive, doPush) {
2605
2661
  }
2606
2662
  async function isBehind({ repo, label }, interactive) {
2607
2663
  const loader = interactive ? spinner() : void 0;
2608
- loader?.start(`${label}: checking upstream`);
2664
+ const message = `${label}: checking upstream`;
2665
+ loader?.start(message);
2609
2666
  try {
2610
- await fetchRemote(repo);
2667
+ await fetchRemote(repo, reportHooks(label, loader, message));
2611
2668
  const behind = await behindCount(repo);
2612
2669
  loader?.stop(
2613
2670
  behind > 0 ? `Behind upstream by ${behind} commit${behind > 1 ? "s" : ""}` : "Up to date with upstream"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.2.0",
3
+ "version": "1.4.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": {