dw-sdlc 0.12.1 → 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.
- package/README.md +25 -16
- package/dist/cli.js +2 -0
- package/dist/commands/gate.js +3 -2
- package/dist/commands/pr-reviewed.d.ts +1 -1
- package/dist/commands/pr-reviewed.js +32 -13
- package/dist/commands/pr-ship.js +35 -8
- package/dist/commands/recap.js +2 -23
- package/dist/commands/sweep.d.ts +2 -0
- package/dist/commands/sweep.js +406 -0
- package/dist/landed.d.ts +7 -0
- package/dist/landed.js +86 -0
- package/dist/pr/decide.d.ts +9 -5
- package/dist/pr/decide.js +36 -9
- package/dist/pr/read.d.ts +2 -0
- package/dist/pr/read.js +12 -8
- package/dist/pr/state.d.ts +32 -6
- package/dist/pr/state.js +57 -16
- package/dist/pr/upstream.d.ts +16 -0
- package/dist/pr/upstream.js +31 -0
- package/dist/run-command.d.ts +1 -1
- package/dist/run-command.js +20 -4
- package/dist/sweep-terminals.d.ts +89 -0
- package/dist/sweep-terminals.js +262 -0
- package/dist/sweep-trees.d.ts +17 -0
- package/dist/sweep-trees.js +72 -0
- package/dist/working-tree.d.ts +2 -0
- package/dist/working-tree.js +5 -0
- package/dist/worktree-lock.d.ts +18 -0
- package/dist/worktree-lock.js +64 -0
- package/package.json +1 -1
package/dist/pr/read.js
CHANGED
|
@@ -64,6 +64,10 @@ function parseOrCannotRun(parse) {
|
|
|
64
64
|
throw error;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
/** The remedy for a pull request `command`, as `pr ship`, cannot find by itself: name it in full. */
|
|
68
|
+
export function nameItRemedy(command) {
|
|
69
|
+
return `name it: dw-sdlc ${command} <n> -R owner/name`;
|
|
70
|
+
}
|
|
67
71
|
/** The pull request named in full, or the one `gh pr view` resolves from what is given. */
|
|
68
72
|
function targetOf({ number, repo }, command) {
|
|
69
73
|
if (number !== undefined && repo !== undefined) {
|
|
@@ -72,8 +76,7 @@ function targetOf({ number, repo }, command) {
|
|
|
72
76
|
const named = [...(number === undefined ? [] : [String(number)]), ...(repo === undefined ? [] : ["-R", repo])];
|
|
73
77
|
const view = gh(["pr", "view", ...named, "--json", "url"]);
|
|
74
78
|
if (view.status !== 0) {
|
|
75
|
-
|
|
76
|
-
throw new CannotRun(`gh pr view found no pull request: ${firstLine(view.stderr)}`, remedy);
|
|
79
|
+
throw new CannotRun(`gh pr view found no pull request: ${firstLine(view.stderr)}`, nameItRemedy(command));
|
|
77
80
|
}
|
|
78
81
|
return parseOrCannotRun(() => parseView(view.stdout));
|
|
79
82
|
}
|
|
@@ -185,25 +188,26 @@ function ownChanges(pr, commit) {
|
|
|
185
188
|
return parseOrCannotRun(() => changesOf(reply));
|
|
186
189
|
}
|
|
187
190
|
/**
|
|
188
|
-
* `pr` and whether its head carries the last
|
|
189
|
-
* same own changes at both.
|
|
191
|
+
* `pr` and whether its head carries the success of the last commit a Review read: a clean read, only
|
|
192
|
+
* merges since it, and the same own changes at both. A read mark never carries. The compares run only
|
|
193
|
+
* when a Review awaits the head.
|
|
190
194
|
*/
|
|
191
195
|
function withCarried(pr) {
|
|
192
|
-
const last = pr.
|
|
193
|
-
if (last === null || !last.mergesOnly || pr.state !== "OPEN" || !awaitsReview(pr)) {
|
|
196
|
+
const last = pr.lastRead;
|
|
197
|
+
if (last === null || !last.clean || !last.mergesOnly || pr.state !== "OPEN" || !awaitsReview(pr)) {
|
|
194
198
|
return { ...pr, carried: false };
|
|
195
199
|
}
|
|
196
200
|
return { ...pr, carried: sameChanges(ownChanges(pr, last.commit), ownChanges(pr, pr.head)) };
|
|
197
201
|
}
|
|
198
202
|
/**
|
|
199
203
|
* `pr` and, when `sized` and a Review awaits a head that carries none, the files the Review would read:
|
|
200
|
-
* the compare from the last
|
|
204
|
+
* the compare from the last commit a Review read, else from the base, to the head.
|
|
201
205
|
*/
|
|
202
206
|
function withChanged(pr, sized) {
|
|
203
207
|
if (!sized || pr.carried || pr.state !== "OPEN" || !awaitsReview(pr)) {
|
|
204
208
|
return { ...pr, changed: null };
|
|
205
209
|
}
|
|
206
|
-
const from = pr.
|
|
210
|
+
const from = pr.lastRead?.commit ?? pr.baseOid;
|
|
207
211
|
const reply = compared(pr, from, pr.head, `the compare of ${from.slice(0, 7)}...${pr.head.slice(0, 7)}`);
|
|
208
212
|
return { ...pr, changed: parseOrCannotRun(() => changedOf(reply)) };
|
|
209
213
|
}
|
package/dist/pr/state.d.ts
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
export declare const REVIEW = "dw-sdlc/review";
|
|
3
3
|
/** How the description of a success the script posts under off begins: no one read that head. */
|
|
4
4
|
export declare const OFF_PREFIX = "off:";
|
|
5
|
+
/**
|
|
6
|
+
* The read mark: the review status in this state and with this description says a Review read that
|
|
7
|
+
* head and left a Finding open.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MARK_STATE = "failure";
|
|
10
|
+
export declare const READ_MARK = "read, findings open";
|
|
11
|
+
/** How the description of the success the dev's `--accept` posts reads: the head, taken with its Findings open. */
|
|
12
|
+
export declare const ACCEPTED = "accepted by the dev, findings open";
|
|
5
13
|
export declare const QUERY = "query($owner: String!, $name: String!, $number: Int!) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n url number state isDraft mergeable mergeStateStatus reviewDecision headRefOid headRefName baseRefName baseRefOid\n baseRef { target { oid } } viewerDidAuthor mergeCommit { oid }\n reviewThreads(first: 100) { nodes { id isResolved isOutdated path line\n comments(first: 1) { nodes { author { login } body } }\n latest: comments(last: 1) { nodes { viewerDidAuthor } } } }\n commits(last: 1) { nodes { commit { committedDate statusCheckRollup {\n contexts(first: 50) { nodes {\n ... on CheckRun { name status conclusion detailsUrl startedAt isRequired(pullRequestNumber: $number) }\n ... on StatusContext { context state targetUrl createdAt isRequired(pullRequestNumber: $number) } } } } } } }\n history: commits(last: 100) { nodes { commit { oid parents { totalCount } status { context(name: \"dw-sdlc/review\") { state description } } } } }\n }\n }\n}";
|
|
6
14
|
export type Bucket = "pass" | "fail" | "pending" | "skipping";
|
|
7
15
|
export type CiCheck = {
|
|
@@ -51,12 +59,17 @@ export type PrState = {
|
|
|
51
59
|
unresolvedThreads: Thread[];
|
|
52
60
|
/** The oids of the pull request's last 100 commits, oldest first, the head last. */
|
|
53
61
|
commits: string[];
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The last commit before the head a Review read, of the last 100: a review success not posted under
|
|
64
|
+
* off, or a read mark.
|
|
65
|
+
*/
|
|
66
|
+
lastRead: LastRead | null;
|
|
67
|
+
/** The read marks since the last review success of any kind, of the last 100 commits. */
|
|
68
|
+
marks: ReadMarks;
|
|
69
|
+
/** Whether the head carries the success of `lastRead`: the read decides it from two compares. */
|
|
57
70
|
carried: boolean;
|
|
58
71
|
/**
|
|
59
|
-
* The files a Review would read, from `
|
|
72
|
+
* The files a Review would read, from `lastRead` or the base to the head, read only for `pr.review:
|
|
60
73
|
* auto` to route on: `unsized` when the compare cannot list them all, null when not read.
|
|
61
74
|
*/
|
|
62
75
|
changed: ChangedFile[] | "unsized" | null;
|
|
@@ -75,11 +88,24 @@ export type ChangedFile = {
|
|
|
75
88
|
previous: string | null;
|
|
76
89
|
lines: number | null;
|
|
77
90
|
};
|
|
78
|
-
/**
|
|
79
|
-
|
|
91
|
+
/**
|
|
92
|
+
* A commit a Review read; `clean` when that Review left a success on it, not a read mark; and whether
|
|
93
|
+
* every commit after it, the head included, is a merge.
|
|
94
|
+
*/
|
|
95
|
+
export type LastRead = {
|
|
80
96
|
commit: string;
|
|
97
|
+
clean: boolean;
|
|
81
98
|
mergesOnly: boolean;
|
|
82
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* The Reviews that left a Finding open since the last review success, a success under off and the dev's
|
|
102
|
+
* acceptance included: `count` their read marks, the head's own among them, and `since` that success's
|
|
103
|
+
* commit, null when none of the last 100 holds one.
|
|
104
|
+
*/
|
|
105
|
+
export type ReadMarks = {
|
|
106
|
+
count: number;
|
|
107
|
+
since: string | null;
|
|
108
|
+
};
|
|
83
109
|
/** A reply that is not the shape the query asks for; the message names the field. */
|
|
84
110
|
export declare class ReplyError extends Error {
|
|
85
111
|
}
|
package/dist/pr/state.js
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
export const REVIEW = "dw-sdlc/review";
|
|
5
5
|
/** How the description of a success the script posts under off begins: no one read that head. */
|
|
6
6
|
export const OFF_PREFIX = "off:";
|
|
7
|
+
/**
|
|
8
|
+
* The read mark: the review status in this state and with this description says a Review read that
|
|
9
|
+
* head and left a Finding open.
|
|
10
|
+
*/
|
|
11
|
+
export const MARK_STATE = "failure";
|
|
12
|
+
export const READ_MARK = "read, findings open";
|
|
13
|
+
/** How the description of the success the dev's `--accept` posts reads: the head, taken with its Findings open. */
|
|
14
|
+
export const ACCEPTED = "accepted by the dev, findings open";
|
|
7
15
|
export const QUERY = `query($owner: String!, $name: String!, $number: Int!) {
|
|
8
16
|
repository(owner: $owner, name: $name) {
|
|
9
17
|
pullRequest(number: $number) {
|
|
@@ -191,26 +199,57 @@ function threadOf(thread) {
|
|
|
191
199
|
export function awaitsReview(pr) {
|
|
192
200
|
return pr.viewerDidAuthor && !pr.checks.some((check) => check.name === REVIEW && check.bucket === "pass");
|
|
193
201
|
}
|
|
194
|
-
/**
|
|
195
|
-
|
|
202
|
+
/**
|
|
203
|
+
* What a commit's review status says: `clean` a success, `findings` the read mark, `off` a success
|
|
204
|
+
* posted under off, which says no one read the commit, null no status of the Review.
|
|
205
|
+
*/
|
|
206
|
+
function postedOn(commit) {
|
|
196
207
|
const status = childOrNull(commit, "status");
|
|
197
208
|
const context = status === null ? null : childOrNull(status, "context");
|
|
198
|
-
if (context === null
|
|
199
|
-
return
|
|
209
|
+
if (context === null) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const state = string(context, "state");
|
|
213
|
+
const description = stringOrNull(context, "description") ?? "";
|
|
214
|
+
if (state === "SUCCESS") {
|
|
215
|
+
return description.startsWith(OFF_PREFIX) ? "off" : "clean";
|
|
200
216
|
}
|
|
201
|
-
return
|
|
217
|
+
return state === MARK_STATE.toUpperCase() && description === READ_MARK ? "findings" : null;
|
|
218
|
+
}
|
|
219
|
+
/** The pull request's last 100 commits, oldest first, each with the review status it carries. */
|
|
220
|
+
function historyOf(pr) {
|
|
221
|
+
return nodes(pr, "history").map((node) => {
|
|
222
|
+
const commit = child(node, "commit");
|
|
223
|
+
return {
|
|
224
|
+
oid: string(commit, "oid"),
|
|
225
|
+
posted: postedOn(commit),
|
|
226
|
+
merge: integer(child(commit, "parents"), "totalCount") > 1
|
|
227
|
+
};
|
|
228
|
+
});
|
|
202
229
|
}
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
/**
|
|
231
|
+
* The last commit before `head` a Review read, the history oldest first. A read mark with only merges
|
|
232
|
+
* after it is passed over: no fix answers its Findings yet, and a Review from it would read only the
|
|
233
|
+
* base's changes, so the Review reads from the read before it and finds them again.
|
|
234
|
+
*/
|
|
235
|
+
function lastReadOf(history, head) {
|
|
236
|
+
const commits = history.map(({ oid, posted, merge }) => ({
|
|
237
|
+
oid,
|
|
238
|
+
read: oid === head || posted === "off" ? null : posted,
|
|
239
|
+
merge
|
|
240
|
+
}));
|
|
241
|
+
const mergesOnlyAfter = (index) => commits.slice(index + 1).every(({ merge }) => merge);
|
|
242
|
+
const index = commits.findLastIndex(({ read }, at) => read === "clean" || (read === "findings" && !mergesOnlyAfter(at)));
|
|
243
|
+
const last = commits[index];
|
|
244
|
+
if (last === undefined) {
|
|
209
245
|
return null;
|
|
210
246
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
247
|
+
return { commit: last.oid, clean: last.read === "clean", mergesOnly: mergesOnlyAfter(index) };
|
|
248
|
+
}
|
|
249
|
+
function readMarksOf(history) {
|
|
250
|
+
const success = history.findLastIndex(({ posted }) => posted === "clean" || posted === "off");
|
|
251
|
+
const count = history.slice(success + 1).filter(({ posted }) => posted === "findings").length;
|
|
252
|
+
return { count, since: history[success]?.oid ?? null };
|
|
214
253
|
}
|
|
215
254
|
/** The repository and number of the pull request whose URL `gh pr view --json url` printed. */
|
|
216
255
|
export function parseView(reply) {
|
|
@@ -233,6 +272,7 @@ export function parsePr(reply, repo) {
|
|
|
233
272
|
const mergeCommit = childOrNull(pr, "mergeCommit");
|
|
234
273
|
const head = string(pr, "headRefOid");
|
|
235
274
|
const baseRef = childOrNull(pr, "baseRef");
|
|
275
|
+
const history = historyOf(pr);
|
|
236
276
|
return {
|
|
237
277
|
url: string(pr, "url"),
|
|
238
278
|
repo,
|
|
@@ -253,7 +293,8 @@ export function parsePr(reply, repo) {
|
|
|
253
293
|
unresolvedThreads: nodes(pr, "reviewThreads")
|
|
254
294
|
.filter((thread) => !boolean(thread, "isResolved") && !boolean(thread, "isOutdated"))
|
|
255
295
|
.map(threadOf),
|
|
256
|
-
commits:
|
|
257
|
-
|
|
296
|
+
commits: history.map(({ oid }) => oid),
|
|
297
|
+
lastRead: lastReadOf(history, head),
|
|
298
|
+
marks: readMarksOf(history)
|
|
258
299
|
};
|
|
259
300
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PrState } from "./state.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The remote branch name `branch` tracks, when it differs from `branch`. Read from the config, so it holds
|
|
4
|
+
* after the remote deleted the branch.
|
|
5
|
+
*/
|
|
6
|
+
export declare function upstreamName(root: string, branch: string): string | undefined;
|
|
7
|
+
/** Whether `pr`'s commits hold `head`, so the head is on GitHub whatever became of the upstream. */
|
|
8
|
+
export declare function holds(pr: PrState, head: string | null): boolean;
|
|
9
|
+
/** The checked-out branch at `root`, its head commit, and the other name its upstream gives. */
|
|
10
|
+
export type RenamedUpstream = {
|
|
11
|
+
branch: string;
|
|
12
|
+
head: string;
|
|
13
|
+
upstream: string;
|
|
14
|
+
};
|
|
15
|
+
/** The checked-out branch whose upstream has another name; undefined on a detached HEAD or a branch with no commit. */
|
|
16
|
+
export declare function renamedUpstream(root: string): RenamedUpstream | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// A branch's upstream, read from git, for a branch pushed under another name: gh resolves
|
|
2
|
+
// its pull request neither through `@{push}` nor by the local name, so `pr ship` and recap ask by the name
|
|
3
|
+
// the upstream gives, and take that pull request only when its commits hold the local head.
|
|
4
|
+
import { asked } from "../working-tree.js";
|
|
5
|
+
/**
|
|
6
|
+
* The remote branch name `branch` tracks, when it differs from `branch`. Read from the config, so it holds
|
|
7
|
+
* after the remote deleted the branch.
|
|
8
|
+
*/
|
|
9
|
+
export function upstreamName(root, branch) {
|
|
10
|
+
const remote = asked(root, ["config", "--get", `branch.${branch}.remote`]);
|
|
11
|
+
const merge = asked(root, ["config", "--get", `branch.${branch}.merge`]);
|
|
12
|
+
if (remote === undefined || remote === "." || merge === undefined || !merge.startsWith("refs/heads/")) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const name = merge.slice("refs/heads/".length);
|
|
16
|
+
return name === branch ? undefined : name;
|
|
17
|
+
}
|
|
18
|
+
/** Whether `pr`'s commits hold `head`, so the head is on GitHub whatever became of the upstream. */
|
|
19
|
+
export function holds(pr, head) {
|
|
20
|
+
return head !== null && pr.commits.includes(head);
|
|
21
|
+
}
|
|
22
|
+
/** The checked-out branch whose upstream has another name; undefined on a detached HEAD or a branch with no commit. */
|
|
23
|
+
export function renamedUpstream(root) {
|
|
24
|
+
const branch = asked(root, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
25
|
+
const head = asked(root, ["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
26
|
+
if (branch === undefined || head === undefined) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const upstream = upstreamName(root, branch);
|
|
30
|
+
return upstream === undefined ? undefined : { branch, head, upstream };
|
|
31
|
+
}
|
package/dist/run-command.d.ts
CHANGED
|
@@ -19,6 +19,6 @@ export type Ran = {
|
|
|
19
19
|
export declare const TAIL_LINES = 50;
|
|
20
20
|
/** Runs `body` with SIGINT and SIGTERM held, so a signal stops the command running and the Gate reports it. */
|
|
21
21
|
export declare function holdingSignals<T>(body: () => Promise<T>): Promise<T>;
|
|
22
|
-
/** The signal that stopped the run, once one has
|
|
22
|
+
/** The signal that stopped the run, once one has: one the Gate got or one that ended a command. */
|
|
23
23
|
export declare function stoppedBy(): NodeJS.Signals | undefined;
|
|
24
24
|
export declare function runCommand(command: string, root: string, output: Output): Promise<Ran>;
|
package/dist/run-command.js
CHANGED
|
@@ -13,6 +13,11 @@ const STOPPING = ["SIGINT", "SIGTERM"];
|
|
|
13
13
|
let running;
|
|
14
14
|
/** The first SIGINT or SIGTERM the Gate got; no command starts after it. */
|
|
15
15
|
let received;
|
|
16
|
+
/**
|
|
17
|
+
* The SIGINT or SIGTERM that ended a command; no command starts after it either. Ctrl-C reaches the
|
|
18
|
+
* command and the Gate together, and the command's exit can be seen before the Gate's own signal.
|
|
19
|
+
*/
|
|
20
|
+
let commandEndedBy;
|
|
16
21
|
function release() {
|
|
17
22
|
for (const signal of STOPPING) {
|
|
18
23
|
process.off(signal, stop);
|
|
@@ -32,6 +37,8 @@ function stop(signal) {
|
|
|
32
37
|
export async function holdingSignals(body) {
|
|
33
38
|
running = undefined;
|
|
34
39
|
received = undefined;
|
|
40
|
+
commandEndedBy = undefined;
|
|
41
|
+
release();
|
|
35
42
|
for (const signal of STOPPING) {
|
|
36
43
|
process.on(signal, stop);
|
|
37
44
|
}
|
|
@@ -41,12 +48,16 @@ export async function holdingSignals(body) {
|
|
|
41
48
|
finally {
|
|
42
49
|
// One turn of the event loop hands a signal that landed after the last command to its handler.
|
|
43
50
|
await new Promise((resolve) => setImmediate(resolve));
|
|
44
|
-
|
|
51
|
+
// The Gate's own signal may still be on its way after a command it ended: the handlers stay, so
|
|
52
|
+
// that signal does not end the Gate before its report.
|
|
53
|
+
if (commandEndedBy === undefined || received !== undefined) {
|
|
54
|
+
release();
|
|
55
|
+
}
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
|
-
/** The signal that stopped the run, once one has
|
|
58
|
+
/** The signal that stopped the run, once one has: one the Gate got or one that ended a command. */
|
|
48
59
|
export function stoppedBy() {
|
|
49
|
-
return received;
|
|
60
|
+
return received ?? commandEndedBy;
|
|
50
61
|
}
|
|
51
62
|
/** Everything written to `fd`, read from its start: the command's writes moved the offset it shares to the end. */
|
|
52
63
|
function written(fd) {
|
|
@@ -99,7 +110,12 @@ function run(command, root, stdio) {
|
|
|
99
110
|
done({ status: "error", exitCode: null, signal: null, detail: `could not start: ${error.message}` });
|
|
100
111
|
}
|
|
101
112
|
});
|
|
102
|
-
child.once("exit", (exitCode, signal) =>
|
|
113
|
+
child.once("exit", (exitCode, signal) => {
|
|
114
|
+
if (signal !== null && STOPPING.includes(signal)) {
|
|
115
|
+
commandEndedBy ??= signal;
|
|
116
|
+
}
|
|
117
|
+
done(answer(exitCode, signal));
|
|
118
|
+
});
|
|
103
119
|
});
|
|
104
120
|
}
|
|
105
121
|
export async function runCommand(command, root, output) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type Worker } from "./worker/herdr.ts";
|
|
2
|
+
import { type LockVerdict } from "./worktree-lock.ts";
|
|
3
|
+
/** A live terminal of another session, or the sweeping one's own, with the place herdr shows it at. */
|
|
4
|
+
type Seen = {
|
|
5
|
+
address: string;
|
|
6
|
+
name: string;
|
|
7
|
+
folder: string;
|
|
8
|
+
place: string;
|
|
9
|
+
};
|
|
10
|
+
/** A Worker with the pid of the program `hc get` reports in its terminal, which a Claude Code lock names. */
|
|
11
|
+
export type Located = Worker & {
|
|
12
|
+
pid: number | undefined;
|
|
13
|
+
};
|
|
14
|
+
export type Terminals = {
|
|
15
|
+
own: Seen | undefined;
|
|
16
|
+
workers: Located[];
|
|
17
|
+
others: Seen[];
|
|
18
|
+
};
|
|
19
|
+
/** What holds a worktree: the terminal of this session, another session's, or a Worker of this one. */
|
|
20
|
+
export type Holder = {
|
|
21
|
+
role: "own";
|
|
22
|
+
terminal: Seen;
|
|
23
|
+
} | {
|
|
24
|
+
role: "other";
|
|
25
|
+
terminal: Seen;
|
|
26
|
+
} | {
|
|
27
|
+
role: "worker";
|
|
28
|
+
worker: Located;
|
|
29
|
+
};
|
|
30
|
+
/** The terminal to stop and remove, or only to remove once it has finished. */
|
|
31
|
+
export type TerminalPlan = {
|
|
32
|
+
kind: "terminal";
|
|
33
|
+
name: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
stop: boolean;
|
|
36
|
+
tree?: string;
|
|
37
|
+
};
|
|
38
|
+
/** The address of the terminal the Sweep runs in, or undefined outside herdr. */
|
|
39
|
+
export declare function sweepingTerminal(): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Reads herdr for the sweeping terminal `own`: its Workers, finished ones included, and the other live
|
|
42
|
+
* terminals. An `hc` that fails stops the Sweep, since a terminal it cannot see must not count as absent.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readTerminals(own: string): Terminals;
|
|
45
|
+
/** A Worker whose terminal is live but whose folder is gone, with no worktree of `roots` around it, could commit nothing more. */
|
|
46
|
+
export declare function workerWithoutFolder({ workers }: Terminals, roots: string[]): Worker[];
|
|
47
|
+
/** The terminals in the folder of each of `trees`; the main checkout `main` takes those not in a linked one. */
|
|
48
|
+
export declare function holdersOf({ own, workers, others }: Terminals, main: string, trees: string[]): Map<string, Holder[]>;
|
|
49
|
+
/** How the Sweep says a Worker that is working, blocked or yet to report is, or undefined for one that is done. */
|
|
50
|
+
export declare function atWork({ status }: Worker): string | undefined;
|
|
51
|
+
/** What the terminals in a worktree keep it for, or null when only Workers at the end of their turn are in it. */
|
|
52
|
+
export declare function whyHeld(holders: Holder[]): {
|
|
53
|
+
reason: string;
|
|
54
|
+
command: string | null;
|
|
55
|
+
} | null;
|
|
56
|
+
/** The terminals of the Workers done in a worktree that goes, to stop and remove with it. */
|
|
57
|
+
export declare function stopsOf(holders: Holder[], tree: string): TerminalPlan[];
|
|
58
|
+
/** Stops, if `item` asks, and removes its terminal; the words of the `hc` that refused, or undefined when it is gone. */
|
|
59
|
+
export declare function removeTerminal(item: {
|
|
60
|
+
name: string;
|
|
61
|
+
stop?: boolean;
|
|
62
|
+
}): string | undefined;
|
|
63
|
+
/** A Worker the Sweep keeps, with why. */
|
|
64
|
+
export type KeptWorker = {
|
|
65
|
+
name: string;
|
|
66
|
+
reason: string;
|
|
67
|
+
};
|
|
68
|
+
/** The Workers no worktree of this Sweep decides: a finished one is removed, one whose folder is gone goes under the status rule. */
|
|
69
|
+
export declare function outsideWorktrees(terminals: Terminals, roots: string[]): {
|
|
70
|
+
remove: TerminalPlan[];
|
|
71
|
+
keep: KeptWorker[];
|
|
72
|
+
};
|
|
73
|
+
/** The command that stops or removes the terminal of a planned terminal by hand. */
|
|
74
|
+
export declare function byHand(item: {
|
|
75
|
+
name: string;
|
|
76
|
+
stop?: boolean;
|
|
77
|
+
}): string;
|
|
78
|
+
/** The Worker in `holders` whose program is the process a live lock names, when it may be stopped. */
|
|
79
|
+
export declare function lockHolder(pid: number, holders: Holder[]): Located | undefined;
|
|
80
|
+
/** The lock of the worktree at `path` read again once its Worker is stopped, given the program a moment to end. */
|
|
81
|
+
export declare function lockAfterStop(root: string, path: string): LockVerdict;
|
|
82
|
+
type KeptTerminal = {
|
|
83
|
+
name: string;
|
|
84
|
+
reason: string;
|
|
85
|
+
command: string | null;
|
|
86
|
+
};
|
|
87
|
+
/** The terminals in a worktree that stays which the Sweep lists as kept: Workers, and another session's terminals. */
|
|
88
|
+
export declare function keptIn(holders: Holder[], tree: string): KeptTerminal[];
|
|
89
|
+
export {};
|