zapdev 1.17.0 → 2.0.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 +5 -5
- package/dist/cli.js +121 -290
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ Stages all changes, scans them with Gitleaks when installed, generates Conventio
|
|
|
114
114
|
- **Unified review:** generates messages in parallel, then presents every message with its repository name and branch. Repositories with pending changes are shown in bold when terminal styling is enabled.
|
|
115
115
|
- **Actions:** commit all, commit only a named repository, edit a named repository's message, or cancel. Editing returns to the review menu.
|
|
116
116
|
|
|
117
|
-
Only repositories successfully committed during the current run are offered for push or pushed with `--push`. Unselected or cancelled changes remain staged; failed or unselected commit drafts are not pushed. The branch is chosen before committing, never when pushing. Push confirmation is asked once, only for repositories with a
|
|
117
|
+
Only repositories successfully committed during the current run are offered for push or pushed with `--push`. Unselected or cancelled changes remain staged; failed or unselected commit drafts are not pushed. The branch is chosen before committing, never when pushing. Push confirmation is asked once, only for repositories with a resolved push remote; it is omitted when none have one.
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
120
|
zapdev commit
|
|
@@ -139,17 +139,17 @@ zapdev commit -yp # skip review and push confirmation; choose a branch
|
|
|
139
139
|
zapdev commit --pull # pull first, then follow the normal commit flow
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
Before contacting the LLM endpoint, zapdev runs `gitleaks git --staged --verbose` in each changed repository when Gitleaks is installed. A failed scan
|
|
142
|
+
Before contacting the LLM endpoint, zapdev runs `gitleaks git --staged --verbose` in each changed repository when Gitleaks is installed. A failed scan never sends that repository's diff to the LLM and stops the flow before any commit; when Gitleaks is absent, the scan is skipped. The staged diff is sent to the configured endpoint, which may be remote.
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
Any error stops the flow with a nonzero exit code. Parallel work already in flight finishes before exiting; later steps never start. Earlier successful pulls or commits remain applied.
|
|
145
145
|
|
|
146
146
|
With `--pull`, only repositories with local changes are pulled, in parallel. With `--staged`, only repositories with a staged diff are pulled. All pulls must finish successfully before preparation and message generation start in parallel. Git uses each current branch's configured upstream. Repositories without a configured upstream remote are skipped with a warning, even if they have a single remote. Divergent history, a missing upstream branch, or conflicting local changes stop the flow before staging, after all in-flight pulls finish; no rebase, merge commit, or automatic stash is performed. Successful pulls remain applied.
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Each step uses one spinner: start, wait, stop. Normal Git and hook output is captured without live task logs; failure diagnostics are displayed once. Without a terminal, steps print plain progress messages instead. Git keeps terminal input, but interactive hooks must read and write through `/dev/tty`; prompts on captured stdout/stderr are not displayed. Each hook retains a 60-second deadline; override it with `ZD_HOOK_TIMEOUT=120 zapdev commit`. `--yes` skips commit review, not native prompts.
|
|
149
149
|
|
|
150
150
|
Immediately before each selected commit, zapdev asks for a local branch only when the current name exactly matches `main`, `master`, `principal`, `dev`, or `development`, including with `--yes`. The first prompt starts empty; subsequent prompts reuse the previous input across repositories. Press Enter to accept the prefilled name; an empty or cleared input keeps the current branch and leaves the next prompt empty. A different name switches to the existing local branch or creates it from HEAD, without forcing or discarding pending changes. Other branches and repositories with nothing to commit never trigger this prompt. Cancelling stops the remaining commits and all pushes; earlier commits remain local.
|
|
151
151
|
|
|
152
|
-
Pushes always target a remote branch with the current local branch's name, with no destination input.
|
|
152
|
+
Pushes always target a remote branch with the current local branch's name, with no destination input. The remote is the branch's configured upstream remote, otherwise `origin`, otherwise the only configured remote. With no remote or multiple remotes and no `origin`, push is skipped with a warning. The first successful push sets the upstream when none exists; existing upstreams are preserved. Pushes run directly, without a preliminary fetch or history comparison; Git handles up-to-date branches and rejects non-fast-forward updates. No rebase, merge, retry, or force-push.
|
|
153
153
|
|
|
154
154
|
Without a TTY, zapdev commits prepared repositories on nonprotected branches automatically; protected-branch commits are refused and changes remain staged. `--push` can publish without a TTY; without it, commits stay local. Repositories without new changes do not trigger an LLM call, branch prompt, or new commit.
|
|
155
155
|
|
package/dist/cli.js
CHANGED
|
@@ -830,7 +830,6 @@ function isUnicodeSupported() {
|
|
|
830
830
|
}
|
|
831
831
|
var unicode = isUnicodeSupported();
|
|
832
832
|
var isCI = () => process.env.CI === "true";
|
|
833
|
-
var isTTY = (o2) => o2.isTTY === true;
|
|
834
833
|
var unicodeOr = (o2, e2) => unicode ? o2 : e2;
|
|
835
834
|
var S_STEP_ACTIVE = unicodeOr("\u25C6", "*");
|
|
836
835
|
var S_STEP_CANCEL = unicodeOr("\u25A0", "x");
|
|
@@ -1076,7 +1075,7 @@ var spinner = ({
|
|
|
1076
1075
|
const x2 = getColumns(n4), k2 = I3?.styleFrame ?? W, g2 = (e2) => {
|
|
1077
1076
|
const r3 = e2 > 1 ? O2 ?? settings.messages.error : G ?? settings.messages.cancel;
|
|
1078
1077
|
S2 = e2 === 1, d2 && (a3(r3, e2), S2 && typeof h3 == "function" && h3());
|
|
1079
|
-
}, f2 = () => g2(2), i3 = () => g2(1),
|
|
1078
|
+
}, f2 = () => g2(2), i3 = () => g2(1), A = () => {
|
|
1080
1079
|
process.on("uncaughtExceptionMonitor", f2), process.on("unhandledRejection", f2), process.on("SIGINT", i3), process.on("SIGTERM", i3), process.on("exit", g2), m2 && m2.addEventListener("abort", i3);
|
|
1081
1080
|
}, H = () => {
|
|
1082
1081
|
process.removeListener("uncaughtExceptionMonitor", f2), process.removeListener("unhandledRejection", f2), process.removeListener("SIGINT", i3), process.removeListener("SIGTERM", i3), process.removeListener("exit", g2), m2 && m2.removeEventListener("abort", i3);
|
|
@@ -1097,7 +1096,7 @@ var spinner = ({
|
|
|
1097
1096
|
d2 = true, M = block({ output: n4 }), s2 = C2(e2), w2 = performance.now(), N2 && n4.write(`${styleText2("gray", S_BAR)}
|
|
1098
1097
|
`);
|
|
1099
1098
|
let r3 = 0, t3 = 0;
|
|
1100
|
-
|
|
1099
|
+
A(), T2 = setInterval(() => {
|
|
1101
1100
|
if (u4 && s2 === p2)
|
|
1102
1101
|
return;
|
|
1103
1102
|
y2(), p2 = s2;
|
|
@@ -1225,123 +1224,6 @@ ${b2}
|
|
|
1225
1224
|
}).prompt();
|
|
1226
1225
|
};
|
|
1227
1226
|
var i = `${styleText2("gray", S_BAR)} `;
|
|
1228
|
-
var A = (l3) => l3.replace(/\x1b\[(?:\d+;)*\d*[ABCDEFGHfJKSTsu]|\x1b\[(s|u)/g, "");
|
|
1229
|
-
var taskLog = (l3) => {
|
|
1230
|
-
const r3 = l3.output ?? process.stdout, O2 = getColumns(r3), i3 = styleText2("gray", S_BAR), p2 = l3.spacing ?? 1, k2 = 3, m2 = l3.retainLog === true, d2 = !isCI() && isTTY(r3);
|
|
1231
|
-
r3.write(`${i3}
|
|
1232
|
-
`), r3.write(`${styleText2("green", S_STEP_SUBMIT)} ${l3.title}
|
|
1233
|
-
`);
|
|
1234
|
-
for (let e2 = 0; e2 < p2; e2++)
|
|
1235
|
-
r3.write(`${i3}
|
|
1236
|
-
`);
|
|
1237
|
-
const n4 = [
|
|
1238
|
-
{
|
|
1239
|
-
value: "",
|
|
1240
|
-
full: ""
|
|
1241
|
-
}
|
|
1242
|
-
];
|
|
1243
|
-
let v2 = false;
|
|
1244
|
-
const f2 = (e2) => {
|
|
1245
|
-
if (n4.length === 0)
|
|
1246
|
-
return;
|
|
1247
|
-
let s2 = 0;
|
|
1248
|
-
e2 && (s2 += p2 + 2);
|
|
1249
|
-
for (const t3 of n4) {
|
|
1250
|
-
const { value: o2, result: a3 } = t3;
|
|
1251
|
-
let g2 = a3?.message ?? o2;
|
|
1252
|
-
if (g2.length === 0)
|
|
1253
|
-
continue;
|
|
1254
|
-
a3 === void 0 && t3.header !== void 0 && t3.header !== "" && (g2 += `
|
|
1255
|
-
${t3.header}`);
|
|
1256
|
-
const E2 = g2.split(`
|
|
1257
|
-
`).reduce((b2, w2) => w2 === "" ? b2 + 1 : b2 + Math.ceil((w2.length + k2) / O2), 0);
|
|
1258
|
-
s2 += E2;
|
|
1259
|
-
}
|
|
1260
|
-
s2 > 0 && (s2 += 1, r3.write(import_sisteransi2.erase.lines(s2)));
|
|
1261
|
-
}, h3 = (e2, s2, t3) => {
|
|
1262
|
-
const o2 = t3 ? `${e2.full}
|
|
1263
|
-
${e2.value}` : e2.value;
|
|
1264
|
-
e2.header !== void 0 && e2.header !== "" && log.message(
|
|
1265
|
-
e2.header.split(`
|
|
1266
|
-
`).map((a3) => styleText2("bold", a3)),
|
|
1267
|
-
{
|
|
1268
|
-
output: r3,
|
|
1269
|
-
secondarySymbol: i3,
|
|
1270
|
-
symbol: i3,
|
|
1271
|
-
spacing: 0
|
|
1272
|
-
}
|
|
1273
|
-
), log.message(
|
|
1274
|
-
o2.split(`
|
|
1275
|
-
`).map((a3) => styleText2("dim", a3)),
|
|
1276
|
-
{
|
|
1277
|
-
output: r3,
|
|
1278
|
-
secondarySymbol: i3,
|
|
1279
|
-
symbol: i3,
|
|
1280
|
-
spacing: s2 ?? p2
|
|
1281
|
-
}
|
|
1282
|
-
);
|
|
1283
|
-
}, T2 = () => {
|
|
1284
|
-
for (const e2 of n4) {
|
|
1285
|
-
const { header: s2, value: t3, full: o2 } = e2;
|
|
1286
|
-
(s2 === void 0 || s2.length === 0) && t3.length === 0 || h3(e2, void 0, m2 === true && o2.length > 0);
|
|
1287
|
-
}
|
|
1288
|
-
}, L = (e2, s2, t3) => {
|
|
1289
|
-
if (f2(false), (t3?.raw !== true || !v2) && e2.value !== "" && (e2.value += `
|
|
1290
|
-
`), e2.value += A(s2), v2 = t3?.raw === true, l3.limit !== void 0) {
|
|
1291
|
-
const o2 = e2.value.split(`
|
|
1292
|
-
`), a3 = o2.length - l3.limit;
|
|
1293
|
-
if (a3 > 0) {
|
|
1294
|
-
const g2 = o2.splice(0, a3);
|
|
1295
|
-
m2 && (e2.full += (e2.full === "" ? "" : `
|
|
1296
|
-
`) + g2.join(`
|
|
1297
|
-
`));
|
|
1298
|
-
}
|
|
1299
|
-
e2.value = o2.join(`
|
|
1300
|
-
`);
|
|
1301
|
-
}
|
|
1302
|
-
d2 && y2();
|
|
1303
|
-
}, y2 = () => {
|
|
1304
|
-
for (const e2 of n4)
|
|
1305
|
-
e2.result ? e2.result.status === "error" ? log.error(e2.result.message, { output: r3, secondarySymbol: i3, spacing: 0 }) : log.success(e2.result.message, { output: r3, secondarySymbol: i3, spacing: 0 }) : e2.value !== "" && h3(e2, 0);
|
|
1306
|
-
}, B = (e2, s2) => {
|
|
1307
|
-
f2(false), e2.result = s2, d2 && y2();
|
|
1308
|
-
};
|
|
1309
|
-
return {
|
|
1310
|
-
message(e2, s2) {
|
|
1311
|
-
L(n4[0], e2, s2);
|
|
1312
|
-
},
|
|
1313
|
-
group(e2) {
|
|
1314
|
-
const s2 = {
|
|
1315
|
-
header: e2,
|
|
1316
|
-
value: "",
|
|
1317
|
-
full: ""
|
|
1318
|
-
};
|
|
1319
|
-
return n4.push(s2), {
|
|
1320
|
-
message(t3, o2) {
|
|
1321
|
-
L(s2, t3, o2);
|
|
1322
|
-
},
|
|
1323
|
-
error(t3) {
|
|
1324
|
-
B(s2, {
|
|
1325
|
-
status: "error",
|
|
1326
|
-
message: t3
|
|
1327
|
-
});
|
|
1328
|
-
},
|
|
1329
|
-
success(t3) {
|
|
1330
|
-
B(s2, {
|
|
1331
|
-
status: "success",
|
|
1332
|
-
message: t3
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
};
|
|
1336
|
-
},
|
|
1337
|
-
error(e2, s2) {
|
|
1338
|
-
f2(true), log.error(e2, { output: r3, secondarySymbol: i3, spacing: 1 }), s2?.showLog !== false && T2(), n4.splice(1, n4.length - 1), n4[0].value = "", n4[0].full = "";
|
|
1339
|
-
},
|
|
1340
|
-
success(e2, s2) {
|
|
1341
|
-
f2(true), log.success(e2, { output: r3, secondarySymbol: i3, spacing: 1 }), s2?.showLog === true && T2(), n4.splice(1, n4.length - 1), n4[0].value = "", n4[0].full = "";
|
|
1342
|
-
}
|
|
1343
|
-
};
|
|
1344
|
-
};
|
|
1345
1227
|
var text = (e2) => new n2({
|
|
1346
1228
|
validate: e2.validate,
|
|
1347
1229
|
placeholder: e2.placeholder,
|
|
@@ -1854,7 +1736,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
1854
1736
|
// package.json
|
|
1855
1737
|
var package_default = {
|
|
1856
1738
|
name: "zapdev",
|
|
1857
|
-
version: "
|
|
1739
|
+
version: "2.0.0",
|
|
1858
1740
|
description: "Fast, performant and lightweight git chores and dev tools using your terminal and an OpenAI-compatible LLM endpoint.",
|
|
1859
1741
|
type: "module",
|
|
1860
1742
|
bin: {
|
|
@@ -1914,7 +1796,7 @@ var package_default = {
|
|
|
1914
1796
|
|
|
1915
1797
|
// src/commands/commit.ts
|
|
1916
1798
|
import { basename } from "node:path";
|
|
1917
|
-
import { styleText as
|
|
1799
|
+
import { styleText as styleText3 } from "node:util";
|
|
1918
1800
|
|
|
1919
1801
|
// src/lib/config.ts
|
|
1920
1802
|
function resolveHookTimeout(env = process.env) {
|
|
@@ -2441,13 +2323,24 @@ async function getRepoStatus(repo) {
|
|
|
2441
2323
|
};
|
|
2442
2324
|
}
|
|
2443
2325
|
async function getUpstreamRemote(repo, branch) {
|
|
2444
|
-
const remotes =
|
|
2326
|
+
const remotes = await getRemotes(repo);
|
|
2445
2327
|
const upstream = (await git(["for-each-ref", "--format=%(upstream:remotename)", `refs/heads/${branch}`], repo)).trim();
|
|
2446
2328
|
return remotes.includes(upstream) ? upstream : null;
|
|
2447
2329
|
}
|
|
2330
|
+
async function getPushRemote(repo, branch) {
|
|
2331
|
+
const upstream = await getUpstreamRemote(repo, branch);
|
|
2332
|
+
if (upstream) return upstream;
|
|
2333
|
+
const remotes = await getRemotes(repo);
|
|
2334
|
+
return remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : null;
|
|
2335
|
+
}
|
|
2336
|
+
async function getRemotes(repo) {
|
|
2337
|
+
return (await git(["remote"], repo)).trim().split("\n").filter(Boolean);
|
|
2338
|
+
}
|
|
2448
2339
|
async function push(repo, remote, branch, onHook, onOutput) {
|
|
2449
2340
|
await git(["check-ref-format", "--branch", branch], repo);
|
|
2450
|
-
|
|
2341
|
+
const current = await currentBranch(repo);
|
|
2342
|
+
const upstream = (await git(["for-each-ref", "--format=%(upstream)", `refs/heads/${current}`], repo)).trim();
|
|
2343
|
+
await git(["push", ...!upstream ? ["--set-upstream"] : [], "--", remote, `HEAD:refs/heads/${branch}`], repo, onHook, onOutput);
|
|
2451
2344
|
}
|
|
2452
2345
|
async function findRepos(path) {
|
|
2453
2346
|
const root = await repoRoot(path);
|
|
@@ -2582,54 +2475,23 @@ var COMMIT_TYPES = [
|
|
|
2582
2475
|
];
|
|
2583
2476
|
|
|
2584
2477
|
// src/commands/git-task.ts
|
|
2585
|
-
|
|
2586
|
-
async function runGitTask(label, operation, successMessage, run, group) {
|
|
2587
|
-
const title = `${label}: ${operation}`;
|
|
2478
|
+
async function runTask(title, successMessage, run, withGuide = true) {
|
|
2588
2479
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2589
|
-
const loader = interactive
|
|
2590
|
-
const
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
loader.start(title);
|
|
2480
|
+
const loader = interactive ? spinner({ withGuide }) : void 0;
|
|
2481
|
+
const wasRaw = Boolean(process.stdin.isRaw);
|
|
2482
|
+
loader?.start(title);
|
|
2483
|
+
if (loader) {
|
|
2594
2484
|
process.stdin.setRawMode?.(wasRaw);
|
|
2595
2485
|
process.stdin.pause();
|
|
2596
|
-
};
|
|
2597
|
-
let task = group;
|
|
2598
|
-
const onOutput = (chunk, stream) => {
|
|
2599
|
-
if (!interactive) {
|
|
2600
|
-
process[stream].write(chunk);
|
|
2601
|
-
return;
|
|
2602
|
-
}
|
|
2603
|
-
if (!task) loader?.clear();
|
|
2604
|
-
task ??= taskLog({ title: `${title}: running\u2026`, limit: 10, retainLog: true });
|
|
2605
|
-
task.message(chunk, { raw: true });
|
|
2606
|
-
};
|
|
2607
|
-
const onHook = (event) => {
|
|
2608
|
-
loader?.clear();
|
|
2609
|
-
if (event.phase === "start" || event.exitCode !== 0) {
|
|
2610
|
-
const hook = `${label}: ${event.name}`;
|
|
2611
|
-
const message = event.phase === "start" ? `${hook}: running\u2026` : `${hook}: failed (exit ${event.exitCode})`;
|
|
2612
|
-
if (task) task.message(message);
|
|
2613
|
-
else if (event.phase === "start") log.step(styleText3("bold", message));
|
|
2614
|
-
else log.error(message);
|
|
2615
|
-
}
|
|
2616
|
-
if (event.phase === "exit" && !task) startLoading();
|
|
2617
|
-
};
|
|
2618
|
-
if (!interactive) log.info(title);
|
|
2619
|
-
startLoading();
|
|
2486
|
+
} else log.info(title);
|
|
2620
2487
|
try {
|
|
2621
|
-
await run(
|
|
2622
|
-
loader
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
return true;
|
|
2488
|
+
const result = await run(() => void 0);
|
|
2489
|
+
if (loader) loader.stop(successMessage);
|
|
2490
|
+
else log.success(successMessage, { spacing: 0 });
|
|
2491
|
+
return result;
|
|
2626
2492
|
} catch (error) {
|
|
2627
2493
|
loader?.clear();
|
|
2628
|
-
|
|
2629
|
-
group?.message(message);
|
|
2630
|
-
if (task) task.error(message);
|
|
2631
|
-
else if (!(error instanceof GitOutputError && error.hookFailureReported)) log.error(message);
|
|
2632
|
-
return false;
|
|
2494
|
+
throw new Error(`${title} failed: ${errorMessage(error)}`, { cause: error });
|
|
2633
2495
|
}
|
|
2634
2496
|
}
|
|
2635
2497
|
|
|
@@ -2690,94 +2552,79 @@ var commitCommand = defineCommand({
|
|
|
2690
2552
|
effort: args.effort
|
|
2691
2553
|
});
|
|
2692
2554
|
} catch (error) {
|
|
2693
|
-
log.error(errorMessage(error));
|
|
2555
|
+
log.error(errorMessage(error), { spacing: 0 });
|
|
2694
2556
|
process.exitCode = 1;
|
|
2695
2557
|
return;
|
|
2696
2558
|
}
|
|
2697
2559
|
if (interactive) intro("zapdev commit");
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2560
|
+
try {
|
|
2561
|
+
const repos = await findRepos(process.cwd());
|
|
2562
|
+
if (repos.length === 0) {
|
|
2563
|
+
log.warn("No git repository found here or in direct children.");
|
|
2564
|
+
if (interactive) outro("Nothing to do.");
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (args.pull) {
|
|
2568
|
+
const pulls = await runTask("Pulling repositories in parallel", "Repositories pulled", async (onOutput) => {
|
|
2569
|
+
const results2 = await Promise.allSettled(repos.map(async (repo) => {
|
|
2570
|
+
const label = basename(repo);
|
|
2571
|
+
const hasChanges = args.staged ? Boolean((await getStagedDiff(repo)).trim()) : (await getRepoStatus(repo)).hasChanges;
|
|
2572
|
+
if (!hasChanges) return null;
|
|
2573
|
+
const branch = await currentBranch(repo);
|
|
2574
|
+
if (!await getUpstreamRemote(repo, branch)) {
|
|
2575
|
+
return `${label}: no configured upstream remote. Skipping pull.`;
|
|
2576
|
+
}
|
|
2577
|
+
await git(["pull", "--ff-only", "--no-rebase", "--no-autostash"], repo, void 0, onOutput);
|
|
2578
|
+
return null;
|
|
2579
|
+
}));
|
|
2580
|
+
for (const [index, result] of results2.entries()) {
|
|
2581
|
+
if (result.status === "rejected") throw new Error(`${basename(repos[index])}: ${errorMessage(result.reason)}`);
|
|
2582
|
+
}
|
|
2583
|
+
return results2;
|
|
2584
|
+
});
|
|
2585
|
+
for (const result of pulls) {
|
|
2586
|
+
if (result.status === "fulfilled" && result.value) log.warn(result.value, { spacing: 0 });
|
|
2715
2587
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2588
|
+
}
|
|
2589
|
+
const repositories = repos.map((repo) => ({ repo, label: basename(repo), pendingLabel: basename(repo) }));
|
|
2590
|
+
const { results, scan } = await runTask("Preparing repositories and generating commit messages in parallel", "Repositories prepared", async () => {
|
|
2591
|
+
const scan2 = await hasGitleaks();
|
|
2592
|
+
const results2 = await Promise.allSettled(repositories.map(async (repository) => {
|
|
2593
|
+
const { repo } = repository;
|
|
2594
|
+
const { branch, hasChanges } = await getRepoStatus(repo);
|
|
2595
|
+
repository.label = `${basename(repo)} (${branch})`;
|
|
2596
|
+
repository.pendingLabel = hasChanges ? styleText3("bold", repository.label) : repository.label;
|
|
2597
|
+
if (!args.staged) await stageAll(repo);
|
|
2598
|
+
const diff = await getStagedDiff(repo);
|
|
2599
|
+
if (!diff.trim()) return null;
|
|
2600
|
+
const stats = await getStagedDiffStats(repo);
|
|
2601
|
+
if (scan2) await scanStagedChanges(repo);
|
|
2602
|
+
const message = await generateCommitMessage(diff, config, args.type);
|
|
2603
|
+
if (!message) throw new Error("The model returned an empty message.");
|
|
2604
|
+
return { ...repository, message, stats };
|
|
2605
|
+
}));
|
|
2606
|
+
for (const [index, result] of results2.entries()) {
|
|
2607
|
+
if (result.status === "rejected") throw new Error(`${repositories[index].pendingLabel}: ${errorMessage(result.reason)}`);
|
|
2608
|
+
}
|
|
2609
|
+
return { results: results2, scan: scan2 };
|
|
2610
|
+
});
|
|
2611
|
+
if (!scan) log.info("Gitleaks not found, skipping secret scan.", { spacing: 0 });
|
|
2612
|
+
const drafts = [];
|
|
2613
|
+
for (const [index, result] of results.entries()) {
|
|
2614
|
+
if (result.status === "fulfilled") {
|
|
2615
|
+
if (result.value) drafts.push(result.value);
|
|
2616
|
+
else log.info(`${repositories[index].pendingLabel}: nothing to commit.`, { spacing: 0 });
|
|
2719
2617
|
}
|
|
2720
|
-
return runGitTask(label, "pull", `${label}: pulled`, (onHook, onOutput) => git(["pull", "--ff-only", "--no-rebase", "--no-autostash"], repo, onHook, onOutput), pullLog?.group(label));
|
|
2721
|
-
}));
|
|
2722
|
-
loader?.clear();
|
|
2723
|
-
const failed = pulls.some((result) => result.status === "rejected" || result.value === false);
|
|
2724
|
-
if (failed) pullLog?.error("Pulling failed");
|
|
2725
|
-
else pullLog?.success("Repositories pulled", { showLog: true });
|
|
2726
|
-
for (const [index, result] of pulls.entries()) {
|
|
2727
|
-
if (result.status === "rejected") reportGitFailure(`${basename(repos[index])}: pull failed`, result.reason);
|
|
2728
|
-
else if (typeof result.value === "string") log.warn(result.value);
|
|
2729
2618
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2619
|
+
const selected = drafts.length ? await reviewMessages(drafts, interactive && !args.yes) : [];
|
|
2620
|
+
if (!selected) {
|
|
2621
|
+
cancel("Cancelled (changes left staged).");
|
|
2732
2622
|
return;
|
|
2733
2623
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
} catch (error) {
|
|
2739
|
-
log.error(`Gitleaks check failed: ${errorMessage(error)}`);
|
|
2740
|
-
process.exitCode = 1;
|
|
2741
|
-
return;
|
|
2742
|
-
}
|
|
2743
|
-
if (!scan) log.info("Gitleaks not found, skipping secret scan.");
|
|
2744
|
-
loader?.start("Preparing repositories and generating commit messages in parallel");
|
|
2745
|
-
const repositories = repos.map((repo) => ({ repo, label: basename(repo), pendingLabel: basename(repo) }));
|
|
2746
|
-
const results = await Promise.allSettled(repositories.map(async (repository) => {
|
|
2747
|
-
const { repo } = repository;
|
|
2748
|
-
const { branch, hasChanges } = await getRepoStatus(repo);
|
|
2749
|
-
repository.label = `${basename(repo)} (${branch})`;
|
|
2750
|
-
repository.pendingLabel = hasChanges ? styleText4("bold", repository.label) : repository.label;
|
|
2751
|
-
if (!args.staged) await stageAll(repo);
|
|
2752
|
-
const diff = await getStagedDiff(repo);
|
|
2753
|
-
if (!diff.trim()) return null;
|
|
2754
|
-
const stats = await getStagedDiffStats(repo);
|
|
2755
|
-
if (scan) await scanStagedChanges(repo);
|
|
2756
|
-
const message = await generateCommitMessage(diff, config, args.type);
|
|
2757
|
-
if (!message) throw new Error("The model returned an empty message.");
|
|
2758
|
-
return { ...repository, message, stats };
|
|
2759
|
-
}));
|
|
2760
|
-
loader?.stop("Repositories prepared");
|
|
2761
|
-
const drafts = [];
|
|
2762
|
-
for (const [index, result] of results.entries()) {
|
|
2763
|
-
if (result.status === "rejected") {
|
|
2764
|
-
log.error(`${repositories[index].pendingLabel}: ${errorMessage(result.reason)}`);
|
|
2765
|
-
process.exitCode = 1;
|
|
2766
|
-
} else if (result.value) {
|
|
2767
|
-
drafts.push(result.value);
|
|
2768
|
-
} else {
|
|
2769
|
-
log.info(`${repositories[index].pendingLabel}: nothing to commit.`);
|
|
2770
|
-
}
|
|
2771
|
-
}
|
|
2772
|
-
const selected = drafts.length ? await reviewMessages(drafts, interactive && !args.yes) : [];
|
|
2773
|
-
if (!selected) {
|
|
2774
|
-
cancel("Cancelled (changes left staged).");
|
|
2775
|
-
return;
|
|
2776
|
-
}
|
|
2777
|
-
const toSend = [];
|
|
2778
|
-
let previousBranchInput = "";
|
|
2779
|
-
for (const draft of selected) {
|
|
2780
|
-
try {
|
|
2624
|
+
const toSend = [];
|
|
2625
|
+
let previousBranchInput = "";
|
|
2626
|
+
for (const draft of selected) {
|
|
2627
|
+
let withGuide = true;
|
|
2781
2628
|
const current = await currentBranch(draft.repo);
|
|
2782
2629
|
if (/^(main|master|principal|dev|development)$/.test(current)) {
|
|
2783
2630
|
if (!interactive) throw new Error("Committing on a protected branch requires a terminal to choose a branch. Changes remain staged.");
|
|
@@ -2790,70 +2637,54 @@ var commitCommand = defineCommand({
|
|
|
2790
2637
|
return;
|
|
2791
2638
|
}
|
|
2792
2639
|
previousBranchInput = answer.trim();
|
|
2640
|
+
withGuide = Boolean(previousBranchInput);
|
|
2793
2641
|
const branch = previousBranchInput || current;
|
|
2794
2642
|
if (branch !== current) {
|
|
2795
2643
|
await switchBranch(draft.repo, branch);
|
|
2796
2644
|
draft.label = `${basename(draft.repo)} (${branch})`;
|
|
2797
|
-
draft.pendingLabel =
|
|
2645
|
+
draft.pendingLabel = styleText3("bold", draft.label);
|
|
2798
2646
|
}
|
|
2799
2647
|
}
|
|
2800
|
-
|
|
2801
|
-
process.exitCode = 1;
|
|
2802
|
-
continue;
|
|
2803
|
-
}
|
|
2648
|
+
await runTask(`${draft.label}: commit`, `${draft.label}: committed ${draft.message}`, (onOutput) => commit(draft.repo, draft.message, void 0, onOutput), withGuide);
|
|
2804
2649
|
toSend.push(draft);
|
|
2805
|
-
} catch (error) {
|
|
2806
|
-
reportGitFailure(`${draft.pendingLabel}: commit failed`, error);
|
|
2807
|
-
process.exitCode = 1;
|
|
2808
2650
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
for (const { repo, label } of toSend) {
|
|
2814
|
-
try {
|
|
2651
|
+
if (toSend.length === 0) return;
|
|
2652
|
+
if (!interactive && !args.push) return;
|
|
2653
|
+
const destinations = [];
|
|
2654
|
+
for (const { repo, label } of toSend) {
|
|
2815
2655
|
const branch = await currentBranch(repo);
|
|
2816
|
-
const remote = await
|
|
2656
|
+
const remote = await getPushRemote(repo, branch);
|
|
2817
2657
|
if (remote) destinations.push({ repo, label, branch, remote });
|
|
2818
|
-
else log.warn(`${label}: no
|
|
2819
|
-
} catch (error) {
|
|
2820
|
-
reportGitFailure(`${label}: push failed`, error);
|
|
2821
|
-
process.exitCode = 1;
|
|
2822
|
-
return;
|
|
2658
|
+
else log.warn(`${label}: no unambiguous push remote. Skipping push; commit remains local.`, { spacing: 0 });
|
|
2823
2659
|
}
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
outro("Committed. Not pushed.");
|
|
2833
|
-
return;
|
|
2834
|
-
}
|
|
2835
|
-
shouldPush = answer;
|
|
2836
|
-
}
|
|
2837
|
-
if (shouldPush) {
|
|
2838
|
-
for (const { repo, label, branch, remote } of destinations) {
|
|
2839
|
-
if (!await runGitTask(label, "push", `${label}: pushed to ${remote}/${branch}`, (onHook, onOutput) => push(repo, remote, branch, onHook, onOutput))) {
|
|
2840
|
-
process.exitCode = 1;
|
|
2841
|
-
outro("Sending stopped after a Git failure.");
|
|
2660
|
+
let shouldPush = Boolean(args.push);
|
|
2661
|
+
if (!shouldPush && interactive && destinations.length) {
|
|
2662
|
+
const answer = await confirm({
|
|
2663
|
+
message: `Push ${destinations.map(({ label }) => label).join(", ")}?`,
|
|
2664
|
+
initialValue: false
|
|
2665
|
+
});
|
|
2666
|
+
if (isCancel(answer)) {
|
|
2667
|
+
cancel("Committed. Not pushed.");
|
|
2842
2668
|
return;
|
|
2843
2669
|
}
|
|
2670
|
+
shouldPush = answer;
|
|
2844
2671
|
}
|
|
2672
|
+
if (shouldPush) {
|
|
2673
|
+
for (const { repo, label, branch, remote } of destinations) {
|
|
2674
|
+
await runTask(`${label}: push`, `${label}: pushed to ${remote}/${branch}`, (onOutput) => push(repo, remote, branch, void 0, onOutput));
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
if (interactive) outro("Done.");
|
|
2678
|
+
} catch (error) {
|
|
2679
|
+
log.error(errorMessage(error), { spacing: 0 });
|
|
2680
|
+
process.exitCode = 1;
|
|
2845
2681
|
}
|
|
2846
|
-
if (interactive) outro(process.exitCode ? "Finished with errors." : "Done.");
|
|
2847
2682
|
}
|
|
2848
2683
|
});
|
|
2849
|
-
function reportGitFailure(message, error) {
|
|
2850
|
-
if (error instanceof GitOutputError && error.hookFailureReported) return;
|
|
2851
|
-
log.error(error instanceof GitOutputError ? message : `${message}: ${errorMessage(error)}`);
|
|
2852
|
-
}
|
|
2853
2684
|
async function reviewMessages(drafts, canPrompt) {
|
|
2854
2685
|
while (true) {
|
|
2855
|
-
for (const { stats, pendingLabel, message } of drafts) {
|
|
2856
|
-
log.message(`(${
|
|
2686
|
+
for (const [index, { stats, pendingLabel, message }] of drafts.entries()) {
|
|
2687
|
+
log.message(`(${styleText3("green", `+${stats.additions}`)} ${styleText3("red", `-${stats.deletions}`)}) ${pendingLabel}: ${message}`, { spacing: index === 0 ? 1 : 0 });
|
|
2857
2688
|
}
|
|
2858
2689
|
if (!canPrompt) return drafts;
|
|
2859
2690
|
const action = await select({
|