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.
- package/README.md +9 -6
- package/dist/cli.js +104 -56
- 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
|
|
41
|
-
|
|
|
42
|
-
| `ZD_URL`
|
|
43
|
-
| `ZD_MODEL`
|
|
44
|
-
| `ZD_EFFORT`
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2187
|
-
|
|
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
|
|
2190
|
-
if (
|
|
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:
|
|
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
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2614
|
+
function reportHooks(label) {
|
|
2553
2615
|
return (event) => {
|
|
2554
2616
|
const hook = `${label}: ${event.name}`;
|
|
2555
2617
|
if (event.phase === "start") {
|
|
2556
|
-
|
|
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
|
|
2599
|
-
const loader = interactive ? spinner() : void 0;
|
|
2657
|
+
async function syncWithUpstream({ repo, label }, strategy) {
|
|
2600
2658
|
const actionLabel = strategy === "rebase" ? "Rebase" : "Merge";
|
|
2601
|
-
|
|
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
|
|
2605
|
-
|
|
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,
|
|
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,
|
|
2673
|
+
const first = await tryPush(repository, doPush);
|
|
2618
2674
|
if (first.ok) return true;
|
|
2619
|
-
if (upstream && await isBehind(repository
|
|
2675
|
+
if (upstream && await isBehind(repository)) {
|
|
2620
2676
|
const syncStrategy = strategy ?? await chooseSyncStrategy(repository, canPrompt);
|
|
2621
|
-
if (!syncStrategy || !await syncWithUpstream(repository, syncStrategy
|
|
2622
|
-
const retry = await tryPush(repository,
|
|
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 },
|
|
2650
|
-
|
|
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
|
|
2655
|
-
|
|
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 }
|
|
2663
|
-
|
|
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
|
|
2718
|
+
await fetchRemote(repo, reportHooks(label));
|
|
2668
2719
|
const behind = await behindCount(repo);
|
|
2669
|
-
|
|
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
|
}
|