@osdlc/repo-cleaner 0.11.0 → 0.13.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/CHANGELOG.md +29 -0
- package/README.md +93 -13
- package/dist/apply.d.ts +30 -1
- package/dist/apply.js +118 -17
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +125 -24
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plan.d.ts +3 -0
- package/dist/plan.js +14 -1
- package/dist/receipt.d.ts +4 -0
- package/dist/receipt.js +2 -0
- package/dist/render.d.ts +47 -4
- package/dist/render.js +132 -19
- package/dist/status-line.d.ts +4 -2
- package/dist/status-line.js +11 -4
- package/dist/step-board.d.ts +34 -0
- package/dist/step-board.js +101 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worktrees.d.ts +12 -0
- package/dist/worktrees.js +57 -0
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -26,14 +26,16 @@ import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
|
26
26
|
import { isatty } from 'node:tty';
|
|
27
27
|
import { EXIT, askYesNo, canPrompt, defineCli, systemDirs } from '@osdlc/cli-tool';
|
|
28
28
|
import { Git } from '@osdlc/typed-git';
|
|
29
|
+
import { detectColorLevel } from '@osdlc/ux';
|
|
29
30
|
import { resolveBranch } from "./branches.js";
|
|
30
31
|
import { VENDOR_DIRS } from "./devlink.js";
|
|
31
32
|
import { ensureMirror, mirrorProblem } from "./remote-mirror.js";
|
|
32
33
|
import { ApplyArgumentError, applyPlan, parseGrant, parseRowRef } from "./apply.js";
|
|
33
34
|
import { PLAN_VERSION, fileStamp, parsePlan, planRepo, readPlan, writePlan, } from "./plan.js";
|
|
34
35
|
import { RECEIPT_VERSION } from "./receipt.js";
|
|
35
|
-
import { proposedCommands, renderCountsJson, renderCountsText, renderJson, renderReceipt, renderSummary, renderText, rowsActedOn, summarizeReceipt, } from "./render.js";
|
|
36
|
+
import { PLAIN, proposedCommands, renderCountsJson, renderCountsText, renderJson, renderPreview, renderPreviewDrops, renderReceipt, renderStepFailure, renderSummary, renderText, rowsActedOn, summarizeReceipt, TERMINAL_STYLE, } from "./render.js";
|
|
36
37
|
import { NO_STATUS_LINE, describeStep, describeSweep, statusLine, } from "./status-line.js";
|
|
38
|
+
import { stepBoard } from "./step-board.js";
|
|
37
39
|
import { sweepRepos } from "./sweep.js";
|
|
38
40
|
const EXIT_NOTHING_TO_SWEEP = 10;
|
|
39
41
|
const EXIT_STEP_FAILED = 11;
|
|
@@ -49,12 +51,16 @@ const SWEEP_FLAGS = {
|
|
|
49
51
|
only: {
|
|
50
52
|
kind: 'list',
|
|
51
53
|
valueName: 'list',
|
|
52
|
-
help: `comma-separated: ${Object.keys(ONLY).join(', ')} (default:
|
|
54
|
+
help: `comma-separated: ${Object.keys(ONLY).join(', ')} (default: worktrees, branches; remotes with --remotes)`,
|
|
55
|
+
},
|
|
56
|
+
remotes: {
|
|
57
|
+
kind: 'boolean',
|
|
58
|
+
help: 'also sweep remote branches, which is slower: each one is compared with the default branch',
|
|
53
59
|
},
|
|
54
60
|
branch: {
|
|
55
61
|
kind: 'string',
|
|
56
62
|
valueName: 'name|HEAD',
|
|
57
|
-
help: 'only this branch of this repository: its worktree
|
|
63
|
+
help: 'only this branch of this repository: its worktree and local branch, and its remote branch with --remotes',
|
|
58
64
|
},
|
|
59
65
|
remote: {
|
|
60
66
|
kind: 'string',
|
|
@@ -68,11 +74,15 @@ const SWEEP_FLAGS = {
|
|
|
68
74
|
commaSeparated: false,
|
|
69
75
|
help: 'sweep the branches of this remote with no checkout of your own, through a clone kept in the cache directory; repeatable',
|
|
70
76
|
},
|
|
71
|
-
gh: {
|
|
77
|
+
gh: {
|
|
78
|
+
kind: 'boolean',
|
|
79
|
+
negatable: true,
|
|
80
|
+
help: 'with --remotes: classify on git evidence alone (without --remotes gh is never called)',
|
|
81
|
+
},
|
|
72
82
|
fetch: {
|
|
73
83
|
kind: 'boolean',
|
|
74
84
|
negatable: true,
|
|
75
|
-
help: 'classify on the remote-tracking refs already on disk',
|
|
85
|
+
help: 'with --remotes: classify on the remote-tracking refs already on disk (without --remotes nothing is fetched)',
|
|
76
86
|
},
|
|
77
87
|
progress: {
|
|
78
88
|
kind: 'boolean',
|
|
@@ -96,8 +106,9 @@ const OUTPUT = {
|
|
|
96
106
|
};
|
|
97
107
|
const statusCli = () => defineCli({
|
|
98
108
|
name: NAME,
|
|
99
|
-
summary: 'Inventory branches
|
|
100
|
-
'the default branch.
|
|
109
|
+
summary: 'Inventory local branches and worktrees (remote branches too with --remotes), and say ' +
|
|
110
|
+
'which have been merged into the default branch. Offline unless --remotes, which fetches ' +
|
|
111
|
+
'each remote first and asks gh about pull requests; deletes nothing.',
|
|
101
112
|
positionals: PATHS,
|
|
102
113
|
flags: {
|
|
103
114
|
...SWEEP_FLAGS,
|
|
@@ -117,7 +128,7 @@ swept as ./<name>.`,
|
|
|
117
128
|
});
|
|
118
129
|
const summaryCli = () => defineCli({
|
|
119
130
|
name: `${NAME} summary`,
|
|
120
|
-
summary: 'Count local branches
|
|
131
|
+
summary: 'Count local branches and worktrees (and remote branches with --remotes) per repository: how many there are, ' +
|
|
121
132
|
'how many are merged into the default branch, and how many would be deleted, pruned, reviewed or kept.',
|
|
122
133
|
positionals: PATHS,
|
|
123
134
|
flags: { ...SWEEP_FLAGS, output: OUTPUT },
|
|
@@ -160,6 +171,13 @@ const applyCli = () => defineCli({
|
|
|
160
171
|
commaSeparated: false,
|
|
161
172
|
help: 'apply only this row, e.g. local_branch:feat/x; repeatable (default: every row)',
|
|
162
173
|
},
|
|
174
|
+
keep: {
|
|
175
|
+
kind: 'list',
|
|
176
|
+
valueName: 'remote_branch:id',
|
|
177
|
+
default: [],
|
|
178
|
+
commaSeparated: false,
|
|
179
|
+
help: 'keep this remote branch, but refuse the run if its tip moved since the plan, e.g. remote_branch:origin/feat/x; repeatable',
|
|
180
|
+
},
|
|
163
181
|
allow: {
|
|
164
182
|
kind: 'list',
|
|
165
183
|
valueName: 'risk[=category:id]',
|
|
@@ -218,10 +236,31 @@ function failure(ctx, err) {
|
|
|
218
236
|
ctx.io.stderr(`${NAME}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
219
237
|
return EXIT.error;
|
|
220
238
|
}
|
|
239
|
+
/** Color for a report on `stream`, when it is a terminal that takes color. */
|
|
240
|
+
function styleFor(ctx, stream) {
|
|
241
|
+
const level = detectColorLevel({ env: ctx.env, isTTY: ctx.terminal?.[stream] === true });
|
|
242
|
+
return level === 'none' ? PLAIN : TERMINAL_STYLE;
|
|
243
|
+
}
|
|
244
|
+
/** The note under a text report that left remote branches out, or '' when it did not. */
|
|
245
|
+
function remotesNote(swept, style) {
|
|
246
|
+
if (swept.kind !== 'ok' || swept.scope.includes('remote-branches'))
|
|
247
|
+
return '';
|
|
248
|
+
if (!swept.result.repos.some((r) => r.repo?.remote != null))
|
|
249
|
+
return '';
|
|
250
|
+
return (style.muted('Local only: nothing fetched, no gh, remote branches not checked; add --remotes for all three.') + '\n');
|
|
251
|
+
}
|
|
221
252
|
/** Where `--remote-url` keeps its clones. */
|
|
222
253
|
function mirrorCache(ctx) {
|
|
223
254
|
return systemDirs(VENDOR_DIRS, { env: ctx.env }).standalone(NAME).cache().app('remotes').path;
|
|
224
255
|
}
|
|
256
|
+
/** Local work by default: remote branches only when asked for, with `--remotes` or `--only remotes`. */
|
|
257
|
+
const LOCAL_SCOPE = ['worktrees', 'local-branches'];
|
|
258
|
+
function sweepScope(only, remotes) {
|
|
259
|
+
const scope = only === undefined ? [...LOCAL_SCOPE] : only.map((s) => ONLY[s]);
|
|
260
|
+
if (remotes && !scope.includes('remote-branches'))
|
|
261
|
+
scope.push('remote-branches');
|
|
262
|
+
return scope;
|
|
263
|
+
}
|
|
225
264
|
/** Validate the sweep flags and run the sweep, or answer with the exit code. */
|
|
226
265
|
function sweep(name, v, paths, ctx, status) {
|
|
227
266
|
const only = v.only?.filter((s) => s !== '');
|
|
@@ -261,17 +300,20 @@ function sweep(name, v, paths, ctx, status) {
|
|
|
261
300
|
catch (err) {
|
|
262
301
|
return { kind: 'exit', code: failure(ctx, err) };
|
|
263
302
|
}
|
|
303
|
+
const scope = urls.length > 0 ? ['remote-branches'] : sweepScope(only, v.remotes);
|
|
304
|
+
const online = scope.includes('remote-branches');
|
|
264
305
|
const options = {
|
|
265
306
|
baseDir: ctx.cwd,
|
|
266
307
|
cwd: ctx.cwd,
|
|
267
|
-
|
|
268
|
-
|
|
308
|
+
// Local by default means offline: the fetch and gh go with remote branches.
|
|
309
|
+
noGh: !(online && v.gh),
|
|
310
|
+
fetch: online && v.fetch,
|
|
269
311
|
branch,
|
|
270
312
|
// Mid-walk, while the wait is happening — the summary comes too late to
|
|
271
313
|
// explain why the tool appeared to hang.
|
|
272
314
|
onWarn: (message) => ctx.io.stderr(`${NAME}: ${message}\n`),
|
|
273
315
|
...(v.progress ? { onProgress: (p) => status.update(describeSweep(p)) } : {}),
|
|
274
|
-
|
|
316
|
+
scope,
|
|
275
317
|
...(v.remote === undefined ? {} : { remote: v.remote }),
|
|
276
318
|
};
|
|
277
319
|
let entries = paths.length > 0 ? [...paths] : [ctx.cwd];
|
|
@@ -288,7 +330,6 @@ function sweep(name, v, paths, ctx, status) {
|
|
|
288
330
|
status.clear();
|
|
289
331
|
return { kind: 'exit', code: failure(ctx, err) };
|
|
290
332
|
}
|
|
291
|
-
options.scope = ['remote-branches'];
|
|
292
333
|
}
|
|
293
334
|
if (v.progress)
|
|
294
335
|
status.update(`${NAME}: finding repositories`);
|
|
@@ -303,7 +344,7 @@ function sweep(name, v, paths, ctx, status) {
|
|
|
303
344
|
return { kind: 'exit', code: failure(ctx, new Error(refused)) };
|
|
304
345
|
if (result.repos.length === 0)
|
|
305
346
|
return { kind: 'nothing', result };
|
|
306
|
-
return { kind: 'ok', result, branch, values: v };
|
|
347
|
+
return { kind: 'ok', result, branch, values: v, scope, noGh: options.noGh === true };
|
|
307
348
|
}
|
|
308
349
|
/**
|
|
309
350
|
* Name each repository the sweep could not read, on stderr. True when there
|
|
@@ -319,13 +360,13 @@ function reportUnreadable(ctx, name, result) {
|
|
|
319
360
|
function planOf(swept, now) {
|
|
320
361
|
const opts = {
|
|
321
362
|
remote: swept.values.remote ?? null,
|
|
322
|
-
noGh:
|
|
363
|
+
noGh: swept.noGh,
|
|
323
364
|
branch: swept.branch ?? null,
|
|
324
365
|
};
|
|
325
366
|
return {
|
|
326
367
|
version: PLAN_VERSION,
|
|
327
368
|
created_at: now.toISOString(),
|
|
328
|
-
repos: swept.result.repos.flatMap((s) => s.repo === null ? [] : [planRepo(s.repo.root, opts, s.items)]),
|
|
369
|
+
repos: swept.result.repos.flatMap((s) => s.repo === null ? [] : [planRepo(s.repo.root, { ...opts, fetch: s.repo.fetched }, s.items)]),
|
|
329
370
|
};
|
|
330
371
|
}
|
|
331
372
|
function runStatus(argv, ctx, status) {
|
|
@@ -335,7 +376,8 @@ function runStatus(argv, ctx, status) {
|
|
|
335
376
|
if (parsed.status === 'error')
|
|
336
377
|
return usageError(ctx, NAME, parsed.message);
|
|
337
378
|
const v = parsed.values;
|
|
338
|
-
const
|
|
379
|
+
const style = styleFor(ctx, 'stdout');
|
|
380
|
+
const render = (r) => v.output === 'json' ? renderJson(r) : renderText(r, { keep: v.keep, style });
|
|
339
381
|
const swept = sweep(NAME, v, parsed.positionals, ctx, status);
|
|
340
382
|
if (swept.kind === 'exit')
|
|
341
383
|
return swept.code;
|
|
@@ -350,6 +392,8 @@ function runStatus(argv, ctx, status) {
|
|
|
350
392
|
}
|
|
351
393
|
else {
|
|
352
394
|
ctx.io.stdout(render(swept.result));
|
|
395
|
+
if (v.output === 'text')
|
|
396
|
+
ctx.io.stdout(remotesNote(swept, style));
|
|
353
397
|
}
|
|
354
398
|
return EXIT.ok;
|
|
355
399
|
}
|
|
@@ -361,11 +405,14 @@ function runSummary(argv, ctx, status) {
|
|
|
361
405
|
if (parsed.status === 'error')
|
|
362
406
|
return usageError(ctx, name, parsed.message);
|
|
363
407
|
const v = parsed.values;
|
|
364
|
-
const
|
|
408
|
+
const style = styleFor(ctx, 'stdout');
|
|
409
|
+
const render = (r) => v.output === 'json' ? renderCountsJson(r) : renderCountsText(r, style);
|
|
365
410
|
const swept = sweep(name, v, parsed.positionals, ctx, status);
|
|
366
411
|
if (swept.kind === 'exit')
|
|
367
412
|
return swept.code;
|
|
368
413
|
ctx.io.stdout(render(swept.result));
|
|
414
|
+
if (v.output === 'text')
|
|
415
|
+
ctx.io.stdout(remotesNote(swept, style));
|
|
369
416
|
return swept.kind === 'nothing' ? EXIT_NOTHING_TO_SWEEP : EXIT.ok;
|
|
370
417
|
}
|
|
371
418
|
function runPlan(argv, ctx, status) {
|
|
@@ -386,7 +433,8 @@ function runPlan(argv, ctx, status) {
|
|
|
386
433
|
}
|
|
387
434
|
const plan = planOf(swept, new Date());
|
|
388
435
|
const unreadable = reportUnreadable(ctx, name, swept.result);
|
|
389
|
-
ctx
|
|
436
|
+
const style = styleFor(ctx, 'stderr');
|
|
437
|
+
ctx.io.stderr(renderSummary(swept.result, style) + '\n' + remotesNote(swept, style));
|
|
390
438
|
if (v.out === '-') {
|
|
391
439
|
ctx.io.stdout(`${JSON.stringify(plan, null, 2)}\n`);
|
|
392
440
|
}
|
|
@@ -402,6 +450,7 @@ function receiptBeside(planPath, now) {
|
|
|
402
450
|
const stem = basename(planPath).replace(/\.json$/i, '');
|
|
403
451
|
return join(dirname(planPath), `${stem}.receipt-${fileStamp(now)}.json`);
|
|
404
452
|
}
|
|
453
|
+
const dropKey = (d) => JSON.stringify([d.repo, d.category, d.id]);
|
|
405
454
|
/** Rows the run would delete or prune: one each, however many steps (an unlock, a remove) it takes. */
|
|
406
455
|
function wouldRun(receipt) {
|
|
407
456
|
return rowsActedOn(summarizeReceipt(receipt));
|
|
@@ -476,9 +525,11 @@ function runApply(argv, ctx, deps, status) {
|
|
|
476
525
|
const v = parsed.values;
|
|
477
526
|
const paths = parsed.positionals;
|
|
478
527
|
let rows;
|
|
528
|
+
let keep;
|
|
479
529
|
let grants;
|
|
480
530
|
try {
|
|
481
531
|
rows = v.row.map(parseRowRef);
|
|
532
|
+
keep = v.keep.map(parseRowRef);
|
|
482
533
|
grants = v.allow.map(parseGrant);
|
|
483
534
|
}
|
|
484
535
|
catch (err) {
|
|
@@ -507,10 +558,14 @@ function runApply(argv, ctx, deps, status) {
|
|
|
507
558
|
}
|
|
508
559
|
const ok = () => (loaded.unreadable ? EXIT.error : EXIT.ok);
|
|
509
560
|
const repoRoot = v.repoRoot === undefined ? undefined : resolve(ctx.cwd, v.repoRoot);
|
|
510
|
-
|
|
561
|
+
// On a terminal the run is shown live: every step first, then each one
|
|
562
|
+
// checked off as it finishes, with a bar on the status line.
|
|
563
|
+
const live = v.output === 'text' && ctx.terminal?.stdout === true;
|
|
564
|
+
const style = styleFor(ctx, 'stdout');
|
|
565
|
+
const print = (receipt, shownPath, steps = true) => {
|
|
511
566
|
ctx.io.stdout(v.output === 'json'
|
|
512
567
|
? `${JSON.stringify({ receiptPath: shownPath, summary: summarizeReceipt(receipt), receipt }, null, 2)}\n`
|
|
513
|
-
: renderReceipt(receipt, shownPath));
|
|
568
|
+
: renderReceipt(receipt, shownPath, style, { steps }));
|
|
514
569
|
};
|
|
515
570
|
const planned = plan.repos.filter((r) => repoRoot === undefined || resolve(r.root) === resolve(repoRoot));
|
|
516
571
|
if (repoRoot !== undefined && planned.length === 0) {
|
|
@@ -527,6 +582,7 @@ function runApply(argv, ctx, deps, status) {
|
|
|
527
582
|
callerCwd: ctx.cwd,
|
|
528
583
|
planPath: source,
|
|
529
584
|
repoRoot,
|
|
585
|
+
keep,
|
|
530
586
|
// An implicit selection runs only safe-to-delete rows, in the preview and
|
|
531
587
|
// in the run it confirms, whose rows are passed explicitly.
|
|
532
588
|
safeOnly: rows.length === 0,
|
|
@@ -548,11 +604,16 @@ function runApply(argv, ctx, deps, status) {
|
|
|
548
604
|
});
|
|
549
605
|
}
|
|
550
606
|
catch (err) {
|
|
607
|
+
if (err instanceof ApplyArgumentError)
|
|
608
|
+
return usageError(ctx, name, err.message);
|
|
551
609
|
return failure(ctx, err);
|
|
552
610
|
}
|
|
553
611
|
const count = wouldRun(preview);
|
|
554
612
|
if (v.dryRun || count === 0) {
|
|
555
|
-
|
|
613
|
+
if (live)
|
|
614
|
+
ctx.io.stdout(renderPreview(preview, style));
|
|
615
|
+
else
|
|
616
|
+
print(preview, null);
|
|
556
617
|
return ok();
|
|
557
618
|
}
|
|
558
619
|
if (!v.yes) {
|
|
@@ -560,10 +621,22 @@ function runApply(argv, ctx, deps, status) {
|
|
|
560
621
|
if (declined !== null)
|
|
561
622
|
return declined;
|
|
562
623
|
}
|
|
624
|
+
else if (live) {
|
|
625
|
+
ctx.io.stdout(renderPreview(preview, style));
|
|
626
|
+
}
|
|
627
|
+
if (live)
|
|
628
|
+
ctx.io.stdout('\n');
|
|
563
629
|
// Only what the preview showed: rows it dropped stay dropped.
|
|
564
630
|
// Pinned to their repositories: a branch name two repositories share stays two rows.
|
|
565
|
-
|
|
631
|
+
// A kept row's tip check is not a selection; `keep` carries it again.
|
|
632
|
+
const approved = preview.steps.flatMap((s) => s.row === null || s.action === 'verify-remote' ? [] : [{ ...s.row, repo: s.repo }]);
|
|
566
633
|
const approvedGrants = grants.filter((g) => g.row === null || approved.some((r) => r.category === g.row.category && r.id === g.row.id));
|
|
634
|
+
const size = deps.terminalSize ??
|
|
635
|
+
(() => ({
|
|
636
|
+
width: ctx.terminal?.columns || process.stdout.columns || 80,
|
|
637
|
+
height: process.stdout.rows || 24,
|
|
638
|
+
}));
|
|
639
|
+
let board;
|
|
567
640
|
let receipt;
|
|
568
641
|
try {
|
|
569
642
|
receipt = applyPlan(plan, approved, approvedGrants, {
|
|
@@ -571,12 +644,40 @@ function runApply(argv, ctx, deps, status) {
|
|
|
571
644
|
dryRun: false,
|
|
572
645
|
receiptPath,
|
|
573
646
|
dropped: preview.dropped,
|
|
647
|
+
...(live
|
|
648
|
+
? {
|
|
649
|
+
// The board carries the run's progress; the status line stays clear.
|
|
650
|
+
onStart: (steps) => {
|
|
651
|
+
board = stepBoard(ctx.io.stdout, steps, size, style);
|
|
652
|
+
board.draw(null);
|
|
653
|
+
},
|
|
654
|
+
onStep: (i) => board?.draw(i),
|
|
655
|
+
onStepDone: (_i, _total, step) => {
|
|
656
|
+
board?.draw(null);
|
|
657
|
+
// Here rather than after the run: a receipt that then cannot be
|
|
658
|
+
// saved must not take the step's own error with it.
|
|
659
|
+
if (step.result === 'failed') {
|
|
660
|
+
ctx.io.stdout(`\n${renderStepFailure(step, style)}\n`);
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
}
|
|
664
|
+
: {}),
|
|
574
665
|
});
|
|
575
666
|
}
|
|
576
667
|
catch (err) {
|
|
577
668
|
return failure(ctx, err);
|
|
578
669
|
}
|
|
579
|
-
|
|
670
|
+
status.clear();
|
|
671
|
+
if (live) {
|
|
672
|
+
// The run's own re-check can drop a row the preview showed, if it moved
|
|
673
|
+
// while the question was open: say which, since it gets no tick.
|
|
674
|
+
const seen = new Set(preview.dropped.map(dropKey));
|
|
675
|
+
const late = receipt.dropped.filter((d) => !seen.has(dropKey(d)));
|
|
676
|
+
if (late.length > 0)
|
|
677
|
+
ctx.io.stdout(`\n${renderPreviewDrops(late, style)}`);
|
|
678
|
+
ctx.io.stdout('\n');
|
|
679
|
+
}
|
|
680
|
+
print(receipt, receiptPath, !live);
|
|
580
681
|
if (receiptPath === null) {
|
|
581
682
|
ctx.io.stderr(`${name}: no receipt written; pass --receipt <file> to keep one\n`);
|
|
582
683
|
}
|
|
@@ -589,7 +690,7 @@ function runApply(argv, ctx, deps, status) {
|
|
|
589
690
|
function confirm(ctx, name, preview, count, json, deps) {
|
|
590
691
|
// The preview is for the person answering, so it goes where the question does.
|
|
591
692
|
const show = json ? ctx.io.stderr : ctx.io.stdout;
|
|
592
|
-
show(
|
|
693
|
+
show(renderPreview(preview, styleFor(ctx, json ? 'stderr' : 'stdout')));
|
|
593
694
|
const answer = (deps.confirm ?? terminalConfirm)(`Delete ${count} item${count === 1 ? '' : 's'}?`);
|
|
594
695
|
if (answer === null) {
|
|
595
696
|
return usageError(ctx, name, 'refusing to delete without a terminal to ask; pass --yes');
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { classifyLocalBranch, classifyRemoteBranch, gatherLocalBranches, gatherR
|
|
|
18
18
|
export { classifyWorktree, gatherWorktrees, hasBlockers, worktreePruneCommand, worktreeRemoveCommand, worktreeUnlockCommand, type WorktreeBlocker, } from './worktrees.ts';
|
|
19
19
|
export { DiscoveryFailed, discoverRepo } from './discover.ts';
|
|
20
20
|
export { assessLanding, patchEquivalent, type Landing, type LandingCheck } from './landing.ts';
|
|
21
|
-
export { ApplyArgumentError, applyPlan, type RowSelection, parseGrant, parseRowRef, type ApplyOptions, type Grant, } from './apply.ts';
|
|
21
|
+
export { ApplyArgumentError, ApplyRefused, applyPlan, type RowSelection, parseGrant, parseRowRef, type ApplyOptions, type Grant, } from './apply.ts';
|
|
22
22
|
export type { Annotate, HostHooks } from './hooks.ts';
|
|
23
23
|
export { PLAN_VERSION, PlanReadError, planRepo, parsePlan, readPlan, writePlan, type Plan, type PlanRepo, type PlannableRow, } from './plan.ts';
|
|
24
24
|
export { RECEIPT_VERSION, ReceiptSchema, type Dropped, type Receipt, type RowRef, type Step, } from './receipt.ts';
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export { classifyLocalBranch, classifyRemoteBranch, gatherLocalBranches, gatherR
|
|
|
18
18
|
export { classifyWorktree, gatherWorktrees, hasBlockers, worktreePruneCommand, worktreeRemoveCommand, worktreeUnlockCommand, } from "./worktrees.js";
|
|
19
19
|
export { DiscoveryFailed, discoverRepo } from "./discover.js";
|
|
20
20
|
export { assessLanding, patchEquivalent } from "./landing.js";
|
|
21
|
-
export { ApplyArgumentError, applyPlan, parseGrant, parseRowRef, } from "./apply.js";
|
|
21
|
+
export { ApplyArgumentError, ApplyRefused, applyPlan, parseGrant, parseRowRef, } from "./apply.js";
|
|
22
22
|
export { PLAN_VERSION, PlanReadError, planRepo, parsePlan, readPlan, writePlan, } from "./plan.js";
|
|
23
23
|
export { RECEIPT_VERSION, ReceiptSchema, } from "./receipt.js";
|
|
24
24
|
export { defaultProcessProbe, isIdleShell, parseLockOwner, parseLsofCwd, } from "./processes.js";
|
package/dist/plan.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export declare const PlanRepoSchema: z.ZodObject<{
|
|
|
29
29
|
remote: z.ZodNullable<z.ZodString>;
|
|
30
30
|
no_gh: z.ZodBoolean;
|
|
31
31
|
branch: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
32
|
+
fetch: z.ZodDefault<z.ZodBoolean>;
|
|
32
33
|
rows: z.ZodArray<z.ZodObject<{
|
|
33
34
|
category: z.ZodEnum<{
|
|
34
35
|
local_branch: "local_branch";
|
|
@@ -53,6 +54,7 @@ export declare const PlanSchema: z.ZodObject<{
|
|
|
53
54
|
remote: z.ZodNullable<z.ZodString>;
|
|
54
55
|
no_gh: z.ZodBoolean;
|
|
55
56
|
branch: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
57
|
+
fetch: z.ZodDefault<z.ZodBoolean>;
|
|
56
58
|
rows: z.ZodArray<z.ZodObject<{
|
|
57
59
|
category: z.ZodEnum<{
|
|
58
60
|
local_branch: "local_branch";
|
|
@@ -89,6 +91,7 @@ export declare function planRepo(root: string, opts: {
|
|
|
89
91
|
remote: string | null;
|
|
90
92
|
noGh: boolean;
|
|
91
93
|
branch?: string | null;
|
|
94
|
+
fetch?: boolean;
|
|
92
95
|
}, items: readonly PlannableRow[]): PlanRepo;
|
|
93
96
|
/** A UTC timestamp safe in a file name, e.g. `2026-10-04T12-30-05-123Z`. */
|
|
94
97
|
export declare function fileStamp(d: Date): string;
|
package/dist/plan.js
CHANGED
|
@@ -23,6 +23,12 @@ export const PlanRepoSchema = z.object({
|
|
|
23
23
|
no_gh: z.boolean(),
|
|
24
24
|
/** The `branch` the sweep was narrowed to; the re-sweep uses the same. */
|
|
25
25
|
branch: z.string().nullable().default(null),
|
|
26
|
+
/**
|
|
27
|
+
* Whether the sweep fetched before classifying. The re-sweep fetches too, so
|
|
28
|
+
* it judges against the remote's current default branch, as the sweep did.
|
|
29
|
+
* A plan written before this field reads as not fetched.
|
|
30
|
+
*/
|
|
31
|
+
fetch: z.boolean().default(false),
|
|
26
32
|
rows: z.array(PlanRowSchema),
|
|
27
33
|
});
|
|
28
34
|
export const PlanSchema = z.object({
|
|
@@ -78,7 +84,14 @@ export function planRepo(root, opts, items) {
|
|
|
78
84
|
fingerprint: fingerprint(item),
|
|
79
85
|
});
|
|
80
86
|
}
|
|
81
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
root,
|
|
89
|
+
remote: opts.remote,
|
|
90
|
+
no_gh: opts.noGh,
|
|
91
|
+
branch: opts.branch ?? null,
|
|
92
|
+
fetch: opts.fetch ?? false,
|
|
93
|
+
rows,
|
|
94
|
+
};
|
|
82
95
|
}
|
|
83
96
|
/** A UTC timestamp safe in a file name, e.g. `2026-10-04T12-30-05-123Z`. */
|
|
84
97
|
export function fileStamp(d) {
|
package/dist/receipt.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare const DroppedSchema: z.ZodObject<{
|
|
|
26
26
|
"facts-changed": "facts-changed";
|
|
27
27
|
"not-authorized": "not-authorized";
|
|
28
28
|
duplicate: "duplicate";
|
|
29
|
+
kept: "kept";
|
|
29
30
|
}>;
|
|
30
31
|
detail: z.ZodString;
|
|
31
32
|
}, z.core.$strip>;
|
|
@@ -41,6 +42,7 @@ export declare const StepSchema: z.ZodObject<{
|
|
|
41
42
|
id: z.ZodString;
|
|
42
43
|
}, z.core.$strip>>;
|
|
43
44
|
action: z.ZodEnum<{
|
|
45
|
+
"verify-remote": "verify-remote";
|
|
44
46
|
"unlock-worktree": "unlock-worktree";
|
|
45
47
|
"remove-worktree": "remove-worktree";
|
|
46
48
|
"delete-local": "delete-local";
|
|
@@ -85,6 +87,7 @@ export declare const ReceiptSchema: z.ZodObject<{
|
|
|
85
87
|
"facts-changed": "facts-changed";
|
|
86
88
|
"not-authorized": "not-authorized";
|
|
87
89
|
duplicate: "duplicate";
|
|
90
|
+
kept: "kept";
|
|
88
91
|
}>;
|
|
89
92
|
detail: z.ZodString;
|
|
90
93
|
}, z.core.$strip>>;
|
|
@@ -99,6 +102,7 @@ export declare const ReceiptSchema: z.ZodObject<{
|
|
|
99
102
|
id: z.ZodString;
|
|
100
103
|
}, z.core.$strip>>;
|
|
101
104
|
action: z.ZodEnum<{
|
|
105
|
+
"verify-remote": "verify-remote";
|
|
102
106
|
"unlock-worktree": "unlock-worktree";
|
|
103
107
|
"remove-worktree": "remove-worktree";
|
|
104
108
|
"delete-local": "delete-local";
|
package/dist/receipt.js
CHANGED
|
@@ -13,6 +13,7 @@ export const DroppedSchema = RowRefSchema.extend({
|
|
|
13
13
|
'facts-changed',
|
|
14
14
|
'not-authorized',
|
|
15
15
|
'duplicate',
|
|
16
|
+
'kept',
|
|
16
17
|
]),
|
|
17
18
|
detail: z.string(),
|
|
18
19
|
});
|
|
@@ -21,6 +22,7 @@ export const StepSchema = z.object({
|
|
|
21
22
|
/** Null for the closing prune, which belongs to no row. */
|
|
22
23
|
row: RowRefSchema.nullable(),
|
|
23
24
|
action: z.enum([
|
|
25
|
+
'verify-remote',
|
|
24
26
|
'unlock-worktree',
|
|
25
27
|
'remove-worktree',
|
|
26
28
|
'delete-local',
|
package/dist/render.d.ts
CHANGED
|
@@ -6,8 +6,31 @@
|
|
|
6
6
|
* find. JSON carries everything the text report elides, including the facts
|
|
7
7
|
* behind every verdict.
|
|
8
8
|
*/
|
|
9
|
-
import type { Receipt } from './receipt.ts';
|
|
9
|
+
import type { Receipt, Step } from './receipt.ts';
|
|
10
10
|
import { type RepoSweep, type SweepItem, type SweepResult } from './types.ts';
|
|
11
|
+
/**
|
|
12
|
+
* How a report colors its words. {@link PLAIN} leaves them as they are; the
|
|
13
|
+
* CLI passes {@link TERMINAL_STYLE} when the stream is a terminal that takes
|
|
14
|
+
* color.
|
|
15
|
+
*/
|
|
16
|
+
export interface Style {
|
|
17
|
+
/** What a run deletes. */
|
|
18
|
+
delete(text: string): string;
|
|
19
|
+
/** A stale link a run prunes. */
|
|
20
|
+
prune(text: string): string;
|
|
21
|
+
/** What a person has to look at: a review row, a dropped row. */
|
|
22
|
+
review(text: string): string;
|
|
23
|
+
/** What stays, or a step that succeeded. */
|
|
24
|
+
keep(text: string): string;
|
|
25
|
+
/** Detail under a row: commands, the receipt path. */
|
|
26
|
+
muted(text: string): string;
|
|
27
|
+
/** A heading or the line a person is asked to approve. */
|
|
28
|
+
strong(text: string): string;
|
|
29
|
+
}
|
|
30
|
+
/** No color. */
|
|
31
|
+
export declare const PLAIN: Style;
|
|
32
|
+
/** The basic terminal colors: red to delete, blue to prune, yellow to review, green to keep. */
|
|
33
|
+
export declare const TERMINAL_STYLE: Style;
|
|
11
34
|
/**
|
|
12
35
|
* A row's authorizations for a report, e.g. `requires abandon-unmerged (drops 3
|
|
13
36
|
* commits), confirm-open-pr`. Empty when the row needs none.
|
|
@@ -16,6 +39,7 @@ export declare function requiresNote(item: Pick<SweepItem, 'requires' | 'facts'>
|
|
|
16
39
|
/** One repository's rows, indented under its header. */
|
|
17
40
|
export declare function renderRepo(sweep: RepoSweep, opts?: {
|
|
18
41
|
keep?: boolean;
|
|
42
|
+
style?: Style;
|
|
19
43
|
}): string;
|
|
20
44
|
/**
|
|
21
45
|
* What the search cost, or null when nothing was searched — every entry
|
|
@@ -27,19 +51,20 @@ export declare function renderRepo(sweep: RepoSweep, opts?: {
|
|
|
27
51
|
*/
|
|
28
52
|
export declare function renderScan(result: SweepResult): string | null;
|
|
29
53
|
/** The one-line verdict a person reads first, and often only. */
|
|
30
|
-
export declare function renderSummary(result: SweepResult): string;
|
|
54
|
+
export declare function renderSummary(result: SweepResult, style?: Style): string;
|
|
31
55
|
/**
|
|
32
56
|
* One repository as counts: a row per category with its total, how many are
|
|
33
57
|
* merged, and how many the sweep would delete, prune, review or keep.
|
|
34
58
|
*/
|
|
35
59
|
export declare function renderCounts(sweep: RepoSweep): string;
|
|
36
60
|
/** The whole run as counts: a table per repository, then the one-line summary. */
|
|
37
|
-
export declare function renderCountsText(result: SweepResult): string;
|
|
61
|
+
export declare function renderCountsText(result: SweepResult, style?: Style): string;
|
|
38
62
|
/** The whole run's counts as JSON: no rows, no facts. */
|
|
39
63
|
export declare function renderCountsJson(result: SweepResult): string;
|
|
40
64
|
/** The whole run as text: duplicate checkouts, a section per repository, then the summary. */
|
|
41
65
|
export declare function renderText(result: SweepResult, opts?: {
|
|
42
66
|
keep?: boolean;
|
|
67
|
+
style?: Style;
|
|
43
68
|
}): string;
|
|
44
69
|
/**
|
|
45
70
|
* The whole run as JSON, facts included. `extra` adds top-level keys, such as
|
|
@@ -71,5 +96,23 @@ export declare function summarizeReceipt(receipt: Receipt): ApplySummary;
|
|
|
71
96
|
export declare function rowsActedOn(summary: ApplySummary): number;
|
|
72
97
|
/** The closing lines of an apply run: what it deleted, dropped and could not finish. */
|
|
73
98
|
export declare function renderApplySummary(receipt: Receipt): string[];
|
|
99
|
+
/** The word a preview shows for a step: what it does to the row. */
|
|
100
|
+
export declare const STEP_VERB: Record<Step['action'], string>;
|
|
101
|
+
/**
|
|
102
|
+
* What `apply` is about to do, for the person asked to approve it: per
|
|
103
|
+
* repository, each row and the command that deletes it, the rows it will
|
|
104
|
+
* skip and why, then the counts. Terminal controls are stripped from what it
|
|
105
|
+
* shows, so a path or name cannot hide part of what is approved.
|
|
106
|
+
*/
|
|
107
|
+
export declare function renderPreview(receipt: Receipt, style?: Style): string;
|
|
108
|
+
/** Rows dropped by a live run's own re-check, after the preview: each with why. */
|
|
109
|
+
export declare function renderPreviewDrops(dropped: Receipt['dropped'], style?: Style): string;
|
|
110
|
+
/**
|
|
111
|
+
* A failed step for a run shown live: `✗`, the row, and the error in full,
|
|
112
|
+
* which the step's row on the board is too narrow to hold.
|
|
113
|
+
*/
|
|
114
|
+
export declare function renderStepFailure(step: Step, style?: Style): string;
|
|
74
115
|
/** An apply run as text: dropped rows, each step, a summary, then where the receipt is. */
|
|
75
|
-
export declare function renderReceipt(receipt: Receipt, receiptPath: string | null
|
|
116
|
+
export declare function renderReceipt(receipt: Receipt, receiptPath: string | null, style?: Style, opts?: {
|
|
117
|
+
steps?: boolean;
|
|
118
|
+
}): string;
|