@kyubiware/commit-mint 0.9.2 โ 0.9.4
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 +3 -3
- package/dist/bin.mjs +240 -61
- package/dist/bin.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ returns either.
|
|
|
274
274
|
| Form | Behavior |
|
|
275
275
|
|---|---|
|
|
276
276
|
| `string` | Matched files are appended as trailing arguments. Paths with spaces are quoted automatically. |
|
|
277
|
-
| `string[]` | Commands run sequentially, each as a separate command.
|
|
277
|
+
| `string[]` | Commands run sequentially, each as a separate command. All commands run regardless of failures. |
|
|
278
278
|
| `(files) => string \| string[]` | Function receives the matched files. Use when the command depends on the file list. |
|
|
279
279
|
|
|
280
280
|
**String command:**
|
|
@@ -313,8 +313,8 @@ export default {
|
|
|
313
313
|
|
|
314
314
|
- Checks run after `git add`, before the AI call.
|
|
315
315
|
- Globs are processed in declaration order.
|
|
316
|
-
- Commands run sequentially per glob.
|
|
317
|
-
and
|
|
316
|
+
- Commands run sequentially per glob. All commands and all globs always run;
|
|
317
|
+
failures are collected and returned together.
|
|
318
318
|
- 60s timeout per command. ENOENT (command not found) and timeouts are
|
|
319
319
|
reported back to the menu as their own error.
|
|
320
320
|
- Skipped entirely with `cmint -N` or the `c` hotkey toggle.
|
package/dist/bin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cli, command } from "cleye";
|
|
3
3
|
import * as p$1 from "@clack/prompts";
|
|
4
|
-
import { S_BAR, S_BAR_END, S_RADIO_ACTIVE, S_RADIO_INACTIVE, intro, isCancel, limitOptions, log, outro, spinner, symbol } from "@clack/prompts";
|
|
4
|
+
import { S_BAR, S_BAR_END, S_CHECKBOX_ACTIVE, S_CHECKBOX_INACTIVE, S_CHECKBOX_SELECTED, S_RADIO_ACTIVE, S_RADIO_INACTIVE, intro, isCancel, limitOptions, log, outro, spinner, symbol } from "@clack/prompts";
|
|
5
5
|
import { bold, cyan, dim, green, red, yellow } from "kolorist";
|
|
6
6
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import os from "node:os";
|
|
@@ -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.9.
|
|
36
|
+
version: "0.9.4",
|
|
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" },
|
|
@@ -2287,30 +2287,29 @@ function resolveCommands(commands, matchedFiles) {
|
|
|
2287
2287
|
/**
|
|
2288
2288
|
* Run resolved commands for a single glob entry, appending results.
|
|
2289
2289
|
* Function-originated commands run as-is; string commands get matched files appended.
|
|
2290
|
-
*
|
|
2290
|
+
* All commands run regardless of individual failures.
|
|
2291
2291
|
*/
|
|
2292
|
-
async function runCommandsForGlob(cmds, matchedFiles, timeout, results, repoRoot) {
|
|
2292
|
+
async function runCommandsForGlob(cmds, matchedFiles, timeout, results, repoRoot, observer) {
|
|
2293
2293
|
for (const { command, fromFunction } of cmds) {
|
|
2294
2294
|
const fullCommand = fromFunction ? command : buildCommand(command, matchedFiles);
|
|
2295
|
+
const tool = extractToolName(fullCommand) ?? fullCommand.split(" ")[0];
|
|
2295
2296
|
debug("runCommandsForGlob: running '%s'", fullCommand);
|
|
2297
|
+
observer?.onStart?.(tool, fullCommand, matchedFiles);
|
|
2296
2298
|
const result = await runCommand(fullCommand, timeout, repoRoot);
|
|
2297
2299
|
results.push({
|
|
2298
2300
|
...result,
|
|
2299
2301
|
files: matchedFiles
|
|
2300
2302
|
});
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
return false;
|
|
2304
|
-
}
|
|
2303
|
+
observer?.onResult?.(result);
|
|
2304
|
+
if (!result.ok) debug("runCommandsForGlob: check failed, continuing to next command");
|
|
2305
2305
|
}
|
|
2306
|
-
return true;
|
|
2307
2306
|
}
|
|
2308
2307
|
/**
|
|
2309
2308
|
* Run all user-defined checks from .cmintrc against staged files.
|
|
2310
2309
|
* Returns a no-op result when no config exists.
|
|
2311
|
-
*
|
|
2310
|
+
* All checks always run; failures are collected and returned together.
|
|
2312
2311
|
*/
|
|
2313
|
-
async function runAllChecks(repoRoot, stagedFiles, timeout) {
|
|
2312
|
+
async function runAllChecks(repoRoot, stagedFiles, timeout, observer) {
|
|
2314
2313
|
debug("runAllChecks: %d staged files, checking for config in %s", stagedFiles.length, repoRoot);
|
|
2315
2314
|
if (!await detectConfig(repoRoot)) {
|
|
2316
2315
|
debug("runAllChecks: no config found, skipping checks");
|
|
@@ -2329,10 +2328,7 @@ async function runAllChecks(repoRoot, stagedFiles, timeout) {
|
|
|
2329
2328
|
continue;
|
|
2330
2329
|
}
|
|
2331
2330
|
debug("runAllChecks: pattern '%s' matched %d files", glob, matchedFiles.length);
|
|
2332
|
-
|
|
2333
|
-
ok: false,
|
|
2334
|
-
results
|
|
2335
|
-
};
|
|
2331
|
+
await runCommandsForGlob(resolveCommands(commands, matchedFiles), matchedFiles, timeout, results, repoRoot, observer);
|
|
2336
2332
|
}
|
|
2337
2333
|
const ok = results.every((r) => r.ok);
|
|
2338
2334
|
debug("runAllChecks: complete โ ok=%s, %d results", ok, results.length);
|
|
@@ -2414,7 +2410,9 @@ function extractEslintDiagnostics(raw) {
|
|
|
2414
2410
|
function extractTestFailures(raw) {
|
|
2415
2411
|
const failures = [];
|
|
2416
2412
|
const seen = /* @__PURE__ */ new Set();
|
|
2417
|
-
|
|
2413
|
+
const lines = raw.split("\n");
|
|
2414
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2415
|
+
const line = lines[i];
|
|
2418
2416
|
const match = TEST_FILE_FAIL.exec(line);
|
|
2419
2417
|
if (!match) continue;
|
|
2420
2418
|
const file = (match[1] ?? "").trim();
|
|
@@ -2423,13 +2421,35 @@ function extractTestFailures(raw) {
|
|
|
2423
2421
|
const key = `${file}\u0000${name}`;
|
|
2424
2422
|
if (seen.has(key)) continue;
|
|
2425
2423
|
seen.add(key);
|
|
2424
|
+
const message = scanForwardForMessage(lines, i + 1, TEST_FILE_FAIL);
|
|
2426
2425
|
failures.push({
|
|
2427
2426
|
file,
|
|
2428
|
-
name
|
|
2427
|
+
name,
|
|
2428
|
+
message
|
|
2429
2429
|
});
|
|
2430
2430
|
}
|
|
2431
2431
|
return failures;
|
|
2432
2432
|
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Scan forward from `startIndex` to find the first assertion error message line
|
|
2435
|
+
* after a FAIL header. Skips blank lines, separator dashes, stack frames,
|
|
2436
|
+
* subsequent FAIL lines, and vitest summary footers.
|
|
2437
|
+
*/
|
|
2438
|
+
function scanForwardForMessage(lines, startIndex, testFileFailRegex) {
|
|
2439
|
+
for (let j = startIndex; j < lines.length; j++) {
|
|
2440
|
+
const rawLine = lines[j];
|
|
2441
|
+
const trimmed = rawLine.trim();
|
|
2442
|
+
if (!trimmed) continue;
|
|
2443
|
+
const stripped = trimmed.replace(/\s/g, "");
|
|
2444
|
+
if (stripped.length > 5) {
|
|
2445
|
+
if ((stripped.match(/[โฏ\-โ]/g) ?? []).length / stripped.length > .5) continue;
|
|
2446
|
+
}
|
|
2447
|
+
if (/^\s*[โฏ>]\s/.test(rawLine)) continue;
|
|
2448
|
+
if (testFileFailRegex.test(trimmed)) break;
|
|
2449
|
+
if (/^(Test Files|Tests)\s/.test(trimmed)) break;
|
|
2450
|
+
return truncate(trimmed, MAX_SUMMARY_LINE_LENGTH);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2433
2453
|
function formatTestFailureSummary(failures, tool) {
|
|
2434
2454
|
const total = failures.length;
|
|
2435
2455
|
const visible = failures.slice(0, MAX_TEST_FAILURES);
|
|
@@ -2440,13 +2460,19 @@ function formatTestFailureSummary(failures, tool) {
|
|
|
2440
2460
|
const lines = [` ${red("โข")} [${tool}] ${total} failed ${testNoun} in ${fileCount} ${fileNoun}`];
|
|
2441
2461
|
const byFile = /* @__PURE__ */ new Map();
|
|
2442
2462
|
for (const failure of visible) {
|
|
2443
|
-
const
|
|
2444
|
-
|
|
2445
|
-
|
|
2463
|
+
const entries = byFile.get(failure.file) ?? [];
|
|
2464
|
+
entries.push({
|
|
2465
|
+
name: failure.name,
|
|
2466
|
+
message: failure.message
|
|
2467
|
+
});
|
|
2468
|
+
byFile.set(failure.file, entries);
|
|
2446
2469
|
}
|
|
2447
|
-
for (const [file,
|
|
2470
|
+
for (const [file, entries] of byFile) {
|
|
2448
2471
|
lines.push(` ${truncate(file, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
2449
|
-
for (const
|
|
2472
|
+
for (const entry of entries) {
|
|
2473
|
+
lines.push(` ${red("ร")} ${truncate(entry.name, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
2474
|
+
if (entry.message) lines.push(` ${dim("โ")} ${entry.message}`);
|
|
2475
|
+
}
|
|
2450
2476
|
}
|
|
2451
2477
|
if (hidden > 0) lines.push(dim(` +${hidden} more failed ${hidden === 1 ? "test" : "tests"}. View full output for details.`));
|
|
2452
2478
|
return lines.join("\n");
|
|
@@ -2468,6 +2494,21 @@ function truncate(message, maxLength) {
|
|
|
2468
2494
|
if (collapsed.length <= maxLength) return collapsed;
|
|
2469
2495
|
return `${collapsed.slice(0, Math.max(0, maxLength - 1))}โฆ`;
|
|
2470
2496
|
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Filter noisy lines from vitest/jest test output.
|
|
2499
|
+
* Strips stack trace frames (โฏ prefix) and horizontal rule separators (โฏโฏโฏ lines),
|
|
2500
|
+
* while preserving FAIL lines, assertion messages, diff content, and the final summary.
|
|
2501
|
+
*/
|
|
2502
|
+
function filterTestOutput(output) {
|
|
2503
|
+
return output.split("\n").filter((line) => {
|
|
2504
|
+
if (/^\s*[โฏ>]\s/.test(line)) return false;
|
|
2505
|
+
const stripped = line.replace(/\s/g, "");
|
|
2506
|
+
if (stripped.length > 10) {
|
|
2507
|
+
if ((stripped.match(/[โฏ\-โ]/g) ?? []).length / stripped.length > .5) return false;
|
|
2508
|
+
}
|
|
2509
|
+
return true;
|
|
2510
|
+
}).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
2511
|
+
}
|
|
2471
2512
|
async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
2472
2513
|
debug("showCheckFailureMenu: %d errors", errors.length);
|
|
2473
2514
|
let clipboardCopied = false;
|
|
@@ -2507,13 +2548,13 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
|
2507
2548
|
debug("showCheckFailureMenu: user chose %s", choice);
|
|
2508
2549
|
switch (choice) {
|
|
2509
2550
|
case "copy":
|
|
2510
|
-
if (await copyToClipboard(rawStderr)) {
|
|
2551
|
+
if (await copyToClipboard(filterTestOutput(rawStderr))) {
|
|
2511
2552
|
clipboardCopied = true;
|
|
2512
2553
|
p$1.log.step(green("Copied to clipboard."));
|
|
2513
2554
|
} else p$1.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
2514
2555
|
continue;
|
|
2515
2556
|
case "view":
|
|
2516
|
-
p$1.note(rawStderr
|
|
2557
|
+
p$1.note(filterTestOutput(rawStderr) || "(no raw output)", "Full error output");
|
|
2517
2558
|
continue;
|
|
2518
2559
|
case "retry":
|
|
2519
2560
|
if (onRetry) return "retried";
|
|
@@ -2528,24 +2569,42 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
|
2528
2569
|
}
|
|
2529
2570
|
}
|
|
2530
2571
|
//#endregion
|
|
2531
|
-
//#region src/ui/check-
|
|
2572
|
+
//#region src/ui/check-progress.ts
|
|
2532
2573
|
/**
|
|
2533
|
-
*
|
|
2574
|
+
* Creates a live check progress display.
|
|
2534
2575
|
*
|
|
2535
|
-
* -
|
|
2536
|
-
*
|
|
2537
|
-
* -
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2576
|
+
* - Each check gets its own line. While a check runs, a spinner animates
|
|
2577
|
+
* in the status position. When it completes, the spinner becomes a โ or โ.
|
|
2578
|
+
* - There is no separate "Running checksโฆ" header โ the spinner IS the
|
|
2579
|
+
* check line.
|
|
2580
|
+
* - When all checks finish, a `log.info` summary line is emitted.
|
|
2581
|
+
*
|
|
2582
|
+
* Returns a `CheckObserver` (pass directly to `runAllChecks`) plus a `finish()`
|
|
2583
|
+
* method that prints the final summary.
|
|
2540
2584
|
*/
|
|
2541
|
-
function
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2585
|
+
function createCheckProgressDisplay() {
|
|
2586
|
+
const s = spinner();
|
|
2587
|
+
const toolResults = [];
|
|
2588
|
+
let started = false;
|
|
2589
|
+
return {
|
|
2590
|
+
onStart(_tool, _command, _matchedFiles) {
|
|
2591
|
+
if (!started) {
|
|
2592
|
+
s.start(_tool);
|
|
2593
|
+
started = true;
|
|
2594
|
+
} else s.message(_tool);
|
|
2595
|
+
},
|
|
2596
|
+
onResult(result) {
|
|
2597
|
+
toolResults.push({
|
|
2598
|
+
tool: result.tool,
|
|
2599
|
+
ok: result.ok
|
|
2600
|
+
});
|
|
2601
|
+
s.message(`${result.ok ? "โ" : "โ"} ${result.tool}`);
|
|
2602
|
+
},
|
|
2603
|
+
finish(ok) {
|
|
2604
|
+
s.stop(ok ? green("All checks passed") : red("Some checks failed"));
|
|
2605
|
+
if (toolResults.length > 0) log.info(toolResults.map((r) => ` ${r.ok ? green("โ") : red("โ")} ${r.tool}`).join("\n"));
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2549
2608
|
}
|
|
2550
2609
|
//#endregion
|
|
2551
2610
|
//#region src/commands/check-phase.ts
|
|
@@ -2554,7 +2613,7 @@ function stopCheckSpinner(spinner, results) {
|
|
|
2554
2613
|
*
|
|
2555
2614
|
* Single entry point for the check-execution pipeline shared by `runPreCommitChecks`
|
|
2556
2615
|
* (post-staging, normal commit flow) and `runAutoGroupFlow` (pre-staging, auto-group
|
|
2557
|
-
* flow). Encapsulates: detectConfig guard โ
|
|
2616
|
+
* flow). Encapsulates: detectConfig guard โ live progress display โ runAllChecks โ retry loop with
|
|
2558
2617
|
* `showCheckFailureMenu`.
|
|
2559
2618
|
*
|
|
2560
2619
|
* Caller responsibilities:
|
|
@@ -2570,10 +2629,9 @@ function stopCheckSpinner(spinner, results) {
|
|
|
2570
2629
|
async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
|
|
2571
2630
|
if (!await detectConfig(repoRoot)) return "passed";
|
|
2572
2631
|
debug("Running user checks on %d files...", files.length);
|
|
2573
|
-
const
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
stopCheckSpinner(ck, checkResults);
|
|
2632
|
+
const display = createCheckProgressDisplay();
|
|
2633
|
+
let checkResults = await runAllChecks(repoRoot, files, timeout, display);
|
|
2634
|
+
display.finish(checkResults.ok);
|
|
2577
2635
|
debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2578
2636
|
while (!checkResults.ok) {
|
|
2579
2637
|
const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
|
|
@@ -2584,9 +2642,9 @@ async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
|
|
|
2584
2642
|
if (menuResult === "retried") {
|
|
2585
2643
|
debug("Re-running checks after retry...");
|
|
2586
2644
|
if (onRetry) await onRetry();
|
|
2587
|
-
|
|
2588
|
-
checkResults = await runAllChecks(repoRoot, files, timeout);
|
|
2589
|
-
|
|
2645
|
+
const retryDisplay = createCheckProgressDisplay();
|
|
2646
|
+
checkResults = await runAllChecks(repoRoot, files, timeout, retryDisplay);
|
|
2647
|
+
retryDisplay.finish(checkResults.ok);
|
|
2590
2648
|
debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2591
2649
|
continue;
|
|
2592
2650
|
}
|
|
@@ -3570,6 +3628,52 @@ let m = class {
|
|
|
3570
3628
|
}
|
|
3571
3629
|
}
|
|
3572
3630
|
};
|
|
3631
|
+
let nt = class extends m {
|
|
3632
|
+
options;
|
|
3633
|
+
cursor = 0;
|
|
3634
|
+
get _value() {
|
|
3635
|
+
return this.options[this.cursor].value;
|
|
3636
|
+
}
|
|
3637
|
+
get _enabledOptions() {
|
|
3638
|
+
return this.options.filter((t) => t.disabled !== !0);
|
|
3639
|
+
}
|
|
3640
|
+
toggleAll() {
|
|
3641
|
+
const t = this._enabledOptions, s = this.value !== void 0 && this.value.length === t.length;
|
|
3642
|
+
this.value = s ? [] : t.map((e) => e.value);
|
|
3643
|
+
}
|
|
3644
|
+
toggleInvert() {
|
|
3645
|
+
const t = this.value;
|
|
3646
|
+
if (!t) return;
|
|
3647
|
+
const s = this._enabledOptions.filter((e) => !t.includes(e.value));
|
|
3648
|
+
this.value = s.map((e) => e.value);
|
|
3649
|
+
}
|
|
3650
|
+
toggleValue() {
|
|
3651
|
+
this.value === void 0 && (this.value = []);
|
|
3652
|
+
const t = this.value.includes(this._value);
|
|
3653
|
+
this.value = t ? this.value.filter((s) => s !== this._value) : [...this.value, this._value];
|
|
3654
|
+
}
|
|
3655
|
+
constructor(t) {
|
|
3656
|
+
super(t, !1), this.options = t.options, this.value = [...t.initialValues ?? []];
|
|
3657
|
+
const s = Math.max(this.options.findIndex(({ value: e }) => e === t.cursorAt), 0);
|
|
3658
|
+
this.cursor = this.options[s].disabled ? f(s, 1, this.options) : s, this.on("key", (e) => {
|
|
3659
|
+
e === "a" && this.toggleAll(), e === "i" && this.toggleInvert();
|
|
3660
|
+
}), this.on("cursor", (e) => {
|
|
3661
|
+
switch (e) {
|
|
3662
|
+
case "left":
|
|
3663
|
+
case "up":
|
|
3664
|
+
this.cursor = f(this.cursor, -1, this.options);
|
|
3665
|
+
break;
|
|
3666
|
+
case "down":
|
|
3667
|
+
case "right":
|
|
3668
|
+
this.cursor = f(this.cursor, 1, this.options);
|
|
3669
|
+
break;
|
|
3670
|
+
case "space":
|
|
3671
|
+
this.toggleValue();
|
|
3672
|
+
break;
|
|
3673
|
+
}
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3573
3677
|
var ut = class extends m {
|
|
3574
3678
|
options;
|
|
3575
3679
|
cursor = 0;
|
|
@@ -3598,6 +3702,86 @@ var ut = class extends m {
|
|
|
3598
3702
|
}
|
|
3599
3703
|
};
|
|
3600
3704
|
//#endregion
|
|
3705
|
+
//#region src/ui/file-multiselect.ts
|
|
3706
|
+
/**
|
|
3707
|
+
* Render a checkbox-style option line with 4 visual states:
|
|
3708
|
+
* active-selected, selected, active, inactive
|
|
3709
|
+
*/
|
|
3710
|
+
function renderCheckboxOption(label, selected, active) {
|
|
3711
|
+
if (selected && active) return `${green(S_CHECKBOX_SELECTED)} ${label}`;
|
|
3712
|
+
if (selected) return `${dim(S_CHECKBOX_SELECTED)} ${dim(label)}`;
|
|
3713
|
+
if (active) return `${green(S_CHECKBOX_ACTIVE)} ${label}`;
|
|
3714
|
+
return `${dim(S_CHECKBOX_INACTIVE)} ${dim(label)}`;
|
|
3715
|
+
}
|
|
3716
|
+
/**
|
|
3717
|
+
* Build the render function for a MultiSelectPrompt with file-multiselect
|
|
3718
|
+
* styling and a hotkey hint line at the bottom.
|
|
3719
|
+
*/
|
|
3720
|
+
function buildMultiSelectRender(message, options, output) {
|
|
3721
|
+
return function() {
|
|
3722
|
+
const header = `${symbol(this.state)} ${message}`;
|
|
3723
|
+
const value = this.value ?? [];
|
|
3724
|
+
const cursor = this.cursor;
|
|
3725
|
+
switch (this.state) {
|
|
3726
|
+
case "submit": {
|
|
3727
|
+
const labels = value.map((v) => {
|
|
3728
|
+
return options.find((o) => o.value === v)?.label ?? String(v);
|
|
3729
|
+
}).join(dim(", "));
|
|
3730
|
+
return `${header}\n${dim(S_BAR)} ${dim(labels)}`;
|
|
3731
|
+
}
|
|
3732
|
+
case "cancel": {
|
|
3733
|
+
const cancelled = styleText(["strikethrough", "dim"], "Cancelled");
|
|
3734
|
+
return `${header}\n${dim(S_BAR_END)} ${cancelled}`;
|
|
3735
|
+
}
|
|
3736
|
+
default: {
|
|
3737
|
+
const lines = limitOptions({
|
|
3738
|
+
cursor,
|
|
3739
|
+
options,
|
|
3740
|
+
style: (opt, active) => renderCheckboxOption(opt.label, value.includes(opt.value), active),
|
|
3741
|
+
maxItems: 7,
|
|
3742
|
+
output: output ?? process.stdout
|
|
3743
|
+
}).map((line) => `${dim(S_BAR)} ${line}`);
|
|
3744
|
+
const hintLine = "โ/โ navigate ยท space select ยท enter confirm ยท A select all ยท N select none";
|
|
3745
|
+
return [
|
|
3746
|
+
header,
|
|
3747
|
+
...lines,
|
|
3748
|
+
`${dim(S_BAR_END)} ${dim(hintLine)}`
|
|
3749
|
+
].join("\n");
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Multi-select prompt with extended hotkeys for file selection.
|
|
3756
|
+
*
|
|
3757
|
+
* Built-in (from `@clack/core`'s `MultiSelectPrompt`):
|
|
3758
|
+
* `a` โ toggle all (select all / deselect all)
|
|
3759
|
+
* `i` โ invert selection
|
|
3760
|
+
*
|
|
3761
|
+
* Extended hotkeys:
|
|
3762
|
+
* `A` (shift+A) โ select all unconditionally
|
|
3763
|
+
* `N` โ select none (deselect all)
|
|
3764
|
+
*/
|
|
3765
|
+
async function fileMultiSelect(message, options, opts) {
|
|
3766
|
+
const required = opts?.required ?? true;
|
|
3767
|
+
const prompt = new nt({
|
|
3768
|
+
options,
|
|
3769
|
+
required,
|
|
3770
|
+
input: opts?.input,
|
|
3771
|
+
output: opts?.output,
|
|
3772
|
+
validate: (values) => {
|
|
3773
|
+
if (required && (!values || values.length === 0)) return "Please select at least one option.";
|
|
3774
|
+
},
|
|
3775
|
+
render: buildMultiSelectRender(message, options, opts?.output)
|
|
3776
|
+
});
|
|
3777
|
+
prompt.on("key", (char, key) => {
|
|
3778
|
+
if (!char) return;
|
|
3779
|
+
if (char === "a" && key?.shift) prompt.value = options.map((o) => o.value);
|
|
3780
|
+
if (char === "n") prompt.value = [];
|
|
3781
|
+
});
|
|
3782
|
+
return await prompt.prompt();
|
|
3783
|
+
}
|
|
3784
|
+
//#endregion
|
|
3601
3785
|
//#region src/ui/toggle-select.ts
|
|
3602
3786
|
const ON_LABEL = styleText("green", "ON");
|
|
3603
3787
|
const OFF_LABEL = styleText("dim", "OFF");
|
|
@@ -3665,8 +3849,8 @@ function buildPromptRenderer(opts, state) {
|
|
|
3665
3849
|
const toggleList = opts.toggles;
|
|
3666
3850
|
return function() {
|
|
3667
3851
|
const sym = symbol(this.state);
|
|
3668
|
-
const statusLines = toggleList.map((t) => renderToggleState(t, state[t.hotkey])).join("\n");
|
|
3669
|
-
const header = `${sym} ${opts.message}\n${
|
|
3852
|
+
const statusLines = toggleList.map((t) => `${dim(S_BAR)} ${renderToggleState(t, state[t.hotkey])}`).join("\n");
|
|
3853
|
+
const header = `${sym} ${opts.message}\n${statusLines}`;
|
|
3670
3854
|
switch (this.state) {
|
|
3671
3855
|
case "submit": {
|
|
3672
3856
|
const selected = optionList[this.cursor];
|
|
@@ -3795,14 +3979,10 @@ async function showStagingMenu(files, hasChecks) {
|
|
|
3795
3979
|
files: files.map((f) => f.path),
|
|
3796
3980
|
all: true
|
|
3797
3981
|
};
|
|
3798
|
-
const selected = await
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
value: f.path
|
|
3803
|
-
})),
|
|
3804
|
-
required: true
|
|
3805
|
-
});
|
|
3982
|
+
const selected = await fileMultiSelect("Select files to stage:", sorted.map((f) => ({
|
|
3983
|
+
label: `${statusLabel(f.status)} ${f.path}`,
|
|
3984
|
+
value: f.path
|
|
3985
|
+
})), { required: true });
|
|
3806
3986
|
if (p$1.isCancel(selected)) return null;
|
|
3807
3987
|
return {
|
|
3808
3988
|
files: selected,
|
|
@@ -3835,10 +4015,9 @@ async function handleStaging(changedFiles, flags) {
|
|
|
3835
4015
|
await stageAll();
|
|
3836
4016
|
const allFiles = await getStagedFiles();
|
|
3837
4017
|
if (await detectConfig(repoRoot)) {
|
|
3838
|
-
const
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
stopCheckSpinner(ckSpinner, ckResult);
|
|
4018
|
+
const display = createCheckProgressDisplay();
|
|
4019
|
+
const ckResult = await runAllChecks(repoRoot, allFiles, 6e4, display);
|
|
4020
|
+
display.finish(ckResult.ok);
|
|
3842
4021
|
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}`);
|
|
3843
4022
|
}
|
|
3844
4023
|
currentFiles = await getChangedFiles();
|