@kyubiware/commit-mint 0.9.3 → 0.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 +5 -4
- package/dist/bin.mjs +137 -63
- package/dist/bin.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ cmint # interactive cli for committing changes
|
|
|
72
72
|
```bash
|
|
73
73
|
cmint # interactive: stage → checks → review → commit
|
|
74
74
|
cmint -a # auto-group, generate messages, commit everything
|
|
75
|
+
cmint -a 3 # auto-group into exactly 3 commits
|
|
75
76
|
cmint -s # stage all tracked files in single commit, skip staging menu
|
|
76
77
|
cmint config # edit provider, model, locale, etc.
|
|
77
78
|
cmint update # update cmint to the latest published version
|
|
@@ -185,7 +186,7 @@ single raw-stderr entry.
|
|
|
185
186
|
|
|
186
187
|
| Flag | Description |
|
|
187
188
|
|---|---|
|
|
188
|
-
| `-a`, `--auto` | Auto-
|
|
189
|
+
| `-a`, `--auto [N]` | Auto-group files into N commits (default: LLM decides). Use `-a 0` or `-a` for AI-determined groups |
|
|
189
190
|
| `-m`, `--message <msg>` | Use your own message instead of AI generation |
|
|
190
191
|
| `-H`, `--hint <hint>` | Context hint to the AI (e.g. `"refactor only"`) |
|
|
191
192
|
| `-r`, `--retry` | Retry last failed commit (uses cached message) |
|
|
@@ -274,7 +275,7 @@ returns either.
|
|
|
274
275
|
| Form | Behavior |
|
|
275
276
|
|---|---|
|
|
276
277
|
| `string` | Matched files are appended as trailing arguments. Paths with spaces are quoted automatically. |
|
|
277
|
-
| `string[]` | Commands run sequentially, each as a separate command.
|
|
278
|
+
| `string[]` | Commands run sequentially, each as a separate command. All commands run regardless of failures. |
|
|
278
279
|
| `(files) => string \| string[]` | Function receives the matched files. Use when the command depends on the file list. |
|
|
279
280
|
|
|
280
281
|
**String command:**
|
|
@@ -313,8 +314,8 @@ export default {
|
|
|
313
314
|
|
|
314
315
|
- Checks run after `git add`, before the AI call.
|
|
315
316
|
- Globs are processed in declaration order.
|
|
316
|
-
- Commands run sequentially per glob.
|
|
317
|
-
and
|
|
317
|
+
- Commands run sequentially per glob. All commands and all globs always run;
|
|
318
|
+
failures are collected and returned together.
|
|
318
319
|
- 60s timeout per command. ENOENT (command not found) and timeouts are
|
|
319
320
|
reported back to the menu as their own error.
|
|
320
321
|
- Skipped entirely with `cmint -N` or the `c` hotkey toggle.
|
package/dist/bin.mjs
CHANGED
|
@@ -33,7 +33,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
33
33
|
//#region package.json
|
|
34
34
|
var package_default = {
|
|
35
35
|
name: "@kyubiware/commit-mint",
|
|
36
|
-
version: "0.
|
|
36
|
+
version: "0.10.0",
|
|
37
37
|
description: "🌿 AI-powered git commit tool — auto-group changed files, generate messages, run pre-commit checks",
|
|
38
38
|
type: "module",
|
|
39
39
|
bin: { "cmint": "./dist/bin.mjs" },
|
|
@@ -1727,8 +1727,8 @@ function statusIndicator(status) {
|
|
|
1727
1727
|
function buildFileSummary(files) {
|
|
1728
1728
|
return files.map((f) => `${f.path} (${statusIndicator(f.status)})`).join("\n");
|
|
1729
1729
|
}
|
|
1730
|
-
function buildGroupingSystemPrompt() {
|
|
1731
|
-
|
|
1730
|
+
function buildGroupingSystemPrompt(groupCount) {
|
|
1731
|
+
const lines = [
|
|
1732
1732
|
"You are analyzing changed files in a git repository. Group them into logical commits based on what changed and why. Each group should be a coherent unit of work.",
|
|
1733
1733
|
"",
|
|
1734
1734
|
"Rules:",
|
|
@@ -1745,7 +1745,9 @@ function buildGroupingSystemPrompt() {
|
|
|
1745
1745
|
"files: array of exact file paths from the input",
|
|
1746
1746
|
"",
|
|
1747
1747
|
"Output ONLY valid JSON. No markdown fences, no explanation."
|
|
1748
|
-
]
|
|
1748
|
+
];
|
|
1749
|
+
if (groupCount && groupCount > 0) lines.unshift(`Create exactly ${groupCount} groups.`);
|
|
1750
|
+
return lines.join("\n");
|
|
1749
1751
|
}
|
|
1750
1752
|
function buildGroupingUserPrompt(summary) {
|
|
1751
1753
|
return [
|
|
@@ -1778,7 +1780,24 @@ function buildRetryGroupingPrompt() {
|
|
|
1778
1780
|
"Output ONLY valid JSON. No markdown fences, no explanation."
|
|
1779
1781
|
].join("\n");
|
|
1780
1782
|
}
|
|
1781
|
-
|
|
1783
|
+
/**
|
|
1784
|
+
* When a specific group count is requested but there are fewer included files
|
|
1785
|
+
* than groups, create one-file-per-group groups without calling the AI.
|
|
1786
|
+
* Returns null when the pre-condition is not met.
|
|
1787
|
+
*/
|
|
1788
|
+
function createPerFileGroups(files, groupCount, excluded) {
|
|
1789
|
+
if (!(groupCount && groupCount > 0 && files.length < groupCount)) return null;
|
|
1790
|
+
debug("generateGroups: %d files < %d requested groups, creating per-file groups", files.length, groupCount);
|
|
1791
|
+
return {
|
|
1792
|
+
groups: files.map((f) => ({
|
|
1793
|
+
name: f.path.split("/").pop() || f.path,
|
|
1794
|
+
description: `Changes to ${f.path}`,
|
|
1795
|
+
files: [f.path]
|
|
1796
|
+
})),
|
|
1797
|
+
excluded
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
async function generateGroups(files, apiKey, model, timeout, provider, proxy, groupCount) {
|
|
1782
1801
|
debug("generateGroups: %d files, model=%s", files.length, model ?? "default");
|
|
1783
1802
|
const { included, excluded } = filterExcludedFiles(files);
|
|
1784
1803
|
if (included.length === 0) {
|
|
@@ -1788,8 +1807,10 @@ async function generateGroups(files, apiKey, model, timeout, provider, proxy) {
|
|
|
1788
1807
|
excluded
|
|
1789
1808
|
};
|
|
1790
1809
|
}
|
|
1810
|
+
const perFile = createPerFileGroups(included, groupCount, excluded);
|
|
1811
|
+
if (perFile) return perFile;
|
|
1791
1812
|
const summary = buildFileSummary(included);
|
|
1792
|
-
const systemPrompt = buildGroupingSystemPrompt();
|
|
1813
|
+
const systemPrompt = buildGroupingSystemPrompt(groupCount);
|
|
1793
1814
|
const userPrompt = buildGroupingUserPrompt(summary);
|
|
1794
1815
|
debug("File summary:\n%s", summary);
|
|
1795
1816
|
debug("User prompt length: %d chars", userPrompt.length);
|
|
@@ -2287,30 +2308,29 @@ function resolveCommands(commands, matchedFiles) {
|
|
|
2287
2308
|
/**
|
|
2288
2309
|
* Run resolved commands for a single glob entry, appending results.
|
|
2289
2310
|
* Function-originated commands run as-is; string commands get matched files appended.
|
|
2290
|
-
*
|
|
2311
|
+
* All commands run regardless of individual failures.
|
|
2291
2312
|
*/
|
|
2292
|
-
async function runCommandsForGlob(cmds, matchedFiles, timeout, results, repoRoot) {
|
|
2313
|
+
async function runCommandsForGlob(cmds, matchedFiles, timeout, results, repoRoot, observer) {
|
|
2293
2314
|
for (const { command, fromFunction } of cmds) {
|
|
2294
2315
|
const fullCommand = fromFunction ? command : buildCommand(command, matchedFiles);
|
|
2316
|
+
const tool = extractToolName(fullCommand) ?? fullCommand.split(" ")[0];
|
|
2295
2317
|
debug("runCommandsForGlob: running '%s'", fullCommand);
|
|
2318
|
+
observer?.onStart?.(tool, fullCommand, matchedFiles);
|
|
2296
2319
|
const result = await runCommand(fullCommand, timeout, repoRoot);
|
|
2297
2320
|
results.push({
|
|
2298
2321
|
...result,
|
|
2299
2322
|
files: matchedFiles
|
|
2300
2323
|
});
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
return false;
|
|
2304
|
-
}
|
|
2324
|
+
observer?.onResult?.(result);
|
|
2325
|
+
if (!result.ok) debug("runCommandsForGlob: check failed, continuing to next command");
|
|
2305
2326
|
}
|
|
2306
|
-
return true;
|
|
2307
2327
|
}
|
|
2308
2328
|
/**
|
|
2309
2329
|
* Run all user-defined checks from .cmintrc against staged files.
|
|
2310
2330
|
* Returns a no-op result when no config exists.
|
|
2311
|
-
*
|
|
2331
|
+
* All checks always run; failures are collected and returned together.
|
|
2312
2332
|
*/
|
|
2313
|
-
async function runAllChecks(repoRoot, stagedFiles, timeout) {
|
|
2333
|
+
async function runAllChecks(repoRoot, stagedFiles, timeout, observer) {
|
|
2314
2334
|
debug("runAllChecks: %d staged files, checking for config in %s", stagedFiles.length, repoRoot);
|
|
2315
2335
|
if (!await detectConfig(repoRoot)) {
|
|
2316
2336
|
debug("runAllChecks: no config found, skipping checks");
|
|
@@ -2329,10 +2349,7 @@ async function runAllChecks(repoRoot, stagedFiles, timeout) {
|
|
|
2329
2349
|
continue;
|
|
2330
2350
|
}
|
|
2331
2351
|
debug("runAllChecks: pattern '%s' matched %d files", glob, matchedFiles.length);
|
|
2332
|
-
|
|
2333
|
-
ok: false,
|
|
2334
|
-
results
|
|
2335
|
-
};
|
|
2352
|
+
await runCommandsForGlob(resolveCommands(commands, matchedFiles), matchedFiles, timeout, results, repoRoot, observer);
|
|
2336
2353
|
}
|
|
2337
2354
|
const ok = results.every((r) => r.ok);
|
|
2338
2355
|
debug("runAllChecks: complete — ok=%s, %d results", ok, results.length);
|
|
@@ -2414,7 +2431,9 @@ function extractEslintDiagnostics(raw) {
|
|
|
2414
2431
|
function extractTestFailures(raw) {
|
|
2415
2432
|
const failures = [];
|
|
2416
2433
|
const seen = /* @__PURE__ */ new Set();
|
|
2417
|
-
|
|
2434
|
+
const lines = raw.split("\n");
|
|
2435
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2436
|
+
const line = lines[i];
|
|
2418
2437
|
const match = TEST_FILE_FAIL.exec(line);
|
|
2419
2438
|
if (!match) continue;
|
|
2420
2439
|
const file = (match[1] ?? "").trim();
|
|
@@ -2423,13 +2442,35 @@ function extractTestFailures(raw) {
|
|
|
2423
2442
|
const key = `${file}\u0000${name}`;
|
|
2424
2443
|
if (seen.has(key)) continue;
|
|
2425
2444
|
seen.add(key);
|
|
2445
|
+
const message = scanForwardForMessage(lines, i + 1, TEST_FILE_FAIL);
|
|
2426
2446
|
failures.push({
|
|
2427
2447
|
file,
|
|
2428
|
-
name
|
|
2448
|
+
name,
|
|
2449
|
+
message
|
|
2429
2450
|
});
|
|
2430
2451
|
}
|
|
2431
2452
|
return failures;
|
|
2432
2453
|
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Scan forward from `startIndex` to find the first assertion error message line
|
|
2456
|
+
* after a FAIL header. Skips blank lines, separator dashes, stack frames,
|
|
2457
|
+
* subsequent FAIL lines, and vitest summary footers.
|
|
2458
|
+
*/
|
|
2459
|
+
function scanForwardForMessage(lines, startIndex, testFileFailRegex) {
|
|
2460
|
+
for (let j = startIndex; j < lines.length; j++) {
|
|
2461
|
+
const rawLine = lines[j];
|
|
2462
|
+
const trimmed = rawLine.trim();
|
|
2463
|
+
if (!trimmed) continue;
|
|
2464
|
+
const stripped = trimmed.replace(/\s/g, "");
|
|
2465
|
+
if (stripped.length > 5) {
|
|
2466
|
+
if ((stripped.match(/[⎯\-═]/g) ?? []).length / stripped.length > .5) continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (/^\s*[❯>]\s/.test(rawLine)) continue;
|
|
2469
|
+
if (testFileFailRegex.test(trimmed)) break;
|
|
2470
|
+
if (/^(Test Files|Tests)\s/.test(trimmed)) break;
|
|
2471
|
+
return truncate(trimmed, MAX_SUMMARY_LINE_LENGTH);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2433
2474
|
function formatTestFailureSummary(failures, tool) {
|
|
2434
2475
|
const total = failures.length;
|
|
2435
2476
|
const visible = failures.slice(0, MAX_TEST_FAILURES);
|
|
@@ -2440,13 +2481,19 @@ function formatTestFailureSummary(failures, tool) {
|
|
|
2440
2481
|
const lines = [` ${red("•")} [${tool}] ${total} failed ${testNoun} in ${fileCount} ${fileNoun}`];
|
|
2441
2482
|
const byFile = /* @__PURE__ */ new Map();
|
|
2442
2483
|
for (const failure of visible) {
|
|
2443
|
-
const
|
|
2444
|
-
|
|
2445
|
-
|
|
2484
|
+
const entries = byFile.get(failure.file) ?? [];
|
|
2485
|
+
entries.push({
|
|
2486
|
+
name: failure.name,
|
|
2487
|
+
message: failure.message
|
|
2488
|
+
});
|
|
2489
|
+
byFile.set(failure.file, entries);
|
|
2446
2490
|
}
|
|
2447
|
-
for (const [file,
|
|
2491
|
+
for (const [file, entries] of byFile) {
|
|
2448
2492
|
lines.push(` ${truncate(file, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
2449
|
-
for (const
|
|
2493
|
+
for (const entry of entries) {
|
|
2494
|
+
lines.push(` ${red("×")} ${truncate(entry.name, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
2495
|
+
if (entry.message) lines.push(` ${dim("→")} ${entry.message}`);
|
|
2496
|
+
}
|
|
2450
2497
|
}
|
|
2451
2498
|
if (hidden > 0) lines.push(dim(` +${hidden} more failed ${hidden === 1 ? "test" : "tests"}. View full output for details.`));
|
|
2452
2499
|
return lines.join("\n");
|
|
@@ -2543,24 +2590,42 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
|
2543
2590
|
}
|
|
2544
2591
|
}
|
|
2545
2592
|
//#endregion
|
|
2546
|
-
//#region src/ui/check-
|
|
2593
|
+
//#region src/ui/check-progress.ts
|
|
2547
2594
|
/**
|
|
2548
|
-
*
|
|
2595
|
+
* Creates a live check progress display.
|
|
2549
2596
|
*
|
|
2550
|
-
* -
|
|
2551
|
-
*
|
|
2552
|
-
* -
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2597
|
+
* - Each check gets its own line. While a check runs, a spinner animates
|
|
2598
|
+
* in the status position. When it completes, the spinner becomes a ✓ or ✗.
|
|
2599
|
+
* - There is no separate "Running checks…" header — the spinner IS the
|
|
2600
|
+
* check line.
|
|
2601
|
+
* - When all checks finish, a `log.info` summary line is emitted.
|
|
2602
|
+
*
|
|
2603
|
+
* Returns a `CheckObserver` (pass directly to `runAllChecks`) plus a `finish()`
|
|
2604
|
+
* method that prints the final summary.
|
|
2555
2605
|
*/
|
|
2556
|
-
function
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2606
|
+
function createCheckProgressDisplay() {
|
|
2607
|
+
const s = spinner();
|
|
2608
|
+
const toolResults = [];
|
|
2609
|
+
let started = false;
|
|
2610
|
+
return {
|
|
2611
|
+
onStart(_tool, _command, _matchedFiles) {
|
|
2612
|
+
if (!started) {
|
|
2613
|
+
s.start(_tool);
|
|
2614
|
+
started = true;
|
|
2615
|
+
} else s.message(_tool);
|
|
2616
|
+
},
|
|
2617
|
+
onResult(result) {
|
|
2618
|
+
toolResults.push({
|
|
2619
|
+
tool: result.tool,
|
|
2620
|
+
ok: result.ok
|
|
2621
|
+
});
|
|
2622
|
+
s.message(`${result.ok ? "✓" : "✗"} ${result.tool}`);
|
|
2623
|
+
},
|
|
2624
|
+
finish(ok) {
|
|
2625
|
+
s.stop(ok ? green("All checks passed") : red("Some checks failed"));
|
|
2626
|
+
if (toolResults.length > 0) log.info(toolResults.map((r) => ` ${r.ok ? green("✓") : red("✗")} ${r.tool}`).join("\n"));
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2564
2629
|
}
|
|
2565
2630
|
//#endregion
|
|
2566
2631
|
//#region src/commands/check-phase.ts
|
|
@@ -2569,7 +2634,7 @@ function stopCheckSpinner(spinner, results) {
|
|
|
2569
2634
|
*
|
|
2570
2635
|
* Single entry point for the check-execution pipeline shared by `runPreCommitChecks`
|
|
2571
2636
|
* (post-staging, normal commit flow) and `runAutoGroupFlow` (pre-staging, auto-group
|
|
2572
|
-
* flow). Encapsulates: detectConfig guard →
|
|
2637
|
+
* flow). Encapsulates: detectConfig guard → live progress display → runAllChecks → retry loop with
|
|
2573
2638
|
* `showCheckFailureMenu`.
|
|
2574
2639
|
*
|
|
2575
2640
|
* Caller responsibilities:
|
|
@@ -2585,10 +2650,9 @@ function stopCheckSpinner(spinner, results) {
|
|
|
2585
2650
|
async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
|
|
2586
2651
|
if (!await detectConfig(repoRoot)) return "passed";
|
|
2587
2652
|
debug("Running user checks on %d files...", files.length);
|
|
2588
|
-
const
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
stopCheckSpinner(ck, checkResults);
|
|
2653
|
+
const display = createCheckProgressDisplay();
|
|
2654
|
+
let checkResults = await runAllChecks(repoRoot, files, timeout, display);
|
|
2655
|
+
display.finish(checkResults.ok);
|
|
2592
2656
|
debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2593
2657
|
while (!checkResults.ok) {
|
|
2594
2658
|
const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
|
|
@@ -2599,9 +2663,9 @@ async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
|
|
|
2599
2663
|
if (menuResult === "retried") {
|
|
2600
2664
|
debug("Re-running checks after retry...");
|
|
2601
2665
|
if (onRetry) await onRetry();
|
|
2602
|
-
|
|
2603
|
-
checkResults = await runAllChecks(repoRoot, files, timeout);
|
|
2604
|
-
|
|
2666
|
+
const retryDisplay = createCheckProgressDisplay();
|
|
2667
|
+
checkResults = await runAllChecks(repoRoot, files, timeout, retryDisplay);
|
|
2668
|
+
retryDisplay.finish(checkResults.ok);
|
|
2605
2669
|
debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2606
2670
|
continue;
|
|
2607
2671
|
}
|
|
@@ -2662,13 +2726,14 @@ async function runAutoGroupFlow(changedFiles, flags) {
|
|
|
2662
2726
|
await setConfigValue(configKey, String(key).trim());
|
|
2663
2727
|
debug("API key saved to config");
|
|
2664
2728
|
}
|
|
2729
|
+
const groupCount = typeof flags.auto === "number" ? flags.auto : 0;
|
|
2665
2730
|
const s = spinner();
|
|
2666
2731
|
s.start("Analyzing files...");
|
|
2667
|
-
const validatedGroups = validateGroups((await generateGroups(included, await getProviderApiKey(provider), getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel), config.timeout ? parseInt(config.timeout, 10) : void 0, provider, config.proxy)).groups, included);
|
|
2732
|
+
const validatedGroups = validateGroups((await generateGroups(included, await getProviderApiKey(provider), getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel), config.timeout ? parseInt(config.timeout, 10) : void 0, provider, config.proxy, groupCount)).groups, included);
|
|
2668
2733
|
s.stop("Files analyzed");
|
|
2669
2734
|
showGroupedFiles(validatedGroups, included);
|
|
2670
2735
|
const autoAccept = await getAutoAccept();
|
|
2671
|
-
const skipPrompts = flags.auto || autoAccept;
|
|
2736
|
+
const skipPrompts = flags.auto !== false || autoAccept;
|
|
2672
2737
|
if (skipPrompts) debug("Skipping grouping confirmation (auto=%s autoAccept=%s)", flags.auto, autoAccept);
|
|
2673
2738
|
else if (!await showGroupingConfirmation(validatedGroups, excluded)) {
|
|
2674
2739
|
outro(dim("Cancelled."));
|
|
@@ -3691,11 +3756,13 @@ function buildMultiSelectRender(message, options, output) {
|
|
|
3691
3756
|
return `${header}\n${dim(S_BAR_END)} ${cancelled}`;
|
|
3692
3757
|
}
|
|
3693
3758
|
default: {
|
|
3759
|
+
const stdio = output ?? process.stdout;
|
|
3760
|
+
const termRows = ("rows" in stdio ? stdio.rows : void 0) ?? 24;
|
|
3694
3761
|
const lines = limitOptions({
|
|
3695
3762
|
cursor,
|
|
3696
3763
|
options,
|
|
3697
3764
|
style: (opt, active) => renderCheckboxOption(opt.label, value.includes(opt.value), active),
|
|
3698
|
-
maxItems:
|
|
3765
|
+
maxItems: Math.min(options.length, Math.max(5, termRows - 3)),
|
|
3699
3766
|
output: output ?? process.stdout
|
|
3700
3767
|
}).map((line) => `${dim(S_BAR)} ${line}`);
|
|
3701
3768
|
const hintLine = "↑/↓ navigate · space select · enter confirm · A select all · N select none";
|
|
@@ -3806,8 +3873,8 @@ function buildPromptRenderer(opts, state) {
|
|
|
3806
3873
|
const toggleList = opts.toggles;
|
|
3807
3874
|
return function() {
|
|
3808
3875
|
const sym = symbol(this.state);
|
|
3809
|
-
const statusLines = toggleList.map((t) => renderToggleState(t, state[t.hotkey])).join("\n");
|
|
3810
|
-
const header = `${sym} ${opts.message}\n${
|
|
3876
|
+
const statusLines = toggleList.map((t) => `${dim(S_BAR)} ${renderToggleState(t, state[t.hotkey])}`).join("\n");
|
|
3877
|
+
const header = `${sym} ${opts.message}\n${statusLines}`;
|
|
3811
3878
|
switch (this.state) {
|
|
3812
3879
|
case "submit": {
|
|
3813
3880
|
const selected = optionList[this.cursor];
|
|
@@ -3820,11 +3887,14 @@ function buildPromptRenderer(opts, state) {
|
|
|
3820
3887
|
return `${header}\n${dim(S_BAR)} ${styleText(["strikethrough", "dim"], text)}\n${dim(S_BAR_END)}`;
|
|
3821
3888
|
}
|
|
3822
3889
|
default: {
|
|
3890
|
+
const stdio = opts.output ?? process.stdout;
|
|
3891
|
+
const termRows = ("rows" in stdio ? stdio.rows : void 0) ?? 24;
|
|
3892
|
+
const dynamicMax = Math.min(optionList.length, Math.max(5, termRows - 5));
|
|
3823
3893
|
const lines = limitOptions({
|
|
3824
3894
|
cursor: this.cursor,
|
|
3825
3895
|
options: optionList,
|
|
3826
3896
|
style: (opt, active) => renderOption(opt, active),
|
|
3827
|
-
maxItems:
|
|
3897
|
+
maxItems: dynamicMax,
|
|
3828
3898
|
output: opts.output ?? process.stdout
|
|
3829
3899
|
}).map((line) => `${dim(S_BAR)} ${line}`);
|
|
3830
3900
|
const hotkeysHint = toggleList.map((t) => `\`${t.hotkey}\` toggle ${t.label.toLowerCase()}`).join(" • ");
|
|
@@ -3972,10 +4042,9 @@ async function handleStaging(changedFiles, flags) {
|
|
|
3972
4042
|
await stageAll();
|
|
3973
4043
|
const allFiles = await getStagedFiles();
|
|
3974
4044
|
if (await detectConfig(repoRoot)) {
|
|
3975
|
-
const
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
stopCheckSpinner(ckSpinner, ckResult);
|
|
4045
|
+
const display = createCheckProgressDisplay();
|
|
4046
|
+
const ckResult = await runAllChecks(repoRoot, allFiles, 6e4, display);
|
|
4047
|
+
display.finish(ckResult.ok);
|
|
3979
4048
|
if (!ckResult.ok) for (const r of ckResult.results.filter((r) => !r.ok)) log.info(r.stderr?.trim() || r.stdout?.trim() || `Check failed: ${r.command}`);
|
|
3980
4049
|
}
|
|
3981
4050
|
currentFiles = await getChangedFiles();
|
|
@@ -4054,7 +4123,7 @@ async function commitCommand(flags, version) {
|
|
|
4054
4123
|
if (flags.single) {
|
|
4055
4124
|
debug("Single-commit mode: staging all files");
|
|
4056
4125
|
await stageAll();
|
|
4057
|
-
} else if (flags.auto) {
|
|
4126
|
+
} else if (flags.auto !== false) {
|
|
4058
4127
|
if (flags.message) {
|
|
4059
4128
|
outro(red("--message flag is not compatible with auto-group mode."));
|
|
4060
4129
|
return;
|
|
@@ -4356,9 +4425,10 @@ async function agentCommand(flags) {
|
|
|
4356
4425
|
}
|
|
4357
4426
|
const model = getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel);
|
|
4358
4427
|
const timeout = config.timeout ? parseInt(config.timeout, 10) : void 0;
|
|
4428
|
+
const groupCount = typeof flags.auto === "number" ? flags.auto : 0;
|
|
4359
4429
|
let groups;
|
|
4360
4430
|
try {
|
|
4361
|
-
groups = validateGroups((await generateGroups(included, apiKey, model, timeout, provider, config.proxy)).groups, included);
|
|
4431
|
+
groups = validateGroups((await generateGroups(included, apiKey, model, timeout, provider, config.proxy, groupCount)).groups, included);
|
|
4362
4432
|
} catch (err) {
|
|
4363
4433
|
process.exitCode = EXIT_CODES.AI;
|
|
4364
4434
|
writeAgentResult({
|
|
@@ -4820,8 +4890,12 @@ cli({
|
|
|
4820
4890
|
default: false
|
|
4821
4891
|
},
|
|
4822
4892
|
auto: {
|
|
4823
|
-
type:
|
|
4824
|
-
|
|
4893
|
+
type: (raw) => {
|
|
4894
|
+
if (raw === "") return true;
|
|
4895
|
+
const n = Number(raw);
|
|
4896
|
+
return Number.isNaN(n) ? true : n;
|
|
4897
|
+
},
|
|
4898
|
+
description: "Auto-group files into commits. Use -a <N> to request N groups (0 = LLM decides)",
|
|
4825
4899
|
alias: "a",
|
|
4826
4900
|
default: false
|
|
4827
4901
|
},
|