zapdev 1.8.0 → 1.10.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 +47 -23
- package/dist/cli.js +251 -222
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ Stages all changes, scans them with Gitleaks when installed, generates Conventio
|
|
|
113
113
|
- **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.
|
|
114
114
|
- **Actions:** commit all, commit only a named repository, edit a named repository's message, or cancel. Editing returns to the review menu.
|
|
115
115
|
|
|
116
|
-
Repositories with
|
|
116
|
+
Repositories with no new changes can still send existing commits. Unselected or cancelled changes remain staged; failed or unselected commit drafts are not pushed. Push confirmation is asked once, followed by a destination input for each eligible repository or subtree.
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
119
|
zapdev commit
|
|
@@ -124,61 +124,85 @@ zapdev commit
|
|
|
124
124
|
| `--url <url>` | Override the complete Chat Completions endpoint |
|
|
125
125
|
| `--model <model>` | Override the model |
|
|
126
126
|
| `--effort <effort>` | Override the reasoning effort |
|
|
127
|
-
| `-t, --type <type>` | Force
|
|
128
|
-
| `-p, --push` |
|
|
129
|
-
| `--publish-subtree` | Publish subtrees instead of using the classic push |
|
|
130
|
-
| `--publish-subtree=false` | Force classic push, overriding `zapdev.json` |
|
|
127
|
+
| `-t, --type <type>` | Force an exact lowercase Conventional Commit type (`feat`, `fix`, `chore`, etc.) |
|
|
128
|
+
| `-p, --push` | Skip push confirmation; still ask for each destination branch |
|
|
131
129
|
| `-s, --staged` | Commit only changes that are already staged |
|
|
132
|
-
| `-r, --rebase` | Rebase on upstream if the push is rejected |
|
|
133
|
-
| `-m, --merge` | Merge upstream if the push is rejected |
|
|
134
130
|
| `-y, --yes` | Skip commit review; still confirm push unless `--push` is set |
|
|
135
131
|
|
|
136
132
|
```bash
|
|
137
133
|
zapdev commit -t feat # force the type
|
|
138
134
|
zapdev commit --staged # leave unstaged changes untouched
|
|
139
135
|
zapdev commit -y # commit automatically, then ask before pushing
|
|
140
|
-
zapdev commit -yp # commit
|
|
136
|
+
zapdev commit -yp # commit automatically, then enter each push destination
|
|
141
137
|
```
|
|
142
138
|
|
|
143
139
|
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.
|
|
144
140
|
|
|
145
|
-
|
|
141
|
+
Preparation and commit failures are reported per repository while the others continue. A push failure stops all remaining sends. Any failure produces a nonzero exit code.
|
|
146
142
|
|
|
147
143
|
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`.
|
|
148
144
|
|
|
149
|
-
|
|
145
|
+
Every push asks for a destination branch, even with `--yes --push`. In both modes, prompts start empty and then reuse the previous input across repositories in this run. Press Enter to accept the prefilled name; an empty or cleared input uses the current branch and leaves the next prompt empty. Pushing to `main` is allowed in both modes. Classic pushes use the current branch's upstream remote, or the only configured remote if there is no upstream, and run `git push <remote> HEAD:refs/heads/<input>` without changing local branches or their upstreams. The destination need not exist locally. When no remote can be selected unambiguously, the repository's push is skipped with a warning, without a branch prompt. Git rejections stop the command: no rebase, merge, retry, or force-push.
|
|
150
146
|
|
|
151
|
-
|
|
147
|
+
Before each destination input, both modes check the presumed remote branch: the previous input, or the current branch when empty. If that branch is missing, the check falls back to the remote's default branch (`HEAD`). Only unpublished local commits trigger a destination input; if neither reference exists, zapdev also asks. Classic mode compares HEAD; subtree mode compares its extracted history. This check cannot anticipate a different destination you would type next. Repositories without new changes do not trigger an LLM call or a new commit.
|
|
148
|
+
|
|
149
|
+
Without a TTY, zapdev commits all prepared repositories automatically but cannot push; `--push` reports an error and leaves commits local.
|
|
152
150
|
|
|
153
151
|
#### Subtree publication
|
|
154
152
|
|
|
155
153
|
Place an optional `zapdev.json` in the directory where you launch zapdev:
|
|
156
154
|
|
|
157
155
|
```json
|
|
158
|
-
{
|
|
156
|
+
{
|
|
157
|
+
"subtrees": {
|
|
158
|
+
"projet-front": "front",
|
|
159
|
+
"projet-back": "back"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
159
162
|
```
|
|
160
163
|
|
|
161
164
|
| `zapdev.json` option | Default | Effect |
|
|
162
165
|
| ------------------- | ------- | ------ |
|
|
163
|
-
| `
|
|
166
|
+
| `subtrees` | `{}` | Map repository-relative folders to Git remote names. A nonempty mapping replaces classic push with subtree publication. |
|
|
164
167
|
|
|
165
|
-
- **Scope:** only the launch directory's file is read, not parents or child repositories. Its
|
|
166
|
-
- **
|
|
167
|
-
- **Destinations:**
|
|
168
|
-
- **Publication:**
|
|
169
|
-
- **Failures:** stop
|
|
168
|
+
- **Scope:** only the launch directory's file is read, not parents or child repositories. Its mapping applies to newly committed repositories and repositories without new changes.
|
|
169
|
+
- **Configuration:** only `zapdev.json` selects subtree mode; no subtree CLI flag. Missing or empty `subtrees` uses classic push. Invalid configuration stops the command before staging. Replace the removed `isSubtree` option with an explicit mapping.
|
|
170
|
+
- **Destinations:** only mapped folders are published, using their configured remotes; folder and remote names need not match. Each changed subtree asks for its own destination branch, initially empty, then prefilled with the previous input across repositories in this run. Press Enter to reuse it, or leave the input empty to use the current branch. `main` is allowed; detached HEAD is refused.
|
|
171
|
+
- **Publication:** check the presumed destination branch using the shared unpublished-commit rule above, then run `git subtree push --prefix=<folder> <remote> <input>`. Already published history is skipped without prompting; uncommitted changes are excluded.
|
|
172
|
+
- **Failures:** stop all remaining publications, leaving earlier publications intact. No automatic recovery.
|
|
170
173
|
|
|
171
|
-
Interactive runs still ask for confirmation, including with `--yes`; `--push`
|
|
174
|
+
Interactive runs still ask for confirmation, including with `--yes`; `--push` skips only that confirmation, never the destination inputs. Publication requires a TTY.
|
|
172
175
|
|
|
173
176
|
```bash
|
|
174
|
-
zapdev commit
|
|
175
|
-
zapdev commit
|
|
176
|
-
zapdev commit --publish-subtree=false -yp # force classic push
|
|
177
|
+
zapdev commit # with subtrees configured, ask to publish after committing
|
|
178
|
+
zapdev commit -yp # enter a destination for each changed subtree
|
|
177
179
|
```
|
|
178
180
|
|
|
181
|
+
### `zapdev subtree-init`
|
|
182
|
+
|
|
183
|
+
Create or extend a repository with squashed subtrees, named remotes, and the matching `zapdev.json`. No gum or LLM configuration required.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
zapdev subtree-init my-project \
|
|
187
|
+
front=git@github.com:org/front.git \
|
|
188
|
+
back=git@github.com:org/back.git \
|
|
189
|
+
--origin git@github.com:org/project.git
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Parameter | Effect |
|
|
193
|
+
| --------- | ------ |
|
|
194
|
+
| `<directory>` | Repository to create or extend. New repositories start on `main`; existing branches are kept. |
|
|
195
|
+
| `<name=url>...` | One or more sources. Each name is both the subtree folder and its Git remote name. |
|
|
196
|
+
| `--origin <url>` | Optional parent repository remote; replaces the shell helper's `remote=url` argument. |
|
|
197
|
+
|
|
198
|
+
- Requires a clean working tree, including untracked files. Existing folders, conflicting mappings or remote URLs, and a symlinked `zapdev.json` are refused before importing any subtree.
|
|
199
|
+
- Detects each source's default branch, falling back to `main` with a warning when unavailable; imports with `git subtree add --squash`.
|
|
200
|
+
- Commits the explicit folder-to-remote mapping before imports, preserving existing mappings and other JSON settings. Example: `{"subtrees":{"front":"front","back":"back"}}`.
|
|
201
|
+
- Does not push. Git failures stop immediately without rollback; configuration, remotes, and earlier imports remain in place.
|
|
202
|
+
|
|
179
203
|
### Zed IDE
|
|
180
204
|
|
|
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
|
|
205
|
+
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 commit review and push confirmation, then asks for the push destination.
|
|
182
206
|
|
|
183
207
|
Add the following tasks to `.zed/tasks.json`:
|
|
184
208
|
|
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.10.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: {
|
|
@@ -614,7 +614,7 @@ var package_default = {
|
|
|
614
614
|
|
|
615
615
|
// src/commands/commit.ts
|
|
616
616
|
import { basename } from "node:path";
|
|
617
|
-
import { styleText as
|
|
617
|
+
import { styleText as styleText4 } from "node:util";
|
|
618
618
|
|
|
619
619
|
// node_modules/@clack/core/dist/index.mjs
|
|
620
620
|
import { styleText } from "node:util";
|
|
@@ -1780,43 +1780,9 @@ ${r3}
|
|
|
1780
1780
|
}
|
|
1781
1781
|
}).prompt();
|
|
1782
1782
|
|
|
1783
|
-
// src/types/commit.ts
|
|
1784
|
-
var COMMIT_TYPES = [
|
|
1785
|
-
"feat",
|
|
1786
|
-
"fix",
|
|
1787
|
-
"chore",
|
|
1788
|
-
"docs",
|
|
1789
|
-
"style",
|
|
1790
|
-
"refactor",
|
|
1791
|
-
"perf",
|
|
1792
|
-
"test",
|
|
1793
|
-
"build",
|
|
1794
|
-
"ci",
|
|
1795
|
-
"revert"
|
|
1796
|
-
];
|
|
1797
|
-
|
|
1798
|
-
// src/lib/commit-message.ts
|
|
1799
|
-
function normalizeCommitType(input) {
|
|
1800
|
-
const normalized = input.trim().toLowerCase();
|
|
1801
|
-
return COMMIT_TYPES.includes(normalized) ? normalized : null;
|
|
1802
|
-
}
|
|
1803
|
-
function applyCommitType(systemPrompt, type) {
|
|
1804
|
-
return `${systemPrompt}
|
|
1805
|
-
|
|
1806
|
-
The type MUST be exactly "${type}".`;
|
|
1807
|
-
}
|
|
1808
|
-
var MAX_DIFF_CHARS = 12e3;
|
|
1809
|
-
function truncateDiff(diff, max = MAX_DIFF_CHARS) {
|
|
1810
|
-
return diff.length > max ? diff.slice(0, max) : diff;
|
|
1811
|
-
}
|
|
1812
|
-
function sanitizeCommitMessage(raw) {
|
|
1813
|
-
const firstLine = raw.split("\n", 1)[0] ?? "";
|
|
1814
|
-
return firstLine.replace(/[`"]/g, "").trim();
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
1783
|
// src/lib/config.ts
|
|
1818
1784
|
import { readFile } from "node:fs/promises";
|
|
1819
|
-
import { join } from "node:path";
|
|
1785
|
+
import { isAbsolute, join } from "node:path";
|
|
1820
1786
|
|
|
1821
1787
|
// src/lib/errors.ts
|
|
1822
1788
|
var GitOutputError = class extends Error {
|
|
@@ -1831,13 +1797,12 @@ function errorMessage(error) {
|
|
|
1831
1797
|
}
|
|
1832
1798
|
|
|
1833
1799
|
// src/lib/config.ts
|
|
1834
|
-
async function
|
|
1835
|
-
if (override !== void 0) return override;
|
|
1800
|
+
async function readProjectConfig(directory) {
|
|
1836
1801
|
let content;
|
|
1837
1802
|
try {
|
|
1838
1803
|
content = await readFile(join(directory, "zapdev.json"), "utf8");
|
|
1839
1804
|
} catch (error) {
|
|
1840
|
-
if (error.code === "ENOENT") return
|
|
1805
|
+
if (error.code === "ENOENT") return {};
|
|
1841
1806
|
throw new Error(`Unable to read zapdev.json: ${errorMessage(error)}`, { cause: error });
|
|
1842
1807
|
}
|
|
1843
1808
|
let config;
|
|
@@ -1849,11 +1814,27 @@ async function resolvePublishSubtree(directory, override) {
|
|
|
1849
1814
|
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
1850
1815
|
throw new Error("zapdev.json must contain an object.");
|
|
1851
1816
|
}
|
|
1852
|
-
const
|
|
1853
|
-
if (isSubtree !== void 0
|
|
1854
|
-
throw new Error("zapdev.json isSubtree
|
|
1817
|
+
const settings2 = config;
|
|
1818
|
+
if (settings2.isSubtree !== void 0) {
|
|
1819
|
+
throw new Error("zapdev.json isSubtree is no longer supported; use a subtrees folder-to-remote mapping.");
|
|
1820
|
+
}
|
|
1821
|
+
const subtrees = settings2.subtrees;
|
|
1822
|
+
if (subtrees === void 0) return settings2;
|
|
1823
|
+
if (typeof subtrees !== "object" || subtrees === null || Array.isArray(subtrees)) {
|
|
1824
|
+
throw new Error("zapdev.json subtrees must be a folder-to-remote object.");
|
|
1825
|
+
}
|
|
1826
|
+
for (const [prefix, remote] of Object.entries(subtrees)) {
|
|
1827
|
+
if (prefix !== prefix.trim() || isAbsolute(prefix) || prefix.includes("\\") || prefix.split("/").some((part) => ["", ".", "..", ".git"].includes(part))) {
|
|
1828
|
+
throw new Error(`zapdev.json subtree folder "${prefix}" must be a repository-relative directory.`);
|
|
1829
|
+
}
|
|
1830
|
+
if (typeof remote !== "string" || !remote || /\s/.test(remote) || remote.startsWith("-")) {
|
|
1831
|
+
throw new Error(`zapdev.json subtree "${prefix}" must map to a nonempty Git remote name.`);
|
|
1832
|
+
}
|
|
1855
1833
|
}
|
|
1856
|
-
return
|
|
1834
|
+
return settings2;
|
|
1835
|
+
}
|
|
1836
|
+
async function resolveSubtrees(directory) {
|
|
1837
|
+
return (await readProjectConfig(directory)).subtrees ?? {};
|
|
1857
1838
|
}
|
|
1858
1839
|
function resolveHookTimeout(env = process.env) {
|
|
1859
1840
|
const timeout = Number(env.ZD_HOOK_TIMEOUT ?? 60) * 1e3;
|
|
@@ -2252,17 +2233,17 @@ function createHookReporter(onHook, onTimeout, timeout) {
|
|
|
2252
2233
|
}
|
|
2253
2234
|
|
|
2254
2235
|
// src/lib/git.ts
|
|
2255
|
-
|
|
2256
|
-
async function git(args, cwd, onHook) {
|
|
2236
|
+
async function git(args, cwd, onHook, onNativeOutput) {
|
|
2257
2237
|
const timeout = resolveHookTimeout();
|
|
2258
|
-
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && ["commit", "push", "
|
|
2238
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && ["commit", "push", "subtree", "fetch", "ls-remote"].includes(args[0] ?? "") && !(args[0] === "subtree" && args[1] === "split"));
|
|
2259
2239
|
const terminalState = interactive && process.platform !== "win32" ? execFileSync("stty", ["-g"], { stdio: ["inherit", "pipe", "ignore"], encoding: "utf8" }).trim() : void 0;
|
|
2240
|
+
if (interactive) onNativeOutput?.("start");
|
|
2260
2241
|
const child = R2("git", args, {
|
|
2261
2242
|
nodeOptions: {
|
|
2262
2243
|
cwd,
|
|
2263
2244
|
detached: !interactive && process.platform !== "win32",
|
|
2264
2245
|
env: { ...process.env, GIT_TRACE2_EVENT: "3" },
|
|
2265
|
-
stdio: interactive ? ["inherit", "inherit", "inherit", "pipe"] : ["pipe", "pipe", "pipe", "pipe"]
|
|
2246
|
+
stdio: interactive ? ["inherit", args[0] === "ls-remote" ? "pipe" : "inherit", "inherit", "pipe"] : ["pipe", "pipe", "pipe", "pipe"]
|
|
2266
2247
|
}
|
|
2267
2248
|
});
|
|
2268
2249
|
let timeoutError;
|
|
@@ -2299,7 +2280,11 @@ async function git(args, cwd, onHook) {
|
|
|
2299
2280
|
} finally {
|
|
2300
2281
|
reader?.close();
|
|
2301
2282
|
close();
|
|
2302
|
-
|
|
2283
|
+
try {
|
|
2284
|
+
if (terminalState) execFileSync("stty", [terminalState], { stdio: ["inherit", "ignore", "ignore"] });
|
|
2285
|
+
} finally {
|
|
2286
|
+
if (interactive) onNativeOutput?.("exit");
|
|
2287
|
+
}
|
|
2303
2288
|
}
|
|
2304
2289
|
}
|
|
2305
2290
|
function killProcessTree(pid) {
|
|
@@ -2332,8 +2317,8 @@ async function stageAll(repo) {
|
|
|
2332
2317
|
async function getStagedDiff(repo) {
|
|
2333
2318
|
return git(["diff", "--cached"], repo);
|
|
2334
2319
|
}
|
|
2335
|
-
async function commit(repo, message, onHook) {
|
|
2336
|
-
await git(["commit", "--quiet", "-m", message], repo, onHook);
|
|
2320
|
+
async function commit(repo, message, onHook, onNativeOutput) {
|
|
2321
|
+
await git(["commit", "--quiet", "-m", message], repo, onHook, onNativeOutput);
|
|
2337
2322
|
}
|
|
2338
2323
|
async function currentBranch(repo) {
|
|
2339
2324
|
return (await git(["symbolic-ref", "--short", "HEAD"], repo)).trim();
|
|
@@ -2346,38 +2331,26 @@ async function getRepoStatus(repo) {
|
|
|
2346
2331
|
hasChanges: lines.some((line) => !line.startsWith("#"))
|
|
2347
2332
|
};
|
|
2348
2333
|
}
|
|
2349
|
-
async function
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2334
|
+
async function getPushRemote(repo, branch) {
|
|
2335
|
+
const remotes = (await git(["remote"], repo)).trim().split("\n").filter(Boolean);
|
|
2336
|
+
const upstream = (await git(["for-each-ref", "--format=%(upstream:remotename)", `refs/heads/${branch}`], repo)).trim();
|
|
2337
|
+
if (upstream) return remotes.includes(upstream) ? upstream : null;
|
|
2338
|
+
return remotes.length === 1 ? remotes[0] : null;
|
|
2352
2339
|
}
|
|
2353
|
-
async function push(repo, onHook) {
|
|
2354
|
-
await git(["push", "--quiet"], repo, onHook);
|
|
2355
|
-
}
|
|
2356
|
-
async function pushSetUpstream(repo, branch, 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.");
|
|
2340
|
+
async function push(repo, remote, branch, onHook, prefix, onNativeOutput) {
|
|
2361
2341
|
await git(["check-ref-format", "--branch", branch], repo);
|
|
2362
|
-
await git(["
|
|
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
|
-
}
|
|
2369
|
-
async function pullRebase(repo, onHook) {
|
|
2370
|
-
await git(["pull", "--quiet", "--rebase"], repo, onHook);
|
|
2371
|
-
}
|
|
2372
|
-
async function pullMerge(repo, onHook) {
|
|
2373
|
-
await git(["pull", "--quiet", "--no-rebase", "--no-edit"], repo, onHook);
|
|
2342
|
+
await git(prefix ? ["subtree", "push", `--prefix=${prefix}`, remote, branch] : ["push", "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook, onNativeOutput);
|
|
2374
2343
|
}
|
|
2375
|
-
async function
|
|
2376
|
-
await
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2344
|
+
async function hasUnpushedCommits(repo, remote, branch, onHook, prefix, onNativeOutput) {
|
|
2345
|
+
if (await tryGit(["rev-parse", "--verify", "HEAD"], repo) === null) return false;
|
|
2346
|
+
await git(["check-ref-format", "--branch", branch], repo);
|
|
2347
|
+
const source = prefix ? (await git(["subtree", "split", `--prefix=${prefix}`, "--quiet", "HEAD"], repo)).trim() : "HEAD";
|
|
2348
|
+
const ref = `refs/heads/${branch}`;
|
|
2349
|
+
const refs = new Set((await git(["ls-remote", "--", remote, ref, "HEAD"], repo, onHook, onNativeOutput)).trim().split("\n").map((line) => line.split(" ")[1]));
|
|
2350
|
+
const target = refs.has(ref) ? ref : refs.has("HEAD") ? "HEAD" : null;
|
|
2351
|
+
if (!target) return true;
|
|
2352
|
+
await git(["fetch", "--quiet", "--", remote, target], repo, onHook, onNativeOutput);
|
|
2353
|
+
return Boolean((await git(["rev-list", "--max-count=1", `FETCH_HEAD..${source}`], repo)).trim());
|
|
2381
2354
|
}
|
|
2382
2355
|
async function findRepos(path) {
|
|
2383
2356
|
const root = await repoRoot(path);
|
|
@@ -2420,6 +2393,21 @@ var commit_message_default = "You are zapdev, a CLI tool that creates commits qu
|
|
|
2420
2393
|
// src/prompts/index.ts
|
|
2421
2394
|
var COMMIT_SYSTEM_PROMPT = commit_message_default.trim();
|
|
2422
2395
|
|
|
2396
|
+
// src/lib/commit-message.ts
|
|
2397
|
+
function applyCommitType(systemPrompt, type) {
|
|
2398
|
+
return `${systemPrompt}
|
|
2399
|
+
|
|
2400
|
+
The type MUST be exactly "${type}".`;
|
|
2401
|
+
}
|
|
2402
|
+
var MAX_DIFF_CHARS = 12e3;
|
|
2403
|
+
function truncateDiff(diff, max = MAX_DIFF_CHARS) {
|
|
2404
|
+
return diff.length > max ? diff.slice(0, max) : diff;
|
|
2405
|
+
}
|
|
2406
|
+
function sanitizeCommitMessage(raw) {
|
|
2407
|
+
const firstLine = raw.split("\n", 1)[0] ?? "";
|
|
2408
|
+
return firstLine.replace(/[`"]/g, "").trim();
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2423
2411
|
// src/lib/llm.ts
|
|
2424
2412
|
var REQUEST_TIMEOUT_MS = 25e3;
|
|
2425
2413
|
async function generateCommitMessage(diff, config, type) {
|
|
@@ -2459,17 +2447,11 @@ async function generateCommitMessage(diff, config, type) {
|
|
|
2459
2447
|
return sanitizeCommitMessage(content);
|
|
2460
2448
|
}
|
|
2461
2449
|
function describeRequestError(error) {
|
|
2462
|
-
if (
|
|
2450
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
2463
2451
|
return `LLM did not answer within ${REQUEST_TIMEOUT_MS / 1e3}s.`;
|
|
2464
2452
|
}
|
|
2465
2453
|
return "Could not complete the LLM request. Check URL and endpoint availability.";
|
|
2466
2454
|
}
|
|
2467
|
-
function errorName(error) {
|
|
2468
|
-
if (error && typeof error === "object" && "name" in error && typeof error.name === "string") {
|
|
2469
|
-
return error.name;
|
|
2470
|
-
}
|
|
2471
|
-
return null;
|
|
2472
|
-
}
|
|
2473
2455
|
function serverError(body) {
|
|
2474
2456
|
if (!body || typeof body !== "object" || !("error" in body)) return null;
|
|
2475
2457
|
const error = body.error;
|
|
@@ -2487,6 +2469,28 @@ function messageContent(body) {
|
|
|
2487
2469
|
return typeof message.content === "string" ? message.content : null;
|
|
2488
2470
|
}
|
|
2489
2471
|
|
|
2472
|
+
// src/types/commit.ts
|
|
2473
|
+
var COMMIT_TYPES = [
|
|
2474
|
+
"feat",
|
|
2475
|
+
"fix",
|
|
2476
|
+
"chore",
|
|
2477
|
+
"docs",
|
|
2478
|
+
"style",
|
|
2479
|
+
"refactor",
|
|
2480
|
+
"perf",
|
|
2481
|
+
"test",
|
|
2482
|
+
"build",
|
|
2483
|
+
"ci",
|
|
2484
|
+
"revert"
|
|
2485
|
+
];
|
|
2486
|
+
|
|
2487
|
+
// src/commands/native-output.ts
|
|
2488
|
+
import { styleText as styleText3 } from "node:util";
|
|
2489
|
+
var reportNativeOutput = (phase) => {
|
|
2490
|
+
const symbol2 = styleText3("gray", phase === "start" ? S_BAR_END : S_BAR_START);
|
|
2491
|
+
log.message(phase === "start" ? symbol2 : ["", symbol2], { withGuide: false, spacing: 0 });
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2490
2494
|
// src/commands/commit.ts
|
|
2491
2495
|
var commitCommand = defineCommand({
|
|
2492
2496
|
meta: {
|
|
@@ -2507,34 +2511,22 @@ var commitCommand = defineCommand({
|
|
|
2507
2511
|
description: "Override $ZD_EFFORT, sent as reasoning_effort."
|
|
2508
2512
|
},
|
|
2509
2513
|
type: {
|
|
2510
|
-
type: "
|
|
2514
|
+
type: "enum",
|
|
2515
|
+
options: [...COMMIT_TYPES],
|
|
2516
|
+
required: false,
|
|
2511
2517
|
alias: "t",
|
|
2512
2518
|
description: `Force the Conventional Commits type (${COMMIT_TYPES.join(", ")}).`
|
|
2513
2519
|
},
|
|
2514
2520
|
push: {
|
|
2515
2521
|
type: "boolean",
|
|
2516
2522
|
alias: "p",
|
|
2517
|
-
description: "
|
|
2518
|
-
},
|
|
2519
|
-
"publish-subtree": {
|
|
2520
|
-
type: "boolean",
|
|
2521
|
-
description: "Publish projet-front and projet-back; use --publish-subtree=false to force classic push."
|
|
2523
|
+
description: "Skip push confirmation; still ask for each destination branch."
|
|
2522
2524
|
},
|
|
2523
2525
|
staged: {
|
|
2524
2526
|
type: "boolean",
|
|
2525
2527
|
alias: "s",
|
|
2526
2528
|
description: "Commit only changes that are already staged."
|
|
2527
2529
|
},
|
|
2528
|
-
rebase: {
|
|
2529
|
-
type: "boolean",
|
|
2530
|
-
alias: "r",
|
|
2531
|
-
description: "Rebase on the upstream branch if the push is rejected."
|
|
2532
|
-
},
|
|
2533
|
-
merge: {
|
|
2534
|
-
type: "boolean",
|
|
2535
|
-
alias: "m",
|
|
2536
|
-
description: "Merge the upstream branch if the push is rejected."
|
|
2537
|
-
},
|
|
2538
2530
|
yes: {
|
|
2539
2531
|
type: "boolean",
|
|
2540
2532
|
alias: "y",
|
|
@@ -2543,22 +2535,8 @@ var commitCommand = defineCommand({
|
|
|
2543
2535
|
},
|
|
2544
2536
|
async run({ args }) {
|
|
2545
2537
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2546
|
-
if (args.rebase && args.merge) {
|
|
2547
|
-
log.error("Choose either --rebase or --merge, not both.");
|
|
2548
|
-
process.exitCode = 1;
|
|
2549
|
-
return;
|
|
2550
|
-
}
|
|
2551
|
-
const syncStrategy = args.rebase ? "rebase" : args.merge ? "merge" : void 0;
|
|
2552
|
-
const type = args.type ? normalizeCommitType(args.type) : void 0;
|
|
2553
|
-
if (type === null) {
|
|
2554
|
-
log.error(
|
|
2555
|
-
`Invalid type "${args.type}". Valid types: ${COMMIT_TYPES.join(", ")}.`
|
|
2556
|
-
);
|
|
2557
|
-
process.exitCode = 1;
|
|
2558
|
-
return;
|
|
2559
|
-
}
|
|
2560
2538
|
let config;
|
|
2561
|
-
let
|
|
2539
|
+
let subtrees;
|
|
2562
2540
|
try {
|
|
2563
2541
|
resolveHookTimeout();
|
|
2564
2542
|
config = resolveConfig(process.env, {
|
|
@@ -2566,12 +2544,13 @@ var commitCommand = defineCommand({
|
|
|
2566
2544
|
model: args.model,
|
|
2567
2545
|
effort: args.effort
|
|
2568
2546
|
});
|
|
2569
|
-
|
|
2547
|
+
subtrees = await resolveSubtrees(process.cwd());
|
|
2570
2548
|
} catch (error) {
|
|
2571
2549
|
log.error(errorMessage(error));
|
|
2572
2550
|
process.exitCode = 1;
|
|
2573
2551
|
return;
|
|
2574
2552
|
}
|
|
2553
|
+
const publishSubtreeMode = Object.keys(subtrees).length > 0;
|
|
2575
2554
|
if (interactive) intro("zapdev commit");
|
|
2576
2555
|
const repos = await findRepos(process.cwd());
|
|
2577
2556
|
if (repos.length === 0) {
|
|
@@ -2595,17 +2574,18 @@ var commitCommand = defineCommand({
|
|
|
2595
2574
|
const { repo } = repository;
|
|
2596
2575
|
const { branch, hasChanges } = await getRepoStatus(repo);
|
|
2597
2576
|
repository.label = `${basename(repo)} (${branch})`;
|
|
2598
|
-
repository.pendingLabel = hasChanges ?
|
|
2577
|
+
repository.pendingLabel = hasChanges ? styleText4(["bold", "underline"], repository.label) : repository.label;
|
|
2599
2578
|
if (!args.staged) await stageAll(repo);
|
|
2600
2579
|
const diff = await getStagedDiff(repo);
|
|
2601
2580
|
if (!diff.trim()) return null;
|
|
2602
2581
|
if (scan) await scanStagedChanges(repo);
|
|
2603
|
-
const message = await generateCommitMessage(diff, config, type);
|
|
2582
|
+
const message = await generateCommitMessage(diff, config, args.type);
|
|
2604
2583
|
if (!message) throw new Error("The model returned an empty message.");
|
|
2605
2584
|
return { ...repository, message };
|
|
2606
2585
|
}));
|
|
2607
2586
|
loader?.stop("Repositories prepared");
|
|
2608
2587
|
const drafts = [];
|
|
2588
|
+
const unchanged = [];
|
|
2609
2589
|
for (const [index, result] of results.entries()) {
|
|
2610
2590
|
if (result.status === "rejected") {
|
|
2611
2591
|
log.error(`${repositories[index].pendingLabel}: ${errorMessage(result.reason)}`);
|
|
@@ -2614,34 +2594,32 @@ var commitCommand = defineCommand({
|
|
|
2614
2594
|
drafts.push(result.value);
|
|
2615
2595
|
} else {
|
|
2616
2596
|
log.info(`${repositories[index].pendingLabel}: nothing to commit.`);
|
|
2597
|
+
unchanged.push(repositories[index]);
|
|
2617
2598
|
}
|
|
2618
2599
|
}
|
|
2619
|
-
|
|
2620
|
-
if (interactive) outro("No commits prepared.");
|
|
2621
|
-
return;
|
|
2622
|
-
}
|
|
2623
|
-
const selected = await reviewMessages(drafts, interactive && !args.yes);
|
|
2600
|
+
const selected = drafts.length ? await reviewMessages(drafts, interactive && !args.yes) : [];
|
|
2624
2601
|
if (!selected) {
|
|
2625
2602
|
cancel("Cancelled (changes left staged).");
|
|
2626
2603
|
return;
|
|
2627
2604
|
}
|
|
2628
|
-
const
|
|
2605
|
+
const toSend = [];
|
|
2629
2606
|
for (const draft of selected) {
|
|
2630
2607
|
log.info(`${draft.label}: committing`);
|
|
2631
2608
|
try {
|
|
2632
|
-
await commit(draft.repo, draft.message, reportHooks(draft.label));
|
|
2633
|
-
|
|
2609
|
+
await commit(draft.repo, draft.message, reportHooks(draft.label), reportNativeOutput);
|
|
2610
|
+
toSend.push(draft);
|
|
2634
2611
|
log.success(`${draft.label}: committed ${draft.message}`);
|
|
2635
2612
|
} catch (error) {
|
|
2636
2613
|
reportGitFailure(`${draft.pendingLabel}: commit failed`, error);
|
|
2637
2614
|
process.exitCode = 1;
|
|
2638
2615
|
}
|
|
2639
2616
|
}
|
|
2640
|
-
|
|
2617
|
+
toSend.push(...unchanged);
|
|
2618
|
+
if (toSend.length === 0) return;
|
|
2641
2619
|
let shouldPush = Boolean(args.push);
|
|
2642
2620
|
if (!shouldPush && interactive) {
|
|
2643
2621
|
const answer = await confirm({
|
|
2644
|
-
message: `${publishSubtreeMode ? "Publish subtrees in" : "Push"} ${
|
|
2622
|
+
message: `${publishSubtreeMode ? "Publish subtrees in" : "Push"} ${toSend.map(({ label }) => label).join(", ")}?`,
|
|
2645
2623
|
initialValue: false
|
|
2646
2624
|
});
|
|
2647
2625
|
if (isCancel(answer)) {
|
|
@@ -2651,33 +2629,61 @@ var commitCommand = defineCommand({
|
|
|
2651
2629
|
shouldPush = answer;
|
|
2652
2630
|
}
|
|
2653
2631
|
if (shouldPush) {
|
|
2654
|
-
|
|
2632
|
+
if (!interactive) {
|
|
2633
|
+
log.error("Pushing requires a terminal to choose each destination branch. Commits remain local.");
|
|
2634
|
+
process.exitCode = 1;
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
let previousBranchInput = "";
|
|
2638
|
+
for (const { repo, label } of toSend) {
|
|
2655
2639
|
try {
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2640
|
+
const current = await currentBranch(repo);
|
|
2641
|
+
const destinations = publishSubtreeMode ? Object.entries(subtrees) : [["", await getPushRemote(repo, current)]];
|
|
2642
|
+
for (const [prefix, remote] of destinations) {
|
|
2643
|
+
if (!remote) {
|
|
2644
|
+
log.warn(`${label}: no remote or ambiguous remote choice. Skipping push; commit remains local.`);
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
log.info(`${label}: checking unpublished commits${prefix ? ` in ${prefix}` : ""} (${remote}/${previousBranchInput || current}).`);
|
|
2648
|
+
if (!await hasUnpushedCommits(repo, remote, previousBranchInput || current, reportHooks(label), prefix || void 0, reportNativeOutput)) {
|
|
2649
|
+
log.info(`${label}: no unpushed commits${prefix ? ` in ${prefix}` : ""}. Skipping.`);
|
|
2650
|
+
continue;
|
|
2651
|
+
}
|
|
2652
|
+
const answer = await text({
|
|
2653
|
+
message: `${label}: branch to push ${publishSubtreeMode ? `${prefix} to ` : "to "}${remote}`,
|
|
2654
|
+
initialValue: previousBranchInput
|
|
2655
|
+
});
|
|
2656
|
+
if (isCancel(answer)) {
|
|
2657
|
+
outro("Sending cancelled. Remaining commits stay local.");
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
previousBranchInput = answer.trim();
|
|
2661
|
+
const branch = previousBranchInput || current;
|
|
2662
|
+
log.info(`${label}: pushing ${publishSubtreeMode ? `${prefix} \u2192 ` : ""}${remote}/${branch}`);
|
|
2663
|
+
await push(repo, remote, branch, reportHooks(label), prefix || void 0, reportNativeOutput);
|
|
2664
|
+
log.success(`${label}: pushed to ${remote}/${branch}`);
|
|
2665
|
+
}
|
|
2662
2666
|
} catch (error) {
|
|
2663
|
-
reportGitFailure(`${
|
|
2667
|
+
reportGitFailure(`${label}: ${publishSubtreeMode ? "publication" : "push"} failed`, error);
|
|
2664
2668
|
process.exitCode = 1;
|
|
2669
|
+
outro("Sending stopped after a Git failure.");
|
|
2670
|
+
return;
|
|
2665
2671
|
}
|
|
2666
2672
|
}
|
|
2667
2673
|
}
|
|
2668
2674
|
if (interactive) outro(process.exitCode ? "Finished with errors." : "Done.");
|
|
2669
2675
|
}
|
|
2670
2676
|
});
|
|
2671
|
-
function reportGitFailure(message, error
|
|
2677
|
+
function reportGitFailure(message, error) {
|
|
2672
2678
|
if (error instanceof GitOutputError && error.hookFailureReported) return;
|
|
2673
|
-
log
|
|
2679
|
+
log.error(error instanceof GitOutputError ? message : `${message}: ${errorMessage(error)}`);
|
|
2674
2680
|
}
|
|
2675
2681
|
function reportHooks(label) {
|
|
2676
|
-
const options = { secondarySymbol: "" };
|
|
2682
|
+
const options = { secondarySymbol: "", withGuide: false };
|
|
2677
2683
|
return (event) => {
|
|
2678
2684
|
const hook = `${label}: ${event.name}`;
|
|
2679
2685
|
if (event.phase === "start") {
|
|
2680
|
-
log.step(
|
|
2686
|
+
log.step(styleText4("bold", hook), options);
|
|
2681
2687
|
} else {
|
|
2682
2688
|
if (event.exitCode === 0) log.success(`${hook}: completed`, options);
|
|
2683
2689
|
else log.error(`${hook}: failed (exit ${event.exitCode})`, options);
|
|
@@ -2716,91 +2722,114 @@ async function reviewMessages(drafts, canPrompt) {
|
|
|
2716
2722
|
action.draft.message = edited.trim();
|
|
2717
2723
|
}
|
|
2718
2724
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2725
|
+
|
|
2726
|
+
// src/lib/subtree-init.ts
|
|
2727
|
+
import { lstat, mkdir, writeFile } from "node:fs/promises";
|
|
2728
|
+
import { join as join3, resolve } from "node:path";
|
|
2729
|
+
function parseSubtreeSources(entries) {
|
|
2730
|
+
if (entries.length === 0) throw new Error("Provide at least one subtree as name=url.");
|
|
2731
|
+
const names = /* @__PURE__ */ new Set();
|
|
2732
|
+
return entries.map((entry) => {
|
|
2733
|
+
const separator = entry.indexOf("=");
|
|
2734
|
+
const name = entry.slice(0, separator);
|
|
2735
|
+
const url = entry.slice(separator + 1);
|
|
2736
|
+
if (separator < 1 || !/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(name) || name === "zapdev.json" || name.includes("..") || name.endsWith(".") || name.endsWith(".lock") || !url.trim() || url.startsWith("-") || url.includes("\0")) {
|
|
2737
|
+
throw new Error(`Invalid source "${entry}": expected name=url with a simple folder and remote name.`);
|
|
2738
|
+
}
|
|
2739
|
+
if (names.has(name)) throw new Error(`Duplicate subtree name "${name}".`);
|
|
2740
|
+
names.add(name);
|
|
2741
|
+
return { name, url };
|
|
2742
|
+
});
|
|
2730
2743
|
}
|
|
2731
|
-
async function
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2744
|
+
async function initializeSubtrees(directory, sources, options = {}) {
|
|
2745
|
+
if (!directory.trim()) throw new Error("Directory cannot be empty.");
|
|
2746
|
+
if (options.origin !== void 0 && (!options.origin.trim() || options.origin.startsWith("-") || options.origin.includes("\0"))) {
|
|
2747
|
+
throw new Error("--origin must be a nonempty repository URL.");
|
|
2748
|
+
}
|
|
2749
|
+
const repo = resolve(directory);
|
|
2750
|
+
await mkdir(repo, { recursive: true });
|
|
2751
|
+
if (!await exists(join3(repo, ".git"))) await git(["init", "--quiet", "-b", "main"], repo);
|
|
2752
|
+
if ((await getRepoStatus(repo)).hasChanges) {
|
|
2753
|
+
throw new Error("Working tree must be clean before adding subtrees (including untracked files).");
|
|
2754
|
+
}
|
|
2755
|
+
const configPath = join3(repo, "zapdev.json");
|
|
2756
|
+
if (await exists(configPath) && (await lstat(configPath)).isSymbolicLink()) {
|
|
2757
|
+
throw new Error("Refusing to overwrite zapdev.json through a symbolic link.");
|
|
2758
|
+
}
|
|
2759
|
+
const settings2 = await readProjectConfig(repo);
|
|
2760
|
+
const mapping = new Map(Object.entries(settings2.subtrees ?? {}));
|
|
2761
|
+
const remotes = /* @__PURE__ */ new Map();
|
|
2762
|
+
if (options.origin !== void 0) remotes.set("origin", options.origin);
|
|
2763
|
+
for (const { name, url } of sources) {
|
|
2764
|
+
if (await exists(join3(repo, name))) throw new Error(`"${name}" already exists.`);
|
|
2765
|
+
const mappedRemote = mapping.get(name);
|
|
2766
|
+
if (mappedRemote && mappedRemote !== name) throw new Error(`"${name}" is already mapped to remote "${mappedRemote}".`);
|
|
2767
|
+
const plannedURL = remotes.get(name);
|
|
2768
|
+
if (plannedURL && plannedURL !== url) throw new Error(`Remote "${name}" was requested with another URL: ${plannedURL}`);
|
|
2769
|
+
remotes.set(name, url);
|
|
2770
|
+
}
|
|
2771
|
+
const missingRemotes = [];
|
|
2772
|
+
for (const [name, url] of remotes) {
|
|
2773
|
+
const existingURL = await tryGit(["remote", "get-url", name], repo);
|
|
2774
|
+
if (existingURL === null) missingRemotes.push([name, url]);
|
|
2775
|
+
else if (existingURL.trim() !== url) throw new Error(`Remote "${name}" already exists with another URL: ${existingURL.trim()}`);
|
|
2776
|
+
}
|
|
2777
|
+
const imports = [];
|
|
2778
|
+
for (const source of sources) {
|
|
2779
|
+
options.onProgress?.(`Detecting default branch for ${source.name}`);
|
|
2780
|
+
const refs = await git(["ls-remote", "--symref", "--", source.url, "HEAD"], repo, void 0, options.onNativeOutput);
|
|
2781
|
+
const branch = /^ref: refs\/heads\/(.+)\tHEAD$/m.exec(refs)?.[1];
|
|
2782
|
+
if (!branch) options.onWarning?.(`Could not detect default branch for ${source.name}; using main.`);
|
|
2783
|
+
imports.push({ ...source, branch: branch ?? "main" });
|
|
2784
|
+
}
|
|
2785
|
+
const initial = await tryGit(["rev-parse", "--verify", "HEAD"], repo) === null;
|
|
2786
|
+
for (const { name } of sources) mapping.set(name, name);
|
|
2787
|
+
await writeFile(configPath, `${JSON.stringify({ ...settings2, subtrees: Object.fromEntries(mapping) }, null, 2)}
|
|
2788
|
+
`);
|
|
2789
|
+
await git(["add", "--", "zapdev.json"], repo);
|
|
2790
|
+
if (initial || (await getStagedDiff(repo)).trim()) {
|
|
2791
|
+
await commit(repo, initial ? "chore: init" : "chore: configure subtrees", void 0, options.onNativeOutput);
|
|
2743
2792
|
}
|
|
2744
|
-
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
const [upstream, branch] = await Promise.all([hasUpstream(repo), currentBranch(repo)]);
|
|
2749
|
-
const doPush = (onHook) => upstream ? push(repo, onHook) : pushSetUpstream(repo, branch, onHook);
|
|
2750
|
-
const first = await tryPush(repository, doPush);
|
|
2751
|
-
if (first.ok) return true;
|
|
2752
|
-
if (upstream && await isBehind(repository)) {
|
|
2753
|
-
const syncStrategy = strategy ?? await chooseSyncStrategy(repository, canPrompt);
|
|
2754
|
-
if (!syncStrategy || !await syncWithUpstream(repository, syncStrategy)) return false;
|
|
2755
|
-
const retry = await tryPush(repository, doPush);
|
|
2756
|
-
if (retry.ok) return true;
|
|
2757
|
-
reportGitFailure(`${label}: push failed`, retry.error);
|
|
2758
|
-
return false;
|
|
2793
|
+
for (const [name, url] of missingRemotes) await git(["remote", "add", name, url], repo);
|
|
2794
|
+
for (const { name, branch } of imports) {
|
|
2795
|
+
options.onProgress?.(`Adding ${name} (${branch})`);
|
|
2796
|
+
await git(["subtree", "add", `--prefix=${name}`, name, branch, "--squash"], repo, void 0, options.onNativeOutput);
|
|
2759
2797
|
}
|
|
2760
|
-
reportGitFailure(`${label}: push failed`, first.error);
|
|
2761
|
-
return false;
|
|
2762
2798
|
}
|
|
2763
|
-
async function
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
}
|
|
2768
|
-
const action = await select({
|
|
2769
|
-
message: `${label}: branch is behind upstream. How should zapdev sync it?`,
|
|
2770
|
-
options: [
|
|
2771
|
-
{ value: "rebase", label: "Rebase" },
|
|
2772
|
-
{ value: "merge", label: "Merge" },
|
|
2773
|
-
{ value: "quit", label: "Quit" }
|
|
2774
|
-
]
|
|
2799
|
+
async function exists(path) {
|
|
2800
|
+
return lstat(path).then(() => true, (error) => {
|
|
2801
|
+
if (error.code === "ENOENT") return false;
|
|
2802
|
+
throw error;
|
|
2775
2803
|
});
|
|
2776
|
-
if (isCancel(action) || action === "quit") {
|
|
2777
|
-
log.warn("Push cancelled. Commit remains local.");
|
|
2778
|
-
return null;
|
|
2779
|
-
}
|
|
2780
|
-
return action;
|
|
2781
2804
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
}
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2805
|
+
|
|
2806
|
+
// src/commands/subtree-init.ts
|
|
2807
|
+
var subtreeInitCommand = defineCommand({
|
|
2808
|
+
meta: { name: "subtree-init", description: "Initialize a repository with squashed subtrees and their named remotes." },
|
|
2809
|
+
args: {
|
|
2810
|
+
directory: { type: "positional", required: true, description: "Repository directory to create or extend." },
|
|
2811
|
+
subtree: { type: "positional", required: true, description: "name=url; add more positional sources for additional subtrees." },
|
|
2812
|
+
origin: { type: "string", description: "Optional URL for the parent repository's origin remote." }
|
|
2813
|
+
},
|
|
2814
|
+
async run({ args }) {
|
|
2815
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2816
|
+
try {
|
|
2817
|
+
const sources = parseSubtreeSources(args._.slice(1));
|
|
2818
|
+
if (interactive) intro("zapdev subtree-init");
|
|
2819
|
+
await initializeSubtrees(args.directory, sources, {
|
|
2820
|
+
origin: args.origin,
|
|
2821
|
+
onProgress: log.info,
|
|
2822
|
+
onWarning: log.warn,
|
|
2823
|
+
onNativeOutput: reportNativeOutput
|
|
2824
|
+
});
|
|
2825
|
+
if (interactive) outro("Subtrees ready.");
|
|
2826
|
+
else log.info("Subtrees ready.");
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
log.error(error instanceof GitOutputError ? "Subtree initialization failed." : errorMessage(error));
|
|
2829
|
+
process.exitCode = 1;
|
|
2830
|
+
}
|
|
2802
2831
|
}
|
|
2803
|
-
}
|
|
2832
|
+
});
|
|
2804
2833
|
|
|
2805
2834
|
// src/cli.ts
|
|
2806
2835
|
var main = defineCommand({
|
|
@@ -2810,7 +2839,7 @@ var main = defineCommand({
|
|
|
2810
2839
|
description: "Fast, precise git chores from your terminal."
|
|
2811
2840
|
},
|
|
2812
2841
|
args: commitCommand.args,
|
|
2813
|
-
subCommands: { commit: commitCommand },
|
|
2842
|
+
subCommands: { commit: commitCommand, "subtree-init": subtreeInitCommand },
|
|
2814
2843
|
default: "commit"
|
|
2815
2844
|
});
|
|
2816
2845
|
await runMain(main);
|