zapdev 1.5.0 → 1.7.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 +29 -1
- package/dist/cli.js +106 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,7 +125,9 @@ zapdev commit
|
|
|
125
125
|
| `--model <model>` | Override the model |
|
|
126
126
|
| `--effort <effort>` | Override the reasoning effort |
|
|
127
127
|
| `-t, --type <type>` | Force the Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
|
|
128
|
-
| `-p, --push` | Push after committing without asking
|
|
128
|
+
| `-p, --push` | Push or publish subtrees after committing without asking |
|
|
129
|
+
| `--publish-subtree` | Publish subtrees instead of using the classic push |
|
|
130
|
+
| `--publish-subtree=false` | Force classic push, overriding `zapdev.json` |
|
|
129
131
|
| `-s, --staged` | Commit only changes that are already staged |
|
|
130
132
|
| `-r, --rebase` | Rebase on upstream if the push is rejected |
|
|
131
133
|
| `-m, --merge` | Merge upstream if the push is rejected |
|
|
@@ -146,6 +148,32 @@ Pushing is optimistic, with no preliminary fetch. If the branch is behind upstre
|
|
|
146
148
|
|
|
147
149
|
Without a TTY, zapdev commits all prepared repositories automatically and only pushes when `--push` is set.
|
|
148
150
|
|
|
151
|
+
#### Subtree publication
|
|
152
|
+
|
|
153
|
+
Place an optional `zapdev.json` in the directory where you launch zapdev:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{ "isSubtree": true }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| `zapdev.json` option | Default | Effect |
|
|
160
|
+
| ------------------- | ------- | ------ |
|
|
161
|
+
| `isSubtree` | `false` | Replace classic push with subtree publication when `true`; must be a boolean. |
|
|
162
|
+
|
|
163
|
+
- **Scope:** only the launch directory's file is read, not parents or child repositories. Its setting applies to all successfully committed repositories.
|
|
164
|
+
- **Priority:** explicit `--publish-subtree` / `--publish-subtree=false` overrides the file. Without a flag, invalid configuration stops the command before staging.
|
|
165
|
+
- **Destinations:** `projet-front` → remote `front`, `projet-back` → remote `back`, both using the current branch name. `main` and detached HEAD are refused.
|
|
166
|
+
- **Publication:** fetch each remote's `main`, split committed contents, skip contents identical to `main`, then push without force. Uncommitted changes are excluded.
|
|
167
|
+
- **Failures:** stop that repository's publication, leaving earlier publications intact; other repositories continue. No automatic rebase/merge recovery.
|
|
168
|
+
|
|
169
|
+
Interactive runs still ask for confirmation; `--push` publishes without asking. `--yes` alone does not publish.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
zapdev commit --publish-subtree # ask to publish after committing
|
|
173
|
+
zapdev commit --publish-subtree -yp # publish without zapdev prompts
|
|
174
|
+
zapdev commit --publish-subtree=false -yp # force classic push
|
|
175
|
+
```
|
|
176
|
+
|
|
149
177
|
### Zed IDE
|
|
150
178
|
|
|
151
179
|
For a faster review and commit workflow, review and stage changes from Zed's Git panel, then run `zapdev commit -syp` from a task. The command commits only staged changes, skips prompts, and pushes the commit.
|
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.7.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,46 @@ function sanitizeCommitMessage(raw) {
|
|
|
1815
1815
|
}
|
|
1816
1816
|
|
|
1817
1817
|
// src/lib/config.ts
|
|
1818
|
+
import { readFile } from "node:fs/promises";
|
|
1819
|
+
import { join } from "node:path";
|
|
1820
|
+
|
|
1821
|
+
// src/lib/errors.ts
|
|
1822
|
+
var GitOutputError = class extends Error {
|
|
1823
|
+
constructor(message, hookFailureReported = false) {
|
|
1824
|
+
super(message);
|
|
1825
|
+
this.hookFailureReported = hookFailureReported;
|
|
1826
|
+
}
|
|
1827
|
+
hookFailureReported;
|
|
1828
|
+
};
|
|
1829
|
+
function errorMessage(error) {
|
|
1830
|
+
return error instanceof Error ? error.message : String(error);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/lib/config.ts
|
|
1834
|
+
async function resolvePublishSubtree(directory, override) {
|
|
1835
|
+
if (override !== void 0) return override;
|
|
1836
|
+
let content;
|
|
1837
|
+
try {
|
|
1838
|
+
content = await readFile(join(directory, "zapdev.json"), "utf8");
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
if (error.code === "ENOENT") return false;
|
|
1841
|
+
throw new Error(`Unable to read zapdev.json: ${errorMessage(error)}`, { cause: error });
|
|
1842
|
+
}
|
|
1843
|
+
let config;
|
|
1844
|
+
try {
|
|
1845
|
+
config = JSON.parse(content);
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
throw new Error("zapdev.json must contain valid JSON.", { cause: error });
|
|
1848
|
+
}
|
|
1849
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
1850
|
+
throw new Error("zapdev.json must contain an object.");
|
|
1851
|
+
}
|
|
1852
|
+
const isSubtree = config.isSubtree;
|
|
1853
|
+
if (isSubtree !== void 0 && typeof isSubtree !== "boolean") {
|
|
1854
|
+
throw new Error("zapdev.json isSubtree must be a boolean.");
|
|
1855
|
+
}
|
|
1856
|
+
return isSubtree === true;
|
|
1857
|
+
}
|
|
1818
1858
|
function resolveHookTimeout(env = process.env) {
|
|
1819
1859
|
const timeout = Number(env.ZD_HOOK_TIMEOUT ?? 60) * 1e3;
|
|
1820
1860
|
if (!Number.isFinite(timeout) || timeout < 1 || timeout > 2147483647) {
|
|
@@ -1846,7 +1886,7 @@ function resolveConfig(env = process.env, overrides = {}) {
|
|
|
1846
1886
|
// src/lib/git.ts
|
|
1847
1887
|
import { execFileSync } from "node:child_process";
|
|
1848
1888
|
import { readdir } from "node:fs/promises";
|
|
1849
|
-
import { join } from "node:path";
|
|
1889
|
+
import { join as join2 } from "node:path";
|
|
1850
1890
|
import { createInterface as createInterface2 } from "node:readline";
|
|
1851
1891
|
import { Readable } from "node:stream";
|
|
1852
1892
|
|
|
@@ -2226,7 +2266,11 @@ async function git(args, cwd, onHook) {
|
|
|
2226
2266
|
}
|
|
2227
2267
|
});
|
|
2228
2268
|
let timeoutError;
|
|
2229
|
-
|
|
2269
|
+
let hookFailureReported = false;
|
|
2270
|
+
const { report, close } = createHookReporter((event) => {
|
|
2271
|
+
if (onHook && event.phase === "exit" && event.exitCode !== 0) hookFailureReported = true;
|
|
2272
|
+
onHook?.(event);
|
|
2273
|
+
}, (name) => {
|
|
2230
2274
|
if (timeoutError || child.pid === void 0) return;
|
|
2231
2275
|
timeoutError = new Error(`${name} hook timed out after ${timeout / 1e3} seconds`);
|
|
2232
2276
|
try {
|
|
@@ -2247,7 +2291,9 @@ async function git(args, cwd, onHook) {
|
|
|
2247
2291
|
const result = await child;
|
|
2248
2292
|
if (timeoutError) throw timeoutError;
|
|
2249
2293
|
if (result.exitCode !== 0) {
|
|
2250
|
-
|
|
2294
|
+
const message = result.stderr.trim() || `git ${args.join(" ")} failed`;
|
|
2295
|
+
if (interactive) throw new GitOutputError(message, hookFailureReported);
|
|
2296
|
+
throw new Error(message);
|
|
2251
2297
|
}
|
|
2252
2298
|
return result.stdout;
|
|
2253
2299
|
} finally {
|
|
@@ -2287,7 +2333,7 @@ async function getStagedDiff(repo) {
|
|
|
2287
2333
|
return git(["diff", "--cached"], repo);
|
|
2288
2334
|
}
|
|
2289
2335
|
async function commit(repo, message, onHook) {
|
|
2290
|
-
await git(["commit", "-m", message], repo, onHook);
|
|
2336
|
+
await git(["commit", "--quiet", "-m", message], repo, onHook);
|
|
2291
2337
|
}
|
|
2292
2338
|
async function currentBranch(repo) {
|
|
2293
2339
|
return (await git(["symbolic-ref", "--short", "HEAD"], repo)).trim();
|
|
@@ -2305,19 +2351,29 @@ async function hasUpstream(repo) {
|
|
|
2305
2351
|
return result !== null;
|
|
2306
2352
|
}
|
|
2307
2353
|
async function push(repo, onHook) {
|
|
2308
|
-
await git(["push"], repo, onHook);
|
|
2354
|
+
await git(["push", "--quiet"], repo, onHook);
|
|
2309
2355
|
}
|
|
2310
2356
|
async function pushSetUpstream(repo, branch, onHook) {
|
|
2311
|
-
await git(["push", "-u", "origin", branch], repo, onHook);
|
|
2357
|
+
await git(["push", "--quiet", "-u", "origin", branch], repo, onHook);
|
|
2358
|
+
}
|
|
2359
|
+
async function publishSubtree(repo, remote, branch, onHook) {
|
|
2360
|
+
if (branch === "main") throw new Error("Refusing to publish directly to main.");
|
|
2361
|
+
await git(["check-ref-format", "--branch", branch], repo);
|
|
2362
|
+
await git(["fetch", "--quiet", remote, "main"], repo, onHook);
|
|
2363
|
+
const split = (await git(["subtree", "split", "--quiet", `--prefix=projet-${remote}`], repo)).trim();
|
|
2364
|
+
const changes = await git(["diff", "--name-only", "FETCH_HEAD", split, "--"], repo);
|
|
2365
|
+
if (!changes.trim()) return false;
|
|
2366
|
+
await git(["push", "--quiet", remote, `${split}:refs/heads/${branch}`], repo, onHook);
|
|
2367
|
+
return true;
|
|
2312
2368
|
}
|
|
2313
2369
|
async function pullRebase(repo, onHook) {
|
|
2314
|
-
await git(["pull", "--rebase"], repo, onHook);
|
|
2370
|
+
await git(["pull", "--quiet", "--rebase"], repo, onHook);
|
|
2315
2371
|
}
|
|
2316
2372
|
async function pullMerge(repo, onHook) {
|
|
2317
|
-
await git(["pull", "--no-rebase", "--no-edit"], repo, onHook);
|
|
2373
|
+
await git(["pull", "--quiet", "--no-rebase", "--no-edit"], repo, onHook);
|
|
2318
2374
|
}
|
|
2319
2375
|
async function fetchRemote(repo, onHook) {
|
|
2320
|
-
await git(["fetch"], repo, onHook);
|
|
2376
|
+
await git(["fetch", "--quiet"], repo, onHook);
|
|
2321
2377
|
}
|
|
2322
2378
|
async function behindCount(repo) {
|
|
2323
2379
|
const out = await tryGit(["rev-list", "--count", `HEAD..${UPSTREAM_REF}`], repo);
|
|
@@ -2329,18 +2385,13 @@ async function findRepos(path) {
|
|
|
2329
2385
|
const entries = await readdir(path, { withFileTypes: true }).catch(() => null);
|
|
2330
2386
|
if (!entries) return [];
|
|
2331
2387
|
const dirs = entries.filter((entry) => entry.isDirectory() && entry.name !== "node_modules");
|
|
2332
|
-
const repos = await Promise.all(dirs.map((entry) => repoRoot(
|
|
2388
|
+
const repos = await Promise.all(dirs.map((entry) => repoRoot(join2(path, entry.name))));
|
|
2333
2389
|
return repos.filter((dir) => dir !== null).sort();
|
|
2334
2390
|
}
|
|
2335
2391
|
async function repoRoot(dir) {
|
|
2336
2392
|
return (await tryGit(["rev-parse", "--show-toplevel"], dir))?.trim() ?? null;
|
|
2337
2393
|
}
|
|
2338
2394
|
|
|
2339
|
-
// src/lib/errors.ts
|
|
2340
|
-
function errorMessage(error) {
|
|
2341
|
-
return error instanceof Error ? error.message : String(error);
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
2395
|
// src/lib/gitleaks.ts
|
|
2345
2396
|
async function hasGitleaks() {
|
|
2346
2397
|
try {
|
|
@@ -2463,7 +2514,11 @@ var commitCommand = defineCommand({
|
|
|
2463
2514
|
push: {
|
|
2464
2515
|
type: "boolean",
|
|
2465
2516
|
alias: "p",
|
|
2466
|
-
description: "Push after committing without asking."
|
|
2517
|
+
description: "Push or publish subtrees after committing without asking."
|
|
2518
|
+
},
|
|
2519
|
+
"publish-subtree": {
|
|
2520
|
+
type: "boolean",
|
|
2521
|
+
description: "Publish projet-front and projet-back; use --publish-subtree=false to force classic push."
|
|
2467
2522
|
},
|
|
2468
2523
|
staged: {
|
|
2469
2524
|
type: "boolean",
|
|
@@ -2503,6 +2558,7 @@ var commitCommand = defineCommand({
|
|
|
2503
2558
|
return;
|
|
2504
2559
|
}
|
|
2505
2560
|
let config;
|
|
2561
|
+
let publishSubtreeMode;
|
|
2506
2562
|
try {
|
|
2507
2563
|
resolveHookTimeout();
|
|
2508
2564
|
config = resolveConfig(process.env, {
|
|
@@ -2510,6 +2566,7 @@ var commitCommand = defineCommand({
|
|
|
2510
2566
|
model: args.model,
|
|
2511
2567
|
effort: args.effort
|
|
2512
2568
|
});
|
|
2569
|
+
publishSubtreeMode = await resolvePublishSubtree(process.cwd(), args["publish-subtree"]);
|
|
2513
2570
|
} catch (error) {
|
|
2514
2571
|
log.error(errorMessage(error));
|
|
2515
2572
|
process.exitCode = 1;
|
|
@@ -2576,7 +2633,7 @@ var commitCommand = defineCommand({
|
|
|
2576
2633
|
committed.push(draft);
|
|
2577
2634
|
log.success(`${draft.label}: committed ${draft.message}`);
|
|
2578
2635
|
} catch (error) {
|
|
2579
|
-
|
|
2636
|
+
reportGitFailure(`${draft.pendingLabel}: commit failed`, error);
|
|
2580
2637
|
process.exitCode = 1;
|
|
2581
2638
|
}
|
|
2582
2639
|
}
|
|
@@ -2584,11 +2641,11 @@ var commitCommand = defineCommand({
|
|
|
2584
2641
|
let shouldPush = Boolean(args.push);
|
|
2585
2642
|
if (!shouldPush && interactive && !args.yes) {
|
|
2586
2643
|
const answer = await confirm({
|
|
2587
|
-
message:
|
|
2644
|
+
message: `${publishSubtreeMode ? "Publish subtrees in" : "Push"} ${committed.map(({ label }) => label).join(", ")}?`,
|
|
2588
2645
|
initialValue: false
|
|
2589
2646
|
});
|
|
2590
2647
|
if (isCancel(answer)) {
|
|
2591
|
-
outro("Committed. Not pushed.");
|
|
2648
|
+
outro(publishSubtreeMode ? "Committed. Not published." : "Committed. Not pushed.");
|
|
2592
2649
|
return;
|
|
2593
2650
|
}
|
|
2594
2651
|
shouldPush = answer;
|
|
@@ -2596,14 +2653,14 @@ var commitCommand = defineCommand({
|
|
|
2596
2653
|
if (shouldPush) {
|
|
2597
2654
|
for (const repository of committed) {
|
|
2598
2655
|
try {
|
|
2599
|
-
const pushed = await pushOptimistic(
|
|
2656
|
+
const pushed = publishSubtreeMode ? await publishSubtrees(repository) : await pushOptimistic(
|
|
2600
2657
|
repository,
|
|
2601
2658
|
interactive && !args.yes,
|
|
2602
2659
|
syncStrategy
|
|
2603
2660
|
);
|
|
2604
2661
|
if (!pushed) process.exitCode = 1;
|
|
2605
2662
|
} catch (error) {
|
|
2606
|
-
|
|
2663
|
+
reportGitFailure(`${repository.label}: ${publishSubtreeMode ? "publication" : "push"} failed`, error);
|
|
2607
2664
|
process.exitCode = 1;
|
|
2608
2665
|
}
|
|
2609
2666
|
}
|
|
@@ -2611,14 +2668,19 @@ var commitCommand = defineCommand({
|
|
|
2611
2668
|
if (interactive) outro(process.exitCode ? "Finished with errors." : "Done.");
|
|
2612
2669
|
}
|
|
2613
2670
|
});
|
|
2671
|
+
function reportGitFailure(message, error, level = "error") {
|
|
2672
|
+
if (error instanceof GitOutputError && error.hookFailureReported) return;
|
|
2673
|
+
log[level](error instanceof GitOutputError ? message : `${message}: ${errorMessage(error)}`);
|
|
2674
|
+
}
|
|
2614
2675
|
function reportHooks(label) {
|
|
2676
|
+
const options = { secondarySymbol: "" };
|
|
2615
2677
|
return (event) => {
|
|
2616
2678
|
const hook = `${label}: ${event.name}`;
|
|
2617
2679
|
if (event.phase === "start") {
|
|
2618
|
-
log.
|
|
2680
|
+
log.step(styleText3("bold", hook), options);
|
|
2619
2681
|
} else {
|
|
2620
|
-
if (event.exitCode === 0) log.success(`${hook}
|
|
2621
|
-
else log.error(`${hook}
|
|
2682
|
+
if (event.exitCode === 0) log.success(`${hook}: completed`, options);
|
|
2683
|
+
else log.error(`${hook}: failed (exit ${event.exitCode})`, options);
|
|
2622
2684
|
}
|
|
2623
2685
|
};
|
|
2624
2686
|
}
|
|
@@ -2662,10 +2724,25 @@ async function syncWithUpstream({ repo, label }, strategy) {
|
|
|
2662
2724
|
log.success(`${label}: ${strategy === "rebase" ? "rebased on upstream" : "merged upstream"}`);
|
|
2663
2725
|
return true;
|
|
2664
2726
|
} catch (error) {
|
|
2665
|
-
|
|
2727
|
+
reportGitFailure(`${label}: ${actionLabel} failed (resolve conflicts, then push)`, error);
|
|
2666
2728
|
return false;
|
|
2667
2729
|
}
|
|
2668
2730
|
}
|
|
2731
|
+
async function publishSubtrees({ repo, label }) {
|
|
2732
|
+
const branch = await currentBranch(repo);
|
|
2733
|
+
for (const remote of ["front", "back"]) {
|
|
2734
|
+
log.info(`${label}: publishing projet-${remote} \u2192 ${remote}/${branch}`);
|
|
2735
|
+
try {
|
|
2736
|
+
const published = await publishSubtree(repo, remote, branch, reportHooks(label));
|
|
2737
|
+
if (published) log.success(`${label}: published projet-${remote} \u2192 ${remote}/${branch}`);
|
|
2738
|
+
else log.info(`${label}: no changes in projet-${remote}. Skipping.`);
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
reportGitFailure(`${label}: publication to ${remote}/${branch} failed`, error);
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
return true;
|
|
2745
|
+
}
|
|
2669
2746
|
async function pushOptimistic(repository, canPrompt, strategy) {
|
|
2670
2747
|
const { repo, label } = repository;
|
|
2671
2748
|
const [upstream, branch] = await Promise.all([hasUpstream(repo), currentBranch(repo)]);
|
|
@@ -2677,10 +2754,10 @@ async function pushOptimistic(repository, canPrompt, strategy) {
|
|
|
2677
2754
|
if (!syncStrategy || !await syncWithUpstream(repository, syncStrategy)) return false;
|
|
2678
2755
|
const retry = await tryPush(repository, doPush);
|
|
2679
2756
|
if (retry.ok) return true;
|
|
2680
|
-
|
|
2757
|
+
reportGitFailure(`${label}: push failed`, retry.error);
|
|
2681
2758
|
return false;
|
|
2682
2759
|
}
|
|
2683
|
-
|
|
2760
|
+
reportGitFailure(`${label}: push failed`, first.error);
|
|
2684
2761
|
return false;
|
|
2685
2762
|
}
|
|
2686
2763
|
async function chooseSyncStrategy({ label }, interactive) {
|
|
@@ -2720,7 +2797,7 @@ async function isBehind({ repo, label }) {
|
|
|
2720
2797
|
log.info(`${label}: ${behind > 0 ? `Behind upstream by ${behind} commit${behind > 1 ? "s" : ""}` : "Up to date with upstream"}`);
|
|
2721
2798
|
return behind > 0;
|
|
2722
2799
|
} catch (error) {
|
|
2723
|
-
|
|
2800
|
+
reportGitFailure(`${label}: could not check upstream`, error, "warn");
|
|
2724
2801
|
return false;
|
|
2725
2802
|
}
|
|
2726
2803
|
}
|