zapdev 1.4.0 → 1.5.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 +9 -6
  2. package/dist/cli.js +104 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,13 +37,14 @@ flowchart LR
37
37
 
38
38
  ## Environment Variables
39
39
 
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 |
44
- | `ZD_EFFORT` | For `commit` | Sent as `reasoning_effort`; use a value supported by your model, such as `low`, `medium`, or `high` |
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 |
44
+ | `ZD_EFFORT` | For `commit` | Sent as `reasoning_effort`; use a value supported by your model, such as `low`, `medium`, or `high` |
45
+ | `ZD_HOOK_TIMEOUT` | No | Deadline per Git hook in seconds (default: `60`); positive numbers from `0.001` to `2147483.647` |
45
46
 
46
- There are no defaults, automatic provider detection, or backup models. CLI flags override these variables. Legacy `OLLAMA_*` variables are no longer read.
47
+ The LLM settings have no defaults, automatic provider detection, or backup models. CLI flags override these settings. Legacy `OLLAMA_*` variables are no longer read.
47
48
 
48
49
  Requests use the OpenAI Chat Completions format over plain `fetch`, without a provider SDK. No authentication headers are sent; use an endpoint that does not require them. The endpoint and model must support `reasoning_effort`.
49
50
 
@@ -139,6 +140,8 @@ Before contacting the LLM endpoint, zapdev runs `gitleaks git --staged --verbose
139
140
 
140
141
  Failures are reported per repository while the others continue. Any failure produces a nonzero exit code.
141
142
 
143
+ In a terminal, Git and its hooks display live logs and support native prompts, even with `--yes` (which only skips zapdev 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
+
142
145
  Pushing is optimistic, with no preliminary fetch. If the branch is behind upstream, `--rebase` runs `git pull --rebase`, while `--merge` runs `git pull --no-rebase --no-edit`; zapdev then retries once. Without either flag, interactive runs ask whether to rebase, merge, or quit. Runs using `--yes` or without a TTY must provide one of the flags.
143
146
 
144
147
  Without a TTY, zapdev commits all prepared repositories automatically and only pushes when `--push` is set.
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.4.0",
557
+ version: "1.5.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: {
@@ -1815,6 +1815,13 @@ function sanitizeCommitMessage(raw) {
1815
1815
  }
1816
1816
 
1817
1817
  // src/lib/config.ts
1818
+ function resolveHookTimeout(env = process.env) {
1819
+ const timeout = Number(env.ZD_HOOK_TIMEOUT ?? 60) * 1e3;
1820
+ if (!Number.isFinite(timeout) || timeout < 1 || timeout > 2147483647) {
1821
+ throw new Error("ZD_HOOK_TIMEOUT must be a number of seconds between 0.001 and 2147483.647.");
1822
+ }
1823
+ return timeout;
1824
+ }
1818
1825
  function resolveConfig(env = process.env, overrides = {}) {
1819
1826
  const config = {
1820
1827
  url: (overrides.url ?? env.ZD_URL)?.trim() ?? "",
@@ -1837,6 +1844,7 @@ function resolveConfig(env = process.env, overrides = {}) {
1837
1844
  }
1838
1845
 
1839
1846
  // src/lib/git.ts
1847
+ import { execFileSync } from "node:child_process";
1840
1848
  import { readdir } from "node:fs/promises";
1841
1849
  import { join } from "node:path";
1842
1850
  import { createInterface as createInterface2 } from "node:readline";
@@ -2169,9 +2177,9 @@ var R2 = (e2, t3, n4) => {
2169
2177
  var z = R2;
2170
2178
 
2171
2179
  // src/lib/git-hooks.ts
2172
- function createHookReporter(onHook) {
2180
+ function createHookReporter(onHook, onTimeout, timeout) {
2173
2181
  const hooks = /* @__PURE__ */ new Map();
2174
- return (line) => {
2182
+ const report = (line) => {
2175
2183
  let event;
2176
2184
  try {
2177
2185
  const value = JSON.parse(line);
@@ -2183,13 +2191,22 @@ function createHookReporter(onHook) {
2183
2191
  if (typeof event.sid !== "string" || typeof event.child_id !== "number") return;
2184
2192
  const key = `${event.sid}:${event.child_id}`;
2185
2193
  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" });
2194
+ const name = event.hook_name;
2195
+ hooks.set(key, { name, timer: setTimeout(() => onTimeout(name), timeout) });
2196
+ onHook?.({ name, phase: "start" });
2188
2197
  } else if (event.event === "child_exit" && typeof event.code === "number") {
2189
- const name = hooks.get(key);
2190
- if (name === void 0) return;
2198
+ const hook = hooks.get(key);
2199
+ if (!hook) return;
2200
+ clearTimeout(hook.timer);
2191
2201
  hooks.delete(key);
2192
- onHook({ name, phase: "exit", exitCode: event.code });
2202
+ onHook?.({ name: hook.name, phase: "exit", exitCode: event.code });
2203
+ }
2204
+ };
2205
+ return {
2206
+ report,
2207
+ close() {
2208
+ for (const { timer } of hooks.values()) clearTimeout(timer);
2209
+ hooks.clear();
2193
2210
  }
2194
2211
  };
2195
2212
  }
@@ -2197,18 +2214,67 @@ function createHookReporter(onHook) {
2197
2214
  // src/lib/git.ts
2198
2215
  var UPSTREAM_REF = "@{upstream}";
2199
2216
  async function git(args, cwd, onHook) {
2217
+ const timeout = resolveHookTimeout();
2218
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && ["commit", "push", "pull", "fetch"].includes(args[0] ?? ""));
2219
+ const terminalState = interactive && process.platform !== "win32" ? execFileSync("stty", ["-g"], { stdio: ["inherit", "pipe", "ignore"], encoding: "utf8" }).trim() : void 0;
2200
2220
  const child = R2("git", args, {
2201
- nodeOptions: onHook ? { cwd, env: { ...process.env, GIT_TRACE2_EVENT: "3" }, stdio: ["pipe", "pipe", "pipe", "pipe"] } : { cwd }
2221
+ nodeOptions: {
2222
+ cwd,
2223
+ detached: !interactive && process.platform !== "win32",
2224
+ env: { ...process.env, GIT_TRACE2_EVENT: "3" },
2225
+ stdio: interactive ? ["inherit", "inherit", "inherit", "pipe"] : ["pipe", "pipe", "pipe", "pipe"]
2226
+ }
2202
2227
  });
2228
+ let timeoutError;
2229
+ const { report, close } = createHookReporter(onHook, (name) => {
2230
+ if (timeoutError || child.pid === void 0) return;
2231
+ timeoutError = new Error(`${name} hook timed out after ${timeout / 1e3} seconds`);
2232
+ try {
2233
+ if (process.platform === "win32") {
2234
+ execFileSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
2235
+ } else if (interactive) {
2236
+ killProcessTree(child.pid);
2237
+ } else {
2238
+ process.kill(-child.pid, "SIGKILL");
2239
+ }
2240
+ } catch {
2241
+ child.kill("SIGKILL");
2242
+ }
2243
+ }, timeout);
2203
2244
  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;
2208
- if (result.exitCode !== 0) {
2209
- throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed`);
2245
+ const reader = trace instanceof Readable ? createInterface2({ input: trace }).on("line", report) : void 0;
2246
+ try {
2247
+ const result = await child;
2248
+ if (timeoutError) throw timeoutError;
2249
+ if (result.exitCode !== 0) {
2250
+ throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed`);
2251
+ }
2252
+ return result.stdout;
2253
+ } finally {
2254
+ reader?.close();
2255
+ close();
2256
+ if (terminalState) execFileSync("stty", [terminalState], { stdio: ["inherit", "ignore", "ignore"] });
2257
+ }
2258
+ }
2259
+ function killProcessTree(pid) {
2260
+ const children = /* @__PURE__ */ new Map();
2261
+ const output = execFileSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8" });
2262
+ for (const line of output.trim().split("\n")) {
2263
+ const [child, parent] = line.trim().split(/\s+/).map(Number);
2264
+ if (child === void 0 || parent === void 0) continue;
2265
+ const siblings = children.get(parent) ?? [];
2266
+ siblings.push(child);
2267
+ children.set(parent, siblings);
2268
+ }
2269
+ const pids = [pid];
2270
+ for (const parent of pids) pids.push(...children.get(parent) ?? []);
2271
+ for (const child of pids.reverse()) {
2272
+ try {
2273
+ process.kill(child, "SIGKILL");
2274
+ } catch (error) {
2275
+ if (error.code !== "ESRCH") throw error;
2276
+ }
2210
2277
  }
2211
- return result.stdout;
2212
2278
  }
2213
2279
  async function tryGit(args, cwd) {
2214
2280
  const result = await R2("git", args, { nodeOptions: { cwd } });
@@ -2438,6 +2504,7 @@ var commitCommand = defineCommand({
2438
2504
  }
2439
2505
  let config;
2440
2506
  try {
2507
+ resolveHookTimeout();
2441
2508
  config = resolveConfig(process.env, {
2442
2509
  url: args.url,
2443
2510
  model: args.model,
@@ -2503,16 +2570,12 @@ var commitCommand = defineCommand({
2503
2570
  }
2504
2571
  const committed = [];
2505
2572
  for (const draft of selected) {
2506
- const loader2 = interactive ? spinner() : void 0;
2507
- const message = `${draft.label}: committing`;
2508
- loader2?.start(message);
2573
+ log.info(`${draft.label}: committing`);
2509
2574
  try {
2510
- await commit(draft.repo, draft.message, reportHooks(draft.label, loader2, message));
2511
- loader2?.clear();
2575
+ await commit(draft.repo, draft.message, reportHooks(draft.label));
2512
2576
  committed.push(draft);
2513
2577
  log.success(`${draft.label}: committed ${draft.message}`);
2514
2578
  } catch (error) {
2515
- loader2?.clear();
2516
2579
  log.error(`${draft.pendingLabel}: commit failed: ${errorMessage(error)}`);
2517
2580
  process.exitCode = 1;
2518
2581
  }
@@ -2535,7 +2598,6 @@ var commitCommand = defineCommand({
2535
2598
  try {
2536
2599
  const pushed = await pushOptimistic(
2537
2600
  repository,
2538
- interactive,
2539
2601
  interactive && !args.yes,
2540
2602
  syncStrategy
2541
2603
  );
@@ -2549,17 +2611,14 @@ var commitCommand = defineCommand({
2549
2611
  if (interactive) outro(process.exitCode ? "Finished with errors." : "Done.");
2550
2612
  }
2551
2613
  });
2552
- function reportHooks(label, loader, message) {
2614
+ function reportHooks(label) {
2553
2615
  return (event) => {
2554
2616
  const hook = `${label}: ${event.name}`;
2555
2617
  if (event.phase === "start") {
2556
- if (loader) loader.message(`${hook} running\u2026`);
2557
- else log.info(`${hook} running\u2026`);
2618
+ log.info(`${hook} running`);
2558
2619
  } else {
2559
- loader?.clear();
2560
2620
  if (event.exitCode === 0) log.success(`${hook} \u2713`);
2561
2621
  else log.error(`${hook} \u2717 (exit ${event.exitCode})`);
2562
- loader?.start(message);
2563
2622
  }
2564
2623
  };
2565
2624
  }
@@ -2595,31 +2654,28 @@ async function reviewMessages(drafts, canPrompt) {
2595
2654
  action.draft.message = edited.trim();
2596
2655
  }
2597
2656
  }
2598
- async function syncWithUpstream({ repo, label }, strategy, interactive) {
2599
- const loader = interactive ? spinner() : void 0;
2657
+ async function syncWithUpstream({ repo, label }, strategy) {
2600
2658
  const actionLabel = strategy === "rebase" ? "Rebase" : "Merge";
2601
- const message = `${label}: pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`;
2602
- loader?.start(message);
2659
+ log.info(`${label}: pulling --${strategy === "rebase" ? "rebase" : "no-rebase"}`);
2603
2660
  try {
2604
- await (strategy === "rebase" ? pullRebase : pullMerge)(repo, reportHooks(label, loader, message));
2605
- loader?.stop(strategy === "rebase" ? "\u2713 Rebased on upstream" : "\u2713 Merged upstream");
2661
+ await (strategy === "rebase" ? pullRebase : pullMerge)(repo, reportHooks(label));
2662
+ log.success(`${label}: ${strategy === "rebase" ? "rebased on upstream" : "merged upstream"}`);
2606
2663
  return true;
2607
2664
  } catch (error) {
2608
- loader?.error(`${actionLabel} failed`);
2609
2665
  log.error(`${label}: ${actionLabel} failed (resolve conflicts, then push): ${errorMessage(error)}`);
2610
2666
  return false;
2611
2667
  }
2612
2668
  }
2613
- async function pushOptimistic(repository, interactive, canPrompt, strategy) {
2669
+ async function pushOptimistic(repository, canPrompt, strategy) {
2614
2670
  const { repo, label } = repository;
2615
2671
  const [upstream, branch] = await Promise.all([hasUpstream(repo), currentBranch(repo)]);
2616
2672
  const doPush = (onHook) => upstream ? push(repo, onHook) : pushSetUpstream(repo, branch, onHook);
2617
- const first = await tryPush(repository, interactive, doPush);
2673
+ const first = await tryPush(repository, doPush);
2618
2674
  if (first.ok) return true;
2619
- if (upstream && await isBehind(repository, interactive)) {
2675
+ if (upstream && await isBehind(repository)) {
2620
2676
  const syncStrategy = strategy ?? await chooseSyncStrategy(repository, canPrompt);
2621
- if (!syncStrategy || !await syncWithUpstream(repository, syncStrategy, interactive)) return false;
2622
- const retry = await tryPush(repository, interactive, doPush);
2677
+ if (!syncStrategy || !await syncWithUpstream(repository, syncStrategy)) return false;
2678
+ const retry = await tryPush(repository, doPush);
2623
2679
  if (retry.ok) return true;
2624
2680
  log.error(`${label}: push failed: ${errorMessage(retry.error)}`);
2625
2681
  return false;
@@ -2646,32 +2702,24 @@ async function chooseSyncStrategy({ label }, interactive) {
2646
2702
  }
2647
2703
  return action;
2648
2704
  }
2649
- async function tryPush({ label }, interactive, doPush) {
2650
- const loader = interactive ? spinner() : void 0;
2651
- const message = `${label}: pushing`;
2652
- loader?.start(message);
2705
+ async function tryPush({ label }, doPush) {
2706
+ log.info(`${label}: pushing`);
2653
2707
  try {
2654
- await doPush(reportHooks(label, loader, message));
2655
- loader?.stop("\u2713 Pushed");
2708
+ await doPush(reportHooks(label));
2709
+ log.success(`${label}: pushed`);
2656
2710
  return { ok: true };
2657
2711
  } catch (error) {
2658
- loader?.error("Push failed");
2659
2712
  return { ok: false, error };
2660
2713
  }
2661
2714
  }
2662
- async function isBehind({ repo, label }, interactive) {
2663
- const loader = interactive ? spinner() : void 0;
2664
- const message = `${label}: checking upstream`;
2665
- loader?.start(message);
2715
+ async function isBehind({ repo, label }) {
2716
+ log.info(`${label}: checking upstream`);
2666
2717
  try {
2667
- await fetchRemote(repo, reportHooks(label, loader, message));
2718
+ await fetchRemote(repo, reportHooks(label));
2668
2719
  const behind = await behindCount(repo);
2669
- loader?.stop(
2670
- behind > 0 ? `Behind upstream by ${behind} commit${behind > 1 ? "s" : ""}` : "Up to date with upstream"
2671
- );
2720
+ log.info(`${label}: ${behind > 0 ? `Behind upstream by ${behind} commit${behind > 1 ? "s" : ""}` : "Up to date with upstream"}`);
2672
2721
  return behind > 0;
2673
2722
  } catch (error) {
2674
- loader?.error("Could not check upstream");
2675
2723
  log.warn(`${label}: could not check upstream: ${errorMessage(error)}`);
2676
2724
  return false;
2677
2725
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.4.0",
3
+ "version": "1.5.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": {