zapdev 1.6.0 → 1.8.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 +33 -3
  2. package/dist/cli.js +81 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -125,27 +125,57 @@ 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 |
132
- | `-y, --yes` | Skip prompts and commit all prepared repositories |
134
+ | `-y, --yes` | Skip commit review; still confirm push unless `--push` is set |
133
135
 
134
136
  ```bash
135
137
  zapdev commit -t feat # force the type
136
138
  zapdev commit --staged # leave unstaged changes untouched
139
+ zapdev commit -y # commit automatically, then ask before pushing
140
+ zapdev commit -yp # commit and push without confirmation
137
141
  ```
138
142
 
139
143
  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.
140
144
 
141
145
  Failures are reported per repository while the others continue. Any failure produces a nonzero exit code.
142
146
 
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`.
147
+ In a terminal, Git and its hooks display live logs and support native prompts, even with `--yes` (which skips commit review, not native Git 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
148
 
145
149
  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.
146
150
 
147
151
  Without a TTY, zapdev commits all prepared repositories automatically and only pushes when `--push` is set.
148
152
 
153
+ #### Subtree publication
154
+
155
+ Place an optional `zapdev.json` in the directory where you launch zapdev:
156
+
157
+ ```json
158
+ { "isSubtree": true }
159
+ ```
160
+
161
+ | `zapdev.json` option | Default | Effect |
162
+ | ------------------- | ------- | ------ |
163
+ | `isSubtree` | `false` | Replace classic push with subtree publication when `true`; must be a boolean. |
164
+
165
+ - **Scope:** only the launch directory's file is read, not parents or child repositories. Its setting applies to all successfully committed repositories.
166
+ - **Priority:** explicit `--publish-subtree` / `--publish-subtree=false` overrides the file. Without a flag, invalid configuration stops the command before staging.
167
+ - **Destinations:** `projet-front` → remote `front`, `projet-back` → remote `back`, both using the current branch name. `main` and detached HEAD are refused.
168
+ - **Publication:** fetch each remote's `main`, split committed contents, skip contents identical to `main`, then push without force. Uncommitted changes are excluded.
169
+ - **Failures:** stop that repository's publication, leaving earlier publications intact; other repositories continue. No automatic rebase/merge recovery.
170
+
171
+ Interactive runs still ask for confirmation, including with `--yes`; `--push` publishes without asking. Without a TTY, `--yes` alone does not publish.
172
+
173
+ ```bash
174
+ zapdev commit --publish-subtree # ask to publish after committing
175
+ zapdev commit --publish-subtree -yp # publish without zapdev prompts
176
+ zapdev commit --publish-subtree=false -yp # force classic push
177
+ ```
178
+
149
179
  ### Zed IDE
150
180
 
151
181
  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.6.0",
557
+ version: "1.8.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
 
@@ -2176,18 +2216,6 @@ var R2 = (e2, t3, n4) => {
2176
2216
  };
2177
2217
  var z = R2;
2178
2218
 
2179
- // src/lib/errors.ts
2180
- var GitOutputError = class extends Error {
2181
- constructor(message, hookFailureReported = false) {
2182
- super(message);
2183
- this.hookFailureReported = hookFailureReported;
2184
- }
2185
- hookFailureReported;
2186
- };
2187
- function errorMessage(error) {
2188
- return error instanceof Error ? error.message : String(error);
2189
- }
2190
-
2191
2219
  // src/lib/git-hooks.ts
2192
2220
  function createHookReporter(onHook, onTimeout, timeout) {
2193
2221
  const hooks = /* @__PURE__ */ new Map();
@@ -2328,6 +2356,16 @@ async function push(repo, onHook) {
2328
2356
  async function pushSetUpstream(repo, branch, onHook) {
2329
2357
  await git(["push", "--quiet", "-u", "origin", branch], repo, onHook);
2330
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;
2368
+ }
2331
2369
  async function pullRebase(repo, onHook) {
2332
2370
  await git(["pull", "--quiet", "--rebase"], repo, onHook);
2333
2371
  }
@@ -2347,7 +2385,7 @@ async function findRepos(path) {
2347
2385
  const entries = await readdir(path, { withFileTypes: true }).catch(() => null);
2348
2386
  if (!entries) return [];
2349
2387
  const dirs = entries.filter((entry) => entry.isDirectory() && entry.name !== "node_modules");
2350
- const repos = await Promise.all(dirs.map((entry) => repoRoot(join(path, entry.name))));
2388
+ const repos = await Promise.all(dirs.map((entry) => repoRoot(join2(path, entry.name))));
2351
2389
  return repos.filter((dir) => dir !== null).sort();
2352
2390
  }
2353
2391
  async function repoRoot(dir) {
@@ -2476,7 +2514,11 @@ var commitCommand = defineCommand({
2476
2514
  push: {
2477
2515
  type: "boolean",
2478
2516
  alias: "p",
2479
- 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."
2480
2522
  },
2481
2523
  staged: {
2482
2524
  type: "boolean",
@@ -2496,7 +2538,7 @@ var commitCommand = defineCommand({
2496
2538
  yes: {
2497
2539
  type: "boolean",
2498
2540
  alias: "y",
2499
- description: "Skip prompts and commit directly."
2541
+ description: "Skip commit review; still confirm push unless --push is set."
2500
2542
  }
2501
2543
  },
2502
2544
  async run({ args }) {
@@ -2516,6 +2558,7 @@ var commitCommand = defineCommand({
2516
2558
  return;
2517
2559
  }
2518
2560
  let config;
2561
+ let publishSubtreeMode;
2519
2562
  try {
2520
2563
  resolveHookTimeout();
2521
2564
  config = resolveConfig(process.env, {
@@ -2523,6 +2566,7 @@ var commitCommand = defineCommand({
2523
2566
  model: args.model,
2524
2567
  effort: args.effort
2525
2568
  });
2569
+ publishSubtreeMode = await resolvePublishSubtree(process.cwd(), args["publish-subtree"]);
2526
2570
  } catch (error) {
2527
2571
  log.error(errorMessage(error));
2528
2572
  process.exitCode = 1;
@@ -2595,13 +2639,13 @@ var commitCommand = defineCommand({
2595
2639
  }
2596
2640
  if (committed.length === 0) return;
2597
2641
  let shouldPush = Boolean(args.push);
2598
- if (!shouldPush && interactive && !args.yes) {
2642
+ if (!shouldPush && interactive) {
2599
2643
  const answer = await confirm({
2600
- message: `Push ${committed.map(({ label }) => label).join(", ")}?`,
2644
+ message: `${publishSubtreeMode ? "Publish subtrees in" : "Push"} ${committed.map(({ label }) => label).join(", ")}?`,
2601
2645
  initialValue: false
2602
2646
  });
2603
2647
  if (isCancel(answer)) {
2604
- outro("Committed. Not pushed.");
2648
+ outro(publishSubtreeMode ? "Committed. Not published." : "Committed. Not pushed.");
2605
2649
  return;
2606
2650
  }
2607
2651
  shouldPush = answer;
@@ -2609,14 +2653,14 @@ var commitCommand = defineCommand({
2609
2653
  if (shouldPush) {
2610
2654
  for (const repository of committed) {
2611
2655
  try {
2612
- const pushed = await pushOptimistic(
2656
+ const pushed = publishSubtreeMode ? await publishSubtrees(repository) : await pushOptimistic(
2613
2657
  repository,
2614
2658
  interactive && !args.yes,
2615
2659
  syncStrategy
2616
2660
  );
2617
2661
  if (!pushed) process.exitCode = 1;
2618
2662
  } catch (error) {
2619
- reportGitFailure(`${repository.label}: push failed`, error);
2663
+ reportGitFailure(`${repository.label}: ${publishSubtreeMode ? "publication" : "push"} failed`, error);
2620
2664
  process.exitCode = 1;
2621
2665
  }
2622
2666
  }
@@ -2684,6 +2728,21 @@ async function syncWithUpstream({ repo, label }, strategy) {
2684
2728
  return false;
2685
2729
  }
2686
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
+ }
2687
2746
  async function pushOptimistic(repository, canPrompt, strategy) {
2688
2747
  const { repo, label } = repository;
2689
2748
  const [upstream, branch] = await Promise.all([hasUpstream(repo), currentBranch(repo)]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapdev",
3
- "version": "1.6.0",
3
+ "version": "1.8.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": {