@osdlc/repo-cleaner 0.11.0 → 0.14.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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Merge requests from GitLab, through `glab api`.
3
+ *
4
+ * The REST listing, not `glab mr list`: it carries the source and target
5
+ * project ids, which is how a fork's merge request is told apart, and it takes
6
+ * an explicit host and project, so a checkout with several remotes is read
7
+ * against the one the sweep measures.
8
+ */
9
+ import { type CommandRunner } from '@osdlc/command-seam';
10
+ import type { PRInfo } from '../types.ts';
11
+ import type { PrProvider } from './types.ts';
12
+ /** The fields of a merge request this reads. */
13
+ interface MergeRequest {
14
+ iid: number;
15
+ state: string;
16
+ source_branch: string;
17
+ target_branch: string;
18
+ sha?: string | null;
19
+ merged_at?: string | null;
20
+ web_url?: string;
21
+ title?: string;
22
+ source_project_id: number;
23
+ target_project_id: number;
24
+ }
25
+ /** One merge request as a {@link PRInfo}, or null for a fork's or an unknown state's. */
26
+ export declare function mergeRequestToPr(mr: MergeRequest): PRInfo | null;
27
+ /**
28
+ * A provider over `glab`. It needs `target.project`: without one there is
29
+ * nothing to ask, and the listing answers null.
30
+ */
31
+ export declare function gitlabProvider(glab: CommandRunner): PrProvider;
32
+ export {};
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Merge requests from GitLab, through `glab api`.
3
+ *
4
+ * The REST listing, not `glab mr list`: it carries the source and target
5
+ * project ids, which is how a fork's merge request is told apart, and it takes
6
+ * an explicit host and project, so a checkout with several remotes is read
7
+ * against the one the sweep measures.
8
+ */
9
+ import { parseJsonResult, runFailed, } from '@osdlc/command-seam';
10
+ import { PR_LIMIT } from "./github.js";
11
+ const PER_PAGE = 100;
12
+ /** GitLab's states, as the GitHub names classification reads. A locked MR is an open one mid-update. */
13
+ const STATE = {
14
+ opened: 'OPEN',
15
+ locked: 'OPEN',
16
+ merged: 'MERGED',
17
+ closed: 'CLOSED',
18
+ };
19
+ /** Absent, or of the given type (null counting where `nullable`). */
20
+ function optional(value, type, nullable = false) {
21
+ return value === undefined || (nullable && value === null) || typeof value === type;
22
+ }
23
+ function isMergeRequest(value) {
24
+ if (typeof value !== 'object' || value === null)
25
+ return false;
26
+ const mr = value;
27
+ return (optional(mr['sha'], 'string', true) &&
28
+ optional(mr['merged_at'], 'string', true) &&
29
+ optional(mr['web_url'], 'string') &&
30
+ optional(mr['title'], 'string') &&
31
+ typeof mr['iid'] === 'number' &&
32
+ typeof mr['state'] === 'string' &&
33
+ typeof mr['source_branch'] === 'string' &&
34
+ typeof mr['target_branch'] === 'string' &&
35
+ typeof mr['source_project_id'] === 'number' &&
36
+ typeof mr['target_project_id'] === 'number');
37
+ }
38
+ /** One merge request as a {@link PRInfo}, or null for a fork's or an unknown state's. */
39
+ export function mergeRequestToPr(mr) {
40
+ // A fork's source branch is not this repository's branch, whatever its name.
41
+ if (mr.source_project_id !== mr.target_project_id)
42
+ return null;
43
+ const state = STATE[mr.state];
44
+ if (state === undefined || mr.source_branch === '')
45
+ return null;
46
+ return {
47
+ number: mr.iid,
48
+ state,
49
+ mergedAt: mr.merged_at ?? null,
50
+ headRef: mr.source_branch,
51
+ headOid: typeof mr.sha === 'string' && mr.sha !== '' ? mr.sha : null,
52
+ baseRef: mr.target_branch,
53
+ title: mr.title ?? '',
54
+ url: mr.web_url ?? '',
55
+ };
56
+ }
57
+ /** Exited 0 with all its output captured. */
58
+ function succeeded(result) {
59
+ return !runFailed(result);
60
+ }
61
+ /**
62
+ * A provider over `glab`. It needs `target.project`: without one there is
63
+ * nothing to ask, and the listing answers null.
64
+ */
65
+ export function gitlabProvider(glab) {
66
+ const run = (root, argv) => glab.run(argv, { cwd: root });
67
+ const hostArgs = (host) => (host === null ? [] : ['--hostname', host]);
68
+ return {
69
+ name: 'gitlab',
70
+ words: { tool: 'glab', noun: 'merge request', listing: 'glab api' },
71
+ probe(root, target) {
72
+ const available = succeeded(run(root, ['--version']));
73
+ const authenticated = available && succeeded(run(root, ['auth', 'status', ...hostArgs(target.host)]));
74
+ return { available, authenticated };
75
+ },
76
+ listPrs(root, target, branch) {
77
+ if (target.project === null)
78
+ return null;
79
+ const project = encodeURIComponent(target.project);
80
+ const prs = [];
81
+ for (let page = 1; (page - 1) * PER_PAGE < PR_LIMIT; page++) {
82
+ const query = new URLSearchParams({
83
+ state: 'all',
84
+ per_page: String(PER_PAGE),
85
+ page: String(page),
86
+ order_by: 'updated_at',
87
+ sort: 'desc',
88
+ });
89
+ if (branch !== undefined)
90
+ query.set('source_branch', branch);
91
+ const result = run(root, [
92
+ 'api',
93
+ ...hostArgs(target.host),
94
+ `projects/${project}/merge_requests?${query.toString()}`,
95
+ ]);
96
+ // A failed, cut-short or unreadable page fails the listing: a missing
97
+ // open MR would read as no MR.
98
+ if (!succeeded(result))
99
+ return null;
100
+ const listed = parseJsonResult(result);
101
+ if (!listed.ok || !Array.isArray(listed.value))
102
+ return null;
103
+ const rows = listed.value;
104
+ for (const row of rows) {
105
+ if (!isMergeRequest(row))
106
+ continue;
107
+ const pr = mergeRequestToPr(row);
108
+ if (pr !== null)
109
+ prs.push(pr);
110
+ }
111
+ if (rows.length < PER_PAGE)
112
+ break;
113
+ }
114
+ return prs;
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Choosing a provider for a repository, from the caller's choice and the
3
+ * remote's host.
4
+ */
5
+ import { type CommandRunner } from '@osdlc/command-seam';
6
+ import type { Gh } from '@osdlc/typed-gh';
7
+ import type { ForgeTarget, PrProvider, ProviderChoice, ProviderName, ProviderStatus } from './types.ts';
8
+ export { githubProvider } from './github.ts';
9
+ export { gitlabProvider } from './gitlab.ts';
10
+ export * from './types.ts';
11
+ /** Every known provider, over the given clients. `glab` defaults to the real binary. */
12
+ export declare function providers(clients: {
13
+ gh: Gh;
14
+ glab?: CommandRunner | undefined;
15
+ }): Record<ProviderName, PrProvider>;
16
+ /**
17
+ * The host and full project path a remote URL names, groups kept
18
+ * (`gitlab.com:group/sub/name.git` is project `group/sub/name`). A local path
19
+ * or a `file:` URL names no forge.
20
+ */
21
+ export declare function forgeTarget(url: string | null): ForgeTarget;
22
+ /**
23
+ * The provider `choice` names for `target`. `auto` reads the host: one with
24
+ * `gitlab` in its name is GitLab, any other is GitHub, which is what the sweep
25
+ * did before providers existed (`gh` answers nothing for a host it does not
26
+ * know). `none` is no provider.
27
+ */
28
+ export declare function chooseProvider(choice: ProviderChoice, target: ForgeTarget): ProviderName | null;
29
+ /** The chosen provider for a repository, probed; null for none. */
30
+ export declare function probeProvider(choice: ProviderChoice, root: string, target: ForgeTarget, all: Record<ProviderName, PrProvider>): ProviderStatus | null;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Choosing a provider for a repository, from the caller's choice and the
3
+ * remote's host.
4
+ */
5
+ import { resolveRunner } from '@osdlc/command-seam';
6
+ import { githubProvider } from "./github.js";
7
+ import { gitlabProvider } from "./gitlab.js";
8
+ export { githubProvider } from "./github.js";
9
+ export { gitlabProvider } from "./gitlab.js";
10
+ export * from "./types.js";
11
+ /** Every known provider, over the given clients. `glab` defaults to the real binary. */
12
+ export function providers(clients) {
13
+ return {
14
+ github: githubProvider(clients.gh),
15
+ gitlab: gitlabProvider(resolveRunner('glab', clients.glab)),
16
+ };
17
+ }
18
+ const NO_TARGET = { host: null, project: null };
19
+ /**
20
+ * The host and full project path a remote URL names, groups kept
21
+ * (`gitlab.com:group/sub/name.git` is project `group/sub/name`). A local path
22
+ * or a `file:` URL names no forge.
23
+ */
24
+ export function forgeTarget(url) {
25
+ if (url === null)
26
+ return NO_TARGET;
27
+ const trimmed = url.trim();
28
+ if (/^file:/i.test(trimmed))
29
+ return NO_TARGET;
30
+ // scp form, user optional (`host:path` is valid git). A one-letter host is a
31
+ // Windows drive (`C:/repo`), which git reads as a local path.
32
+ const scp = /^(?:[\w.-]+@)?([^:/\\]{2,}):(?!\/\/)(.+)$/.exec(trimmed);
33
+ const uri = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(trimmed);
34
+ const match = uri ?? scp;
35
+ if (match === null)
36
+ return NO_TARGET;
37
+ const path = match[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/, '');
38
+ const parts = path.split('/').filter((p) => p !== '');
39
+ if (parts.length < 2)
40
+ return { host: match[1].toLowerCase(), project: null };
41
+ return { host: match[1].toLowerCase(), project: parts.join('/') };
42
+ }
43
+ /**
44
+ * The provider `choice` names for `target`. `auto` reads the host: one with
45
+ * `gitlab` in its name is GitLab, any other is GitHub, which is what the sweep
46
+ * did before providers existed (`gh` answers nothing for a host it does not
47
+ * know). `none` is no provider.
48
+ */
49
+ export function chooseProvider(choice, target) {
50
+ if (choice === 'none')
51
+ return null;
52
+ if (choice !== 'auto')
53
+ return choice;
54
+ return target.host !== null && target.host.includes('gitlab') ? 'gitlab' : 'github';
55
+ }
56
+ /** The chosen provider for a repository, probed; null for none. */
57
+ export function probeProvider(choice, root, target, all) {
58
+ const name = chooseProvider(choice, target);
59
+ if (name === null)
60
+ return null;
61
+ const provider = all[name];
62
+ return { name, ...provider.words, ...target, ...provider.probe(root, target) };
63
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Where pull request evidence comes from. A provider answers two questions
3
+ * for a repository: can its CLI be asked at all, and which pull (or merge)
4
+ * requests exist. Classification reads {@link PRInfo} and never knows which
5
+ * provider answered.
6
+ */
7
+ import type { PRInfo } from '../types.ts';
8
+ /** The providers this build knows. */
9
+ export declare const PROVIDER_NAMES: readonly ["github", "gitlab"];
10
+ export type ProviderName = (typeof PROVIDER_NAMES)[number];
11
+ /** What a caller may ask for: one provider, none, or the one the remote's host suggests. */
12
+ export declare const PROVIDER_CHOICES: readonly ["auto", "github", "gitlab", "none"];
13
+ export type ProviderChoice = (typeof PROVIDER_CHOICES)[number];
14
+ /** The forge a remote points at, read off its URL. */
15
+ export interface ForgeTarget {
16
+ /** The host, lower-cased, without a port; null when the remote names none. */
17
+ host: string | null;
18
+ /** The full project path, groups included (`group/sub/name`); null when there is none. */
19
+ project: string | null;
20
+ }
21
+ /** How a report names a provider's requests and the lookup that lists them. */
22
+ export interface ProviderWords {
23
+ /** The CLI the provider runs: `gh`, `glab`. */
24
+ tool: string;
25
+ /** What the forge calls one request: `PR`, `merge request`. */
26
+ noun: string;
27
+ /** The command that lists them, as a report quotes it: `gh pr list`. */
28
+ listing: string;
29
+ }
30
+ /** The provider chosen for a repository, and what its probe found. */
31
+ export interface ProviderStatus extends ForgeTarget, ProviderWords {
32
+ name: ProviderName;
33
+ /** The CLI is installed. */
34
+ available: boolean;
35
+ /** The CLI has credentials for `host`. */
36
+ authenticated: boolean;
37
+ }
38
+ /** One source of pull request evidence. */
39
+ export interface PrProvider {
40
+ readonly name: ProviderName;
41
+ readonly words: ProviderWords;
42
+ /** Whether the CLI is installed and signed in for `target`'s host. */
43
+ probe(root: string, target: ForgeTarget): {
44
+ available: boolean;
45
+ authenticated: boolean;
46
+ };
47
+ /**
48
+ * The repository's pull requests, fork PRs already dropped, in any order;
49
+ * null when the listing failed. A failure is not "no PRs": the caller
50
+ * classifies on git evidence alone.
51
+ *
52
+ * @param branch Narrow the listing to this exact head branch. The caller
53
+ * checks the head again, so a provider that cannot narrow may list all.
54
+ */
55
+ listPrs(root: string, target: ForgeTarget, branch?: string): PRInfo[] | null;
56
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Where pull request evidence comes from. A provider answers two questions
3
+ * for a repository: can its CLI be asked at all, and which pull (or merge)
4
+ * requests exist. Classification reads {@link PRInfo} and never knows which
5
+ * provider answered.
6
+ */
7
+ /** The providers this build knows. */
8
+ export const PROVIDER_NAMES = ['github', 'gitlab'];
9
+ /** What a caller may ask for: one provider, none, or the one the remote's host suggests. */
10
+ export const PROVIDER_CHOICES = ['auto', ...PROVIDER_NAMES, 'none'];
package/dist/prs.d.ts CHANGED
@@ -1,26 +1,30 @@
1
1
  /**
2
2
  * The pull requests of a repository, indexed by head branch.
3
3
  *
4
- * One `gh pr list` per repository, not one query per branch: a repository with
4
+ * One listing per repository, not one query per branch: a repository with
5
5
  * sixty branches would otherwise pay sixty round trips to answer a question
6
- * one listing covers.
6
+ * one listing covers. Which forge answers is the provider's business; see
7
+ * `providers/`.
7
8
  */
8
- import type { Gh } from '@osdlc/typed-gh';
9
+ import type { PrProvider } from './providers/index.ts';
9
10
  import type { PRInfo, RepoInfo } from './types.ts';
10
11
  /**
11
12
  * Every same-repository pull request, keyed by head branch. Fork PRs are
12
- * dropped: a fork branch that shares a name proves nothing about ours.
13
+ * dropped by the provider: a fork branch that shares a name proves nothing
14
+ * about ours.
13
15
  *
14
16
  * A branch can carry several PRs over its life. The one that decides a
15
17
  * verdict is the most live: an open PR means keep whatever else happened, and
16
18
  * a merged one means the work landed. Ties go to the higher number, so two
17
19
  * runs over unchanged state agree.
18
20
  *
19
- * An empty map is the honest answer when `gh` is missing, unauthenticated or
20
- * failing. Classification degrades to git evidence and the report says so.
21
+ * An empty map is the honest answer when the provider's CLI is missing,
22
+ * unauthenticated or failing, or when there is no provider. Classification
23
+ * degrades to git evidence and the report says so.
21
24
  *
22
- * @param headPrefix Ask `gh` only for PRs whose head starts with this; the map is
23
- * still keyed by exact head. typed-gh's `head` is exact, so the prefix goes
24
- * through gh's `head:` search, which matches by prefix, and is checked again here.
25
+ * @param provider The provider to ask. It is asked only when it is the one
26
+ * `repo.provider` names and that probe found it signed in.
27
+ * @param branch Ask only for PRs whose head is exactly this branch; rows
28
+ * for any other head are dropped here too.
25
29
  */
26
- export declare function fetchPrs(repo: RepoInfo, gh: Gh, headPrefix?: string): Map<string, PRInfo>;
30
+ export declare function fetchPrs(repo: RepoInfo, provider: PrProvider, branch?: string): Map<string, PRInfo>;
package/dist/prs.js CHANGED
@@ -1,72 +1,48 @@
1
1
  /**
2
2
  * The pull requests of a repository, indexed by head branch.
3
3
  *
4
- * One `gh pr list` per repository, not one query per branch: a repository with
4
+ * One listing per repository, not one query per branch: a repository with
5
5
  * sixty branches would otherwise pay sixty round trips to answer a question
6
- * one listing covers.
6
+ * one listing covers. Which forge answers is the provider's business; see
7
+ * `providers/`.
7
8
  */
8
- /** The fields classification consults. Asking for more costs latency. */
9
- const FIELDS = [
10
- 'number',
11
- 'state',
12
- 'mergedAt',
13
- 'headRefName',
14
- 'headRefOid',
15
- 'baseRefName',
16
- 'isCrossRepository',
17
- 'title',
18
- 'url',
19
- ];
20
- const LIMIT = 300;
21
9
  /** Open outranks merged outranks closed, and a newer PR outranks an older. */
22
10
  function priority(state) {
23
11
  return { OPEN: 3, MERGED: 2, CLOSED: 1 }[state] ?? 0;
24
12
  }
25
13
  /**
26
14
  * Every same-repository pull request, keyed by head branch. Fork PRs are
27
- * dropped: a fork branch that shares a name proves nothing about ours.
15
+ * dropped by the provider: a fork branch that shares a name proves nothing
16
+ * about ours.
28
17
  *
29
18
  * A branch can carry several PRs over its life. The one that decides a
30
19
  * verdict is the most live: an open PR means keep whatever else happened, and
31
20
  * a merged one means the work landed. Ties go to the higher number, so two
32
21
  * runs over unchanged state agree.
33
22
  *
34
- * An empty map is the honest answer when `gh` is missing, unauthenticated or
35
- * failing. Classification degrades to git evidence and the report says so.
23
+ * An empty map is the honest answer when the provider's CLI is missing,
24
+ * unauthenticated or failing, or when there is no provider. Classification
25
+ * degrades to git evidence and the report says so.
36
26
  *
37
- * @param headPrefix Ask `gh` only for PRs whose head starts with this; the map is
38
- * still keyed by exact head. typed-gh's `head` is exact, so the prefix goes
39
- * through gh's `head:` search, which matches by prefix, and is checked again here.
27
+ * @param provider The provider to ask. It is asked only when it is the one
28
+ * `repo.provider` names and that probe found it signed in.
29
+ * @param branch Ask only for PRs whose head is exactly this branch; rows
30
+ * for any other head are dropped here too.
40
31
  */
41
- export function fetchPrs(repo, gh, headPrefix) {
32
+ export function fetchPrs(repo, provider, branch) {
42
33
  const byHead = new Map();
43
- if (!(repo.hasGh && repo.ghAuthenticated))
34
+ const status = repo.provider;
35
+ if (status === null || status.name !== provider.name || !status.authenticated)
44
36
  return byHead;
45
- const rows = gh.at(repo.root).prList({
46
- state: 'all',
47
- limit: LIMIT,
48
- fields: FIELDS,
49
- search: headPrefix === undefined ? undefined : `head:${headPrefix}`,
50
- });
37
+ const rows = provider.listPrs(repo.root, status, branch);
51
38
  if (rows === null)
52
39
  return byHead;
53
- for (const row of rows.rows) {
54
- const head = row.headRefName;
55
- // A fork's head branch is not this repository's branch, whatever its name.
56
- if (!head || row.isCrossRepository)
40
+ for (const info of rows) {
41
+ const head = info.headRef;
42
+ if (!head)
57
43
  continue;
58
- if (headPrefix !== undefined && !head.startsWith(headPrefix))
44
+ if (branch !== undefined && head !== branch)
59
45
  continue;
60
- const info = {
61
- number: row.number,
62
- state: row.state,
63
- mergedAt: row.mergedAt,
64
- headRef: head,
65
- headOid: row.headRefOid,
66
- baseRef: row.baseRefName,
67
- title: row.title,
68
- url: row.url,
69
- };
70
46
  const existing = byHead.get(head);
71
47
  if (existing === undefined) {
72
48
  byHead.set(head, info);
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): string;
116
+ export declare function renderReceipt(receipt: Receipt, receiptPath: string | null, style?: Style, opts?: {
117
+ steps?: boolean;
118
+ }): string;