toolroll 0.9.8 → 0.9.10
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 +30 -0
- package/dist/assignment-brief.d.ts +2 -0
- package/dist/assignment-brief.js +2 -1
- package/dist/assignment-presentation.js +3 -1
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +5 -0
- package/dist/assignment.d.ts +9 -0
- package/dist/assignment.js +42 -8
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-crew.js +4 -0
- package/dist/browser-workspace.d.ts +40 -0
- package/dist/builder.d.ts +2 -0
- package/dist/builder.js +114 -8
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-delivery-state.d.ts +5 -0
- package/dist/chat-delivery.js +45 -8
- package/dist/chat-quiet.d.ts +10 -4
- package/dist/chat-quiet.js +87 -22
- package/dist/chat-voice.d.ts +68 -0
- package/dist/chat-voice.js +175 -0
- package/dist/check-progress.js +4 -3
- package/dist/checkout-cleanup.d.ts +40 -2
- package/dist/checkout-cleanup.js +129 -9
- package/dist/desktop-update.d.ts +52 -0
- package/dist/desktop-update.js +147 -5
- package/dist/discord-chat.js +1 -0
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +8 -0
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +7 -5
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +4 -2
- package/dist/lead-status.d.ts +10 -1
- package/dist/lead-status.js +21 -24
- package/dist/needs-you.d.ts +55 -0
- package/dist/needs-you.js +83 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +122 -15
- package/dist/plane-review.js +4 -0
- package/dist/process-recovery-eligibility.d.ts +1 -0
- package/dist/project-delete.js +1 -0
- package/dist/project-learning.d.ts +3 -3
- package/dist/project-learning.js +28 -17
- package/dist/proof.d.ts +3 -1
- package/dist/proof.js +4 -2
- package/dist/releases.d.ts +9 -0
- package/dist/releases.js +30 -0
- package/dist/result-completion.d.ts +4 -0
- package/dist/result-completion.js +11 -0
- package/dist/review-context.d.ts +3 -0
- package/dist/review-context.js +45 -5
- package/dist/run-closeout.d.ts +89 -0
- package/dist/run-closeout.js +302 -0
- package/dist/serve.d.ts +15 -1
- package/dist/serve.js +129 -14
- package/dist/setup-guide.d.ts +2 -2
- package/dist/shared-deps.d.ts +94 -0
- package/dist/shared-deps.js +365 -0
- package/dist/slack-chat.js +2 -0
- package/dist/storage.js +2 -0
- package/dist/store.d.ts +60 -0
- package/dist/store.js +207 -30
- package/dist/task-status.d.ts +18 -4
- package/dist/task-status.js +35 -18
- package/dist/teams-chat.js +1 -1
- package/dist/telegram-progress.d.ts +6 -1
- package/dist/telegram-progress.js +13 -5
- package/dist/telegram-status.d.ts +2 -1
- package/dist/telegram-status.js +6 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +93 -43
- package/dist/test-temp.d.ts +19 -0
- package/dist/test-temp.js +98 -0
- package/dist/toolroll-update-ui.js +7 -2
- package/dist/toolroll-update.d.ts +21 -1
- package/dist/toolroll-update.js +135 -5
- package/dist/work-index.d.ts +11 -0
- package/dist/work-index.js +103 -16
- package/dist/work-summary.d.ts +3 -1
- package/dist/workspace-ui.js +1 -1
- package/dist/worktree.d.ts +16 -0
- package/dist/worktree.js +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** How chat sounds: a teammate talking. One or two short sentences, outcome
|
|
2
|
+
* first, plain words, one action at most. Every chat (Telegram, Slack,
|
|
3
|
+
* Discord, Teams) takes its titles and its finished-work lines from here, so a
|
|
4
|
+
* task id, a "— revision" suffix or a person's own act never reaches a chat.
|
|
5
|
+
* The console keeps every exact name and fact. */
|
|
6
|
+
import type { TelegramTransport } from "./telegram.js";
|
|
7
|
+
import type { Store } from "./store.js";
|
|
8
|
+
/** The bot's display name in every chat app. */
|
|
9
|
+
export declare const BOT_NAME = "Toolroll";
|
|
10
|
+
/** Updates for one person landing within this window become one message, edited in place as it grows. */
|
|
11
|
+
export declare const BATCH_MS: number;
|
|
12
|
+
export declare const SHORT_TITLE_MAX = 60;
|
|
13
|
+
/** A short human summary of a task's title, at most 60 characters: no "— revision", and no task ids. Only the
|
|
14
|
+
* task's own id and tokens `isTaskId` names as real tasks are dropped; `node-20` and `utf-8` stay. */
|
|
15
|
+
export declare function shortTitle(title: string | null | undefined, taskId?: string | null, isTaskId?: (token: string) => boolean): string;
|
|
16
|
+
/** A task's title as every chat says it: short, with its own id and any other real task's id dropped. */
|
|
17
|
+
export declare function chatTitle(store: Pick<Store, "getTask">, taskId: string): string;
|
|
18
|
+
/** A title inside a sentence or list: "faster tests", but "API keys" and "iOS" keep their capitals. */
|
|
19
|
+
export declare function inSentence(summary: string): string;
|
|
20
|
+
/** The facts one line of a chat update is made of, read from the shared task status. */
|
|
21
|
+
export type FinishedFact = {
|
|
22
|
+
summary: string;
|
|
23
|
+
headline: string;
|
|
24
|
+
checks: "passed" | "failed" | "not-run" | "off" | null;
|
|
25
|
+
report: boolean;
|
|
26
|
+
completedBy: string | null;
|
|
27
|
+
/** An open pull request carries this result: the ask is "Merge it?" rather than "Mark it complete?". */
|
|
28
|
+
pullRequest?: boolean;
|
|
29
|
+
/** The lead did this (built it, or marked it complete), so the lead says "I". */
|
|
30
|
+
lead?: boolean;
|
|
31
|
+
/** Not a finished result but another update about the task: its own words when it stands alone, and a few
|
|
32
|
+
* words for a list ("has a plan to review"). */
|
|
33
|
+
update?: {
|
|
34
|
+
words: string;
|
|
35
|
+
phrase: string;
|
|
36
|
+
} | null;
|
|
37
|
+
};
|
|
38
|
+
/** The two buttons under a result ready for a person: the real next step, then a look first. */
|
|
39
|
+
export declare const READY_ACTIONS: {
|
|
40
|
+
readonly merge: "Merge";
|
|
41
|
+
readonly complete: "Mark complete";
|
|
42
|
+
readonly look: "Look first";
|
|
43
|
+
};
|
|
44
|
+
/** Whether this line asks for the result's next step, so it carries [Merge] or [Mark complete] and [Look first]. */
|
|
45
|
+
export declare function asksToFinish(fact: FinishedFact): boolean;
|
|
46
|
+
/** One update, said plainly: what happened, then what happens next. The lead says "I" for what it did. */
|
|
47
|
+
export declare function finishedLine(fact: FinishedFact): string;
|
|
48
|
+
/** Several updates in one message: "4 tasks finished: faster tests, cleanup, …" (or "3 updates: …" when not all of
|
|
49
|
+
* them are finished work), then what broke and what happens next. */
|
|
50
|
+
export declare function batchLine(facts: readonly FinishedFact[]): string;
|
|
51
|
+
/**
|
|
52
|
+
* Name the Telegram bot Toolroll. On pairing it always becomes Toolroll; on upgrade only a bot still called
|
|
53
|
+
* StandingOrders is renamed, so a name a person chose later is kept. Best effort: a refused or failed call
|
|
54
|
+
* leaves the old name and never blocks pairing or delivery. Returns whether a rename was sent and accepted.
|
|
55
|
+
*/
|
|
56
|
+
export declare function nameTelegramBot(transport: TelegramTransport, when: "pairing" | "upgrade"): Promise<boolean>;
|
|
57
|
+
/** The last word on any pushed chat text: a task's own id (bare or in brackets), a "— revision" suffix and a
|
|
58
|
+
* "Replaced by <id>" never reach a chat, whatever words the fact was recorded with. A bare id reads as the
|
|
59
|
+
* task's short title when one is given, else "this task". */
|
|
60
|
+
export declare function chatText(text: string, tasks?: readonly (string | null | undefined | {
|
|
61
|
+
id: string;
|
|
62
|
+
title?: string;
|
|
63
|
+
})[]): string;
|
|
64
|
+
/** The one button a plain fact may carry: its label names where its machine-minted console path goes; nothing in
|
|
65
|
+
* it comes from the fact's text. */
|
|
66
|
+
export declare function factLinkLabel(path: string): string;
|
|
67
|
+
/** Whether the words already name this title as a whole phrase (so it need not be said twice). */
|
|
68
|
+
export declare function mentions(text: string, title: string): boolean;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { phoneText } from "./telegram-status.js";
|
|
2
|
+
/** The bot's display name in every chat app. */
|
|
3
|
+
export const BOT_NAME = "Toolroll";
|
|
4
|
+
/** Updates for one person landing within this window become one message, edited in place as it grows. */
|
|
5
|
+
export const BATCH_MS = 2 * 60_000;
|
|
6
|
+
export const SHORT_TITLE_MAX = 60;
|
|
7
|
+
/** A token that could be an id: letters and digits joined by `-` or `_`, or with a digit in it (`release-099b`,
|
|
8
|
+
* `t42`). Words such as `node-20` and `utf-8` look the same, so a token goes only when it names a real task. */
|
|
9
|
+
const ID_SHAPED = /\(?(?<![\p{L}\p{N}._/-])[\p{L}\p{N}]+(?:[-_.][\p{L}\p{N}]+)*(?![\p{L}\p{N}_/-]|\.[\p{L}\p{N}])\)?/gu;
|
|
10
|
+
/** An id that could be mistaken for nothing else (`release-099b`, `fix_tax`, `t42`). A one-word id such as `tidy`
|
|
11
|
+
* is also a word, and prose keeps its words. */
|
|
12
|
+
const idLike = (id) => /[-_\d]/.test(id);
|
|
13
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
/** A short human summary of a task's title, at most 60 characters: no "— revision", and no task ids. Only the
|
|
15
|
+
* task's own id and tokens `isTaskId` names as real tasks are dropped; `node-20` and `utf-8` stay. */
|
|
16
|
+
export function shortTitle(title, taskId, isTaskId) {
|
|
17
|
+
let text = phoneText(title ?? "", 400);
|
|
18
|
+
// "— revision", "- revision 2", "(revision)": a revision is the same work to a person.
|
|
19
|
+
text = text.replace(/(?:(?:\s*[—–]+|\s+-+)\s*revision\b(?:\s*#?\d+)?|\s*[([]\s*revision(?:\s*#?\d+)?\s*[)\]])/giu, "");
|
|
20
|
+
text = text.replace(ID_SHAPED, token => {
|
|
21
|
+
const bare = token.replace(/^\(|\)$/g, "");
|
|
22
|
+
return idLike(bare) && (bare === taskId || isTaskId?.(bare) === true) ? " " : token;
|
|
23
|
+
});
|
|
24
|
+
text = text.replace(/\(\s*\)/g, " ").replace(/\s+/g, " ").replace(/^[\s·:,;—–-]+|[\s·:,;—–-]+$/gu, "").trim();
|
|
25
|
+
if (text === "") {
|
|
26
|
+
// A task filed with only its id as a title: the words, never the slug.
|
|
27
|
+
const words = (taskId ?? "").replace(/[-_]+/g, " ").trim();
|
|
28
|
+
return words === "" ? "A task" : shortTitle(words[0].toUpperCase() + words.slice(1));
|
|
29
|
+
}
|
|
30
|
+
if (text.length <= SHORT_TITLE_MAX)
|
|
31
|
+
return text;
|
|
32
|
+
const cut = text.slice(0, SHORT_TITLE_MAX - 1);
|
|
33
|
+
const space = cut.lastIndexOf(" ");
|
|
34
|
+
return `${(space > 20 ? cut.slice(0, space) : cut).replace(/[\s·:,;—–-]+$/u, "")}…`;
|
|
35
|
+
}
|
|
36
|
+
/** A task's title as every chat says it: short, with its own id and any other real task's id dropped. */
|
|
37
|
+
export function chatTitle(store, taskId) {
|
|
38
|
+
return shortTitle(store.getTask(taskId)?.title, taskId, token => token !== taskId && store.getTask(token) !== null);
|
|
39
|
+
}
|
|
40
|
+
/** A title inside a sentence or list: "faster tests", but "API keys" and "iOS" keep their capitals. */
|
|
41
|
+
export function inSentence(summary) {
|
|
42
|
+
return /^\p{Lu}\p{Ll}/u.test(summary) ? summary[0].toLowerCase() + summary.slice(1) : summary;
|
|
43
|
+
}
|
|
44
|
+
/** The two buttons under a result ready for a person: the real next step, then a look first. */
|
|
45
|
+
export const READY_ACTIONS = { merge: "Merge", complete: "Mark complete", look: "Look first" };
|
|
46
|
+
/** Whether this line asks for the result's next step, so it carries [Merge] or [Mark complete] and [Look first]. */
|
|
47
|
+
export function asksToFinish(fact) {
|
|
48
|
+
return fact.update == null && fact.headline === "Ready for review" && !fact.report;
|
|
49
|
+
}
|
|
50
|
+
/** One update, said plainly: what happened, then what happens next. The lead says "I" for what it did. */
|
|
51
|
+
export function finishedLine(fact) {
|
|
52
|
+
if (fact.update != null)
|
|
53
|
+
return fact.update.words;
|
|
54
|
+
const name = fact.summary, lower = inSentence(fact.summary);
|
|
55
|
+
const ready = fact.lead === true ? `I finished ${lower}` : `${name} is ready`;
|
|
56
|
+
switch (fact.headline) {
|
|
57
|
+
case "Ready for review":
|
|
58
|
+
if (fact.report)
|
|
59
|
+
return fact.lead === true ? `I wrote up ${lower}. The report is ready to read.` : `${name}: the report is ready to read.`;
|
|
60
|
+
if (fact.checks === "passed")
|
|
61
|
+
return `${ready}. Your tests passed. ${fact.pullRequest === true ? "Merge it?" : "Mark it complete?"}`;
|
|
62
|
+
if (fact.checks === "off")
|
|
63
|
+
return `${ready}. Checks are off for this project, so look it over first.`;
|
|
64
|
+
return `${ready}, but no tests ran. Look it over first.`;
|
|
65
|
+
case "Failed":
|
|
66
|
+
if (fact.checks === "failed")
|
|
67
|
+
return fact.lead === true
|
|
68
|
+
? `I built ${lower}, but its tests failed. It waits for you: retry or ask for changes.`
|
|
69
|
+
: `${name} is built, but its tests failed. It waits for you: retry or ask for changes.`;
|
|
70
|
+
return fact.lead === true
|
|
71
|
+
? `I couldn't finish ${lower}. The work so far is kept; retry when you're ready.`
|
|
72
|
+
: `${name} stopped before it finished. The work so far is kept; retry when you're ready.`;
|
|
73
|
+
case "Complete":
|
|
74
|
+
if (fact.lead === true && fact.completedBy)
|
|
75
|
+
return `I marked ${lower} complete.`;
|
|
76
|
+
return fact.completedBy ? `${name} is done. ${fact.completedBy} marked it complete.` : `${name} is done.`;
|
|
77
|
+
case "Needs you":
|
|
78
|
+
return fact.lead === true ? `I need your decision on ${lower} before I can go on.` : `${name} needs your decision before it can continue.`;
|
|
79
|
+
default:
|
|
80
|
+
return `${name}: ${fact.headline.toLowerCase()}.`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** One task in a list of updates: "faster tests is ready", "cleanup failed". */
|
|
84
|
+
function phraseOf(fact) {
|
|
85
|
+
const name = inSentence(fact.summary);
|
|
86
|
+
if (fact.update != null)
|
|
87
|
+
return `${name} ${fact.update.phrase}`;
|
|
88
|
+
switch (fact.headline) {
|
|
89
|
+
case "Ready for review": return `${name} is ready`;
|
|
90
|
+
case "Failed": return `${name} failed`;
|
|
91
|
+
case "Complete": return `${name} is done`;
|
|
92
|
+
case "Needs you": return `${name} needs you`;
|
|
93
|
+
default: return `${name}: ${fact.headline.toLowerCase()}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Several updates in one message: "4 tasks finished: faster tests, cleanup, …" (or "3 updates: …" when not all of
|
|
97
|
+
* them are finished work), then what broke and what happens next. */
|
|
98
|
+
export function batchLine(facts) {
|
|
99
|
+
if (facts.length === 1)
|
|
100
|
+
return finishedLine(facts[0]);
|
|
101
|
+
const finishedOnly = facts.every(one => one.update == null);
|
|
102
|
+
const names = finishedOnly ? facts.map(one => inSentence(one.summary)) : facts.map(phraseOf);
|
|
103
|
+
const shown = names.slice(0, 3).join(", ");
|
|
104
|
+
const first = `${facts.length} ${finishedOnly ? "tasks finished" : "updates"}: ${shown}${names.length > 3 ? ", …" : "."}`;
|
|
105
|
+
// What happens next: anything that broke or needs a decision waits for the person.
|
|
106
|
+
const failed = facts.filter(one => one.headline === "Failed");
|
|
107
|
+
const needs = facts.filter(one => one.headline === "Needs you").length;
|
|
108
|
+
const broke = failed.length !== 1 ? `${failed.length} failed`
|
|
109
|
+
: failed[0].checks === "failed" ? `tests failed on ${inSentence(failed[0].summary)}` : `${inSentence(failed[0].summary)} stopped before it finished`;
|
|
110
|
+
const waits = [
|
|
111
|
+
...(failed.length === 0 ? [] : [broke]),
|
|
112
|
+
...(needs === 0 ? [] : [needs === facts.length ? "each needs your decision" : `${needs} ${needs === 1 ? "needs" : "need"} your decision`]),
|
|
113
|
+
];
|
|
114
|
+
if (waits.length === 0)
|
|
115
|
+
return first;
|
|
116
|
+
const said = waits.join(", and ");
|
|
117
|
+
return `${first}\n${said[0].toUpperCase()}${said.slice(1)}; ${failed.length + needs === 1 ? "it waits" : "they wait"} for you.`;
|
|
118
|
+
}
|
|
119
|
+
/** The old product name a bot may still wear from before the rename. */
|
|
120
|
+
const OLD_NAME = /standing\s*-?\s*orders/i;
|
|
121
|
+
/**
|
|
122
|
+
* Name the Telegram bot Toolroll. On pairing it always becomes Toolroll; on upgrade only a bot still called
|
|
123
|
+
* StandingOrders is renamed, so a name a person chose later is kept. Best effort: a refused or failed call
|
|
124
|
+
* leaves the old name and never blocks pairing or delivery. Returns whether a rename was sent and accepted.
|
|
125
|
+
*/
|
|
126
|
+
export async function nameTelegramBot(transport, when) {
|
|
127
|
+
try {
|
|
128
|
+
const current = await transport("getMyName", {});
|
|
129
|
+
const name = current.ok ? current.result?.name : undefined;
|
|
130
|
+
const shown = typeof name === "string" ? name : null;
|
|
131
|
+
if (shown === BOT_NAME)
|
|
132
|
+
return false;
|
|
133
|
+
if (when === "upgrade" && (shown === null || !OLD_NAME.test(shown)))
|
|
134
|
+
return false;
|
|
135
|
+
const set = await transport("setMyName", { name: BOT_NAME });
|
|
136
|
+
return set.ok;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** The last word on any pushed chat text: a task's own id (bare or in brackets), a "— revision" suffix and a
|
|
143
|
+
* "Replaced by <id>" never reach a chat, whatever words the fact was recorded with. A bare id reads as the
|
|
144
|
+
* task's short title when one is given, else "this task". */
|
|
145
|
+
export function chatText(text, tasks = []) {
|
|
146
|
+
let out = text.replace(/(?:(?:[ \t]*[—–]+|[ \t]+-+)[ \t]*revision\b(?:[ \t]*#?\d+)?|[ \t]*[([][ \t]*revision(?:[ \t]*#?\d+)?[ \t]*[)\]])/giu, "");
|
|
147
|
+
out = out.replace(/\bReplaced by (?!a newer task)[^\s.,;:!?)]+/gu, "Replaced by a newer task");
|
|
148
|
+
for (const task of tasks) {
|
|
149
|
+
const id = typeof task === "string" ? task : task?.id;
|
|
150
|
+
if (!id || !idLike(id))
|
|
151
|
+
continue;
|
|
152
|
+
const title = typeof task === "object" && task !== null ? task.title : undefined;
|
|
153
|
+
out = out.replace(new RegExp(`[ \\t]*\\(${escape(id)}\\)|(?<![\\p{L}\\p{N}_/=-])${escape(id)}(?![\\p{L}\\p{N}_-])`, "gu"), match => match.trimStart().startsWith("(") ? "" : title ?? "this task");
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/** The one button a plain fact may carry: its label names where its machine-minted console path goes; nothing in
|
|
158
|
+
* it comes from the fact's text. */
|
|
159
|
+
export function factLinkLabel(path) {
|
|
160
|
+
if (/^\/review\?result=[^&]+&run=\d+&tab=checks$/.test(path))
|
|
161
|
+
return "Inspect result";
|
|
162
|
+
if (/^\/t\/[^?#]+#merge$/.test(path))
|
|
163
|
+
return "Merge";
|
|
164
|
+
if (/^\/chat\?task=[^&]+&result=/.test(path))
|
|
165
|
+
return "Open result";
|
|
166
|
+
if (/^\/(?:chat\?task=|t\/)/.test(path))
|
|
167
|
+
return "Open task";
|
|
168
|
+
if (/^\/d\//.test(path))
|
|
169
|
+
return "Open decision";
|
|
170
|
+
return "Open console";
|
|
171
|
+
}
|
|
172
|
+
/** Whether the words already name this title as a whole phrase (so it need not be said twice). */
|
|
173
|
+
export function mentions(text, title) {
|
|
174
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_-])${escape(title)}(?![\\p{L}\\p{N}_-])`, "u").test(text);
|
|
175
|
+
}
|
package/dist/check-progress.js
CHANGED
|
@@ -153,9 +153,10 @@ export class CheckProgressTracker {
|
|
|
153
153
|
}
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
|
-
// scripts/e2e-parallel.mjs: "Running 7 groups at once
|
|
157
|
-
//
|
|
158
|
-
|
|
156
|
+
// scripts/e2e-parallel.mjs: "Running 7 groups, at most 4 at once (12.0 GB available, about 400 MB each): console,
|
|
157
|
+
// task, …" (before: "Running 7 groups at once: …"), then every group's lines as "[group] [app] …", then
|
|
158
|
+
// "✅ group 3.1 min" per group and "N of M groups passed …".
|
|
159
|
+
const starting = /^Running ([0-9]+) groups(?:, at most [0-9]+)? at once(?: \([^)]*\))?: (.+)$/.exec(line);
|
|
159
160
|
if (starting !== null) {
|
|
160
161
|
const total = safeCount(starting[1]);
|
|
161
162
|
const names = starting[2].split(",").map(one => one.trim()).filter(one => one !== "");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CheckoutCleanup } from "./storage.js";
|
|
2
|
-
import type { Store } from "./store.js";
|
|
2
|
+
import type { Store, WorktreeRow } from "./store.js";
|
|
3
3
|
import { type WorktreePool } from "./worktree.js";
|
|
4
4
|
/** A release candidate's checkout holds the build a deploy installs: kept a week after its task was done. */
|
|
5
5
|
export declare const CANDIDATE_KEEP_MS: number;
|
|
@@ -36,12 +36,33 @@ type Status = {
|
|
|
36
36
|
taskId: string;
|
|
37
37
|
finishedAt: string | null;
|
|
38
38
|
dueAt?: string;
|
|
39
|
+
atOnce?: boolean;
|
|
39
40
|
};
|
|
41
|
+
/** The release a deploy installed last (staged-upgrades/<stage>/deployment.json at phase "deployed"): its commit and
|
|
42
|
+
* builder run. A deploy runs from the gate's checkout, so that checkout stays while it is what runs. */
|
|
43
|
+
export type Deployed = {
|
|
44
|
+
head: string | null;
|
|
45
|
+
run: number | null;
|
|
46
|
+
};
|
|
47
|
+
export declare function deployedRelease(stateDir: string | null): Deployed | null;
|
|
48
|
+
/**
|
|
49
|
+
* Which release a gate belongs to: a failed gate is re-filed as the same name plus a letter (`release-x`, then
|
|
50
|
+
* `release-xb`, `release-xc` …), so a gate whose name is another gate's in the same project plus one letter is that
|
|
51
|
+
* release's.
|
|
52
|
+
*/
|
|
53
|
+
export declare function releaseOf(id: string, gates: ReadonlySet<string>): string;
|
|
40
54
|
/** Where every task stands for cleanup, by task_ref id and by each branch it or its runs used. */
|
|
41
|
-
export declare function taskStatuses(store: Store, now: Date): {
|
|
55
|
+
export declare function taskStatuses(store: Store, now: Date, deployed?: Deployed | null): {
|
|
42
56
|
byRef: Map<number, Status>;
|
|
43
57
|
byBranch: Map<string, Status>;
|
|
44
58
|
};
|
|
59
|
+
/** The let-go checkouts whose task is complete or cancelled, and when it finished. Not a release candidate's that
|
|
60
|
+
* stays: a deploy runs from it. */
|
|
61
|
+
export declare function finishedCheckouts(store: Store, now: Date, repo?: string): {
|
|
62
|
+
row: WorktreeRow;
|
|
63
|
+
taskId: string | null;
|
|
64
|
+
finishedAt: string;
|
|
65
|
+
}[];
|
|
45
66
|
/**
|
|
46
67
|
* What a clean-up would remove now, how much space it would free, and what stays and why. `manual` is a clean-up by
|
|
47
68
|
* hand (it doesn't wait for the setting); otherwise it is the worker's pass under the setting. Reads only.
|
|
@@ -71,6 +92,23 @@ export declare function cleanCheckouts(store: Store, pool: WorktreePool, now: ()
|
|
|
71
92
|
repo?: string;
|
|
72
93
|
only?: ReadonlySet<string>;
|
|
73
94
|
}): Promise<CleanResult>;
|
|
95
|
+
export type SlimResult = {
|
|
96
|
+
slimmed: {
|
|
97
|
+
path: string;
|
|
98
|
+
taskId: string | null;
|
|
99
|
+
dropped: string[];
|
|
100
|
+
bytes: number;
|
|
101
|
+
}[];
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* A kept checkout (its result waiting for review, or with changes) drops its dependencies and build output once its
|
|
105
|
+
* run has ended; the next run's setup restores them (node_modules only goes where the project has a setup to restore
|
|
106
|
+
* it). Never one in use or a release candidate (a deploy installs from it). Each in the ledger.
|
|
107
|
+
*/
|
|
108
|
+
export declare function slimKeptCheckouts(store: Store, pool: WorktreePool, now: () => Date, options: {
|
|
109
|
+
actor: string;
|
|
110
|
+
repo?: string;
|
|
111
|
+
}): Promise<SlimResult>;
|
|
74
112
|
/** Throw away a checkout kept for its changes, on purpose; its branch stays. In the ledger. */
|
|
75
113
|
export declare function discardCheckout(store: Store, pool: WorktreePool, path: string, now: Date, actor: string): Promise<{
|
|
76
114
|
ok: true;
|
package/dist/checkout-cleanup.js
CHANGED
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* Only the working copy goes: its branch, and so every commit, stays, and a later lease of the branch makes a new one.
|
|
7
7
|
*
|
|
8
8
|
* Always kept: a checkout in use, one whose task isn't finished (on hold, failed, a revision under way), a result
|
|
9
|
-
* still waiting for review, a
|
|
10
|
-
*
|
|
11
|
-
* (
|
|
9
|
+
* still waiting for review, a checkout with changes (files Toolroll wrote itself aside), and one with commits on no
|
|
10
|
+
* branch. A release candidate's checkout (a deploy installs from it) is kept only while it is the newest gate for its
|
|
11
|
+
* release (for a week after it was done) or the one deployed; a superseded or cancelled gate goes at once, whatever
|
|
12
|
+
* its task's state (staged releases keep the rollback copy). A checkout no task names (adopted after a crash) goes only
|
|
13
|
+
* in a clean-up by hand. Every removal is in the ledger.
|
|
14
|
+
*
|
|
15
|
+
* A kept checkout waiting for review or with changes drops its dependencies and build output once its run ends
|
|
16
|
+
* (`slimKeptCheckouts`); the next run's setup restores them.
|
|
12
17
|
*/
|
|
13
18
|
import { createHash } from "node:crypto";
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
|
-
import { basename } from "node:path";
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
20
|
+
import { basename, dirname, join } from "node:path";
|
|
16
21
|
import { taskBranches } from "./names.js";
|
|
17
22
|
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
18
23
|
import { CLEANUP_CHOICES, bytesWords, diskBytes } from "./storage.js";
|
|
@@ -24,19 +29,80 @@ export const CANDIDATE_KEEP_MS = 7 * DAY_MS;
|
|
|
24
29
|
export const AUTO_MAX = 20;
|
|
25
30
|
/** Git inspections at once. */
|
|
26
31
|
const INSPECT_AT_ONCE = 8;
|
|
32
|
+
export function deployedRelease(stateDir) {
|
|
33
|
+
if (stateDir === null)
|
|
34
|
+
return null;
|
|
35
|
+
const root = join(stateDir, "staged-upgrades");
|
|
36
|
+
let newest = null;
|
|
37
|
+
let names = [];
|
|
38
|
+
try {
|
|
39
|
+
names = readdirSync(root);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
let journal;
|
|
46
|
+
try {
|
|
47
|
+
journal = JSON.parse(readFileSync(join(root, name, "deployment.json"), "utf8"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (journal["phase"] !== "deployed")
|
|
53
|
+
continue;
|
|
54
|
+
const at = typeof journal["deployedAt"] === "string" ? journal["deployedAt"] : typeof journal["updatedAt"] === "string" ? journal["updatedAt"] : "";
|
|
55
|
+
if (newest !== null && newest.at >= at)
|
|
56
|
+
continue;
|
|
57
|
+
newest = { at, head: typeof journal["candidate"] === "string" ? journal["candidate"] : null, run: Number.isSafeInteger(journal["builder"]) ? Number(journal["builder"]) : null };
|
|
58
|
+
}
|
|
59
|
+
return newest === null ? null : { head: newest.head, run: newest.run };
|
|
60
|
+
}
|
|
61
|
+
/** The folder beside the database (where staged releases live), or null for a database in memory. */
|
|
62
|
+
function stateDirOf(store) {
|
|
63
|
+
const main = store.handle.prepare("PRAGMA database_list").all().find(row => row["name"] === "main");
|
|
64
|
+
return typeof main?.["file"] === "string" && main["file"] !== "" ? dirname(main["file"]) : null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Which release a gate belongs to: a failed gate is re-filed as the same name plus a letter (`release-x`, then
|
|
68
|
+
* `release-xb`, `release-xc` …), so a gate whose name is another gate's in the same project plus one letter is that
|
|
69
|
+
* release's.
|
|
70
|
+
*/
|
|
71
|
+
export function releaseOf(id, gates) {
|
|
72
|
+
return /[b-z]$/.test(id) && gates.has(id.slice(0, -1)) ? id.slice(0, -1) : id;
|
|
73
|
+
}
|
|
27
74
|
const STRENGTH = { "in use": 5, "task not finished": 4, "waiting for review": 3, "release candidate": 2 };
|
|
28
75
|
const strength = (one) => one === undefined ? -1 : one.keep === null ? 0 : STRENGTH[one.keep] ?? 1;
|
|
29
76
|
const stronger = (a, b) => strength(b) > strength(a) || (strength(b) === 0 && strength(a) === 0 && (b.finishedAt ?? "") > (a.finishedAt ?? "")) ? b : a;
|
|
30
77
|
/** Where every task stands for cleanup, by task_ref id and by each branch it or its runs used. */
|
|
31
|
-
export function taskStatuses(store, now) {
|
|
78
|
+
export function taskStatuses(store, now, deployed) {
|
|
32
79
|
const db = store.handle;
|
|
33
80
|
const refs = db.prepare(`SELECT r.id AS ref, r.external_id AS id, r.revision_of AS revision_of, t.state AS state, t.updated_at AS updated_at,
|
|
34
81
|
EXISTS (SELECT 1 FROM hold h WHERE h.task_ref = r.id) AS held,
|
|
35
82
|
EXISTS (SELECT 1 FROM run live WHERE live.task_ref = r.id AND live.finished_at IS NULL) AS live,
|
|
36
|
-
|
|
83
|
+
(SELECT MAX(s.candidate) FROM task_scope s WHERE s.task_id = r.external_id) AS candidate, r.repo AS repo
|
|
37
84
|
FROM task_ref r LEFT JOIN task t ON t.id = r.external_id AND r.backend = 'built-in'`).all();
|
|
38
85
|
const runs = db.prepare("SELECT id, task_ref, branch FROM run").all();
|
|
39
86
|
const refOfRun = new Map(runs.map(row => [Number(row["id"]), Number(row["task_ref"])]));
|
|
87
|
+
// Release candidates: per project and release, the newest gate not cancelled; and the one deployed.
|
|
88
|
+
const gates = refs.filter(row => row["candidate"] !== null);
|
|
89
|
+
const gateIds = new Map();
|
|
90
|
+
for (const row of gates) {
|
|
91
|
+
const repo = String(row["repo"]);
|
|
92
|
+
gateIds.set(repo, (gateIds.get(repo) ?? new Set()).add(String(row["id"])));
|
|
93
|
+
}
|
|
94
|
+
const releaseKey = (row) => `${String(row["repo"])}\u0000${releaseOf(String(row["id"]), gateIds.get(String(row["repo"])))}`;
|
|
95
|
+
const newestGate = new Map();
|
|
96
|
+
for (const row of gates) {
|
|
97
|
+
if (row["state"] === "cancelled")
|
|
98
|
+
continue;
|
|
99
|
+
const key = releaseKey(row);
|
|
100
|
+
newestGate.set(key, Math.max(newestGate.get(key) ?? -1, Number(row["ref"])));
|
|
101
|
+
}
|
|
102
|
+
const live = deployed === undefined ? deployedRelease(stateDirOf(store)) : deployed;
|
|
103
|
+
const deployedRef = live === null ? undefined : live.run !== null ? refOfRun.get(live.run) : undefined;
|
|
104
|
+
const isDeployed = (row) => live !== null &&
|
|
105
|
+
(Number(row["ref"]) === deployedRef || (live.head !== null && String(row["candidate"]) === live.head));
|
|
40
106
|
// The completion names its family's root; its run names the result it completes.
|
|
41
107
|
const completed = new Map();
|
|
42
108
|
const completedRef = new Map();
|
|
@@ -59,16 +125,22 @@ export function taskStatuses(store, now) {
|
|
|
59
125
|
const id = String(row["id"]);
|
|
60
126
|
const state = row["state"] === null ? null : String(row["state"]);
|
|
61
127
|
const updated = row["updated_at"] === null ? null : String(row["updated_at"]);
|
|
128
|
+
const gate = row["candidate"] !== null;
|
|
62
129
|
let status;
|
|
63
130
|
if (Number(row["live"]) === 1)
|
|
64
131
|
status = { keep: "in use", taskId: id, finishedAt: null };
|
|
132
|
+
else if (gate && isDeployed(row))
|
|
133
|
+
status = { keep: "release candidate", taskId: id, finishedAt: updated };
|
|
134
|
+
// A superseded or cancelled gate goes at once, whatever its task's state: a newer gate (or none) is the release.
|
|
135
|
+
else if (gate && (state === "cancelled" || newestGate.get(releaseKey(row)) !== ref))
|
|
136
|
+
status = { keep: null, taskId: id, finishedAt: updated, atOnce: true };
|
|
65
137
|
else if (state === null || Number(row["held"]) === 1 || (state !== "done" && state !== "cancelled"))
|
|
66
138
|
status = { keep: "task not finished", taskId: id, finishedAt: null };
|
|
67
139
|
else if (state === "cancelled")
|
|
68
140
|
status = { keep: null, taskId: id, finishedAt: updated };
|
|
69
141
|
else {
|
|
70
142
|
const completedAt = [completed.get(id), completedRef.get(ref)].filter((one) => one !== undefined).sort().at(-1) ?? null;
|
|
71
|
-
if (
|
|
143
|
+
if (gate) {
|
|
72
144
|
const since = completedAt ?? updated ?? now.toISOString();
|
|
73
145
|
const until = new Date(Date.parse(since) + CANDIDATE_KEEP_MS).toISOString();
|
|
74
146
|
status = until > now.toISOString() ? { keep: "release candidate", taskId: id, finishedAt: since, dueAt: until } : { keep: null, taskId: id, finishedAt: since };
|
|
@@ -119,6 +191,17 @@ function statusOf(row, statuses) {
|
|
|
119
191
|
}
|
|
120
192
|
return found;
|
|
121
193
|
}
|
|
194
|
+
/** The let-go checkouts whose task is complete or cancelled, and when it finished. Not a release candidate's that
|
|
195
|
+
* stays: a deploy runs from it. */
|
|
196
|
+
export function finishedCheckouts(store, now, repo) {
|
|
197
|
+
const statuses = taskStatuses(store, now);
|
|
198
|
+
return store.listWorktrees().filter(row => row.releasedAt !== null && row.runner === null && (repo === undefined || row.repo === repo)).flatMap(row => {
|
|
199
|
+
const status = statusOf(row, statuses);
|
|
200
|
+
if (status === undefined || status.finishedAt === null || status.keep !== null)
|
|
201
|
+
return [];
|
|
202
|
+
return [{ row, taskId: status.taskId, finishedAt: status.finishedAt }];
|
|
203
|
+
});
|
|
204
|
+
}
|
|
122
205
|
/** Why a checkout stays before anyone looks at its files, or null: it may go (as far as its task and the setting say). */
|
|
123
206
|
function keptFor(pool, row, statuses, now, cleanup) {
|
|
124
207
|
const status = statusOf(row, statuses);
|
|
@@ -136,6 +219,8 @@ function keptFor(pool, row, statuses, now, cleanup) {
|
|
|
136
219
|
return { why: null, taskId };
|
|
137
220
|
if (cleanup === "never")
|
|
138
221
|
return { why: "cleanup is off", taskId };
|
|
222
|
+
if (status.atOnce === true)
|
|
223
|
+
return { why: null, taskId };
|
|
139
224
|
const days = CLEANUP_CHOICES.find(one => one.value === cleanup).days;
|
|
140
225
|
const since = [status.finishedAt, row.releasedAt].filter((one) => one !== null).sort().at(-1);
|
|
141
226
|
const dueAt = new Date(Date.parse(since) + days * DAY_MS).toISOString();
|
|
@@ -215,6 +300,41 @@ export async function cleanCheckouts(store, pool, now, options) {
|
|
|
215
300
|
}
|
|
216
301
|
return { removed, kept: pruned.kept, freed: removed.reduce((sum, one) => sum + one.bytes, 0) };
|
|
217
302
|
}
|
|
303
|
+
/** Checkouts this process already looked at since they were let go (path and release time): looked at once. */
|
|
304
|
+
const looked = new Set();
|
|
305
|
+
/**
|
|
306
|
+
* A kept checkout (its result waiting for review, or with changes) drops its dependencies and build output once its
|
|
307
|
+
* run has ended; the next run's setup restores them (node_modules only goes where the project has a setup to restore
|
|
308
|
+
* it). Never one in use or a release candidate (a deploy installs from it). Each in the ledger.
|
|
309
|
+
*/
|
|
310
|
+
export async function slimKeptCheckouts(store, pool, now, options) {
|
|
311
|
+
const rows = store.listWorktrees().filter(row => row.releasedAt !== null && row.runner === null && (options.repo === undefined || row.repo === options.repo)
|
|
312
|
+
&& !looked.has(`${row.path}\u0000${row.releasedAt}`) && existsSync(row.path));
|
|
313
|
+
const slimmed = [];
|
|
314
|
+
if (rows.length === 0)
|
|
315
|
+
return { slimmed };
|
|
316
|
+
const keptAs = (row) => keptFor(pool, row, taskStatuses(store, now()), now(), "manual");
|
|
317
|
+
const statuses = taskStatuses(store, now());
|
|
318
|
+
for (const row of rows) {
|
|
319
|
+
const kept = keptFor(pool, row, statuses, now(), "manual");
|
|
320
|
+
if (kept.why === "in use")
|
|
321
|
+
continue;
|
|
322
|
+
looked.add(`${row.path}\u0000${row.releasedAt}`);
|
|
323
|
+
if (kept.why === "release candidate")
|
|
324
|
+
continue;
|
|
325
|
+
if (kept.why !== "waiting for review" && await pool.inspect(row.path) !== "has changes")
|
|
326
|
+
continue;
|
|
327
|
+
const wanted = (fresh) => { const why = keptAs(fresh).why; return why !== "in use" && why !== "release candidate"; };
|
|
328
|
+
const done = await pool.slim(row, wanted, store.liveWorktreeSetup(row.repo) !== null);
|
|
329
|
+
if (!done.ok || done.dropped.length === 0)
|
|
330
|
+
continue;
|
|
331
|
+
const bytes = done.bytes;
|
|
332
|
+
slimmed.push({ path: row.path, taskId: kept.taskId, dropped: done.dropped, bytes });
|
|
333
|
+
store.recordAction({ at: now().toISOString(), actor: options.actor, repo: row.repo, taskId: kept.taskId, runId: null, action: "checkout slimmed", outcome: "removed", source: "work",
|
|
334
|
+
detail: `${basename(row.path)}: dropped ${done.dropped.join(", ")}${bytes > 0 ? ` (about ${bytesWords(bytes)})` : ""}; the next run's setup restores them` });
|
|
335
|
+
}
|
|
336
|
+
return { slimmed };
|
|
337
|
+
}
|
|
218
338
|
/** Throw away a checkout kept for its changes, on purpose; its branch stays. In the ledger. */
|
|
219
339
|
export async function discardCheckout(store, pool, path, now, actor) {
|
|
220
340
|
const bytes = existsSync(path) ? diskBytes([path]).get(path) ?? 0 : 0;
|
|
@@ -233,7 +353,7 @@ export function whyWords(item) {
|
|
|
233
353
|
case "in use": return "in use";
|
|
234
354
|
case "task not finished": return "its task isn't finished";
|
|
235
355
|
case "waiting for review": return "its result is waiting for review";
|
|
236
|
-
case "release candidate": return `release candidate, kept until ${item.dueAt
|
|
356
|
+
case "release candidate": return item.dueAt === undefined ? "the deployed release candidate" : `newest release candidate, kept until ${item.dueAt.slice(0, 10)}`;
|
|
237
357
|
case "has changes": return "has changes";
|
|
238
358
|
case "has commits": return "has commits on no branch";
|
|
239
359
|
case "unreadable": return "git couldn't read it";
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type UpdateJournal = {
|
|
|
27
27
|
serviceInterrupted?: boolean;
|
|
28
28
|
replacementOccurred?: boolean;
|
|
29
29
|
retryableRecovery?: boolean;
|
|
30
|
+
/** The finished run the update is waiting on (or stopped waiting on). Absent while it waits on ordinary work. */
|
|
31
|
+
waiting?: LingeringRun & {
|
|
32
|
+
since: string;
|
|
33
|
+
};
|
|
30
34
|
};
|
|
31
35
|
type ServiceState = {
|
|
32
36
|
state: string;
|
|
@@ -39,6 +43,8 @@ export type UpdateHooks = {
|
|
|
39
43
|
healthy?: (bundle: DesktopBundle, journal: UpdateJournal, since: string) => Promise<boolean>;
|
|
40
44
|
swap?: (journal: UpdateJournal) => Promise<void>;
|
|
41
45
|
sleep?: (ms: number) => Promise<void>;
|
|
46
|
+
/** The clock the drain's two-minute limit reads (tests). */
|
|
47
|
+
now?: () => Date;
|
|
42
48
|
healthTimeoutMs?: number;
|
|
43
49
|
/** Fault injection for state-machine tests, never selectable by a CLI flag. */
|
|
44
50
|
checkpoint?: (phase: Phase) => void;
|
|
@@ -48,7 +54,53 @@ export declare const updateTerminal: (phase: string) => boolean;
|
|
|
48
54
|
/** Durable receipts are private and independent of the app being replaced. */
|
|
49
55
|
export declare function durableJson(file: string, value: unknown): void;
|
|
50
56
|
export declare function readUpdateJournal(stateDir: string, retainedReceipt?: string): UpdateJournal | null;
|
|
57
|
+
/** A finished run whose end Toolroll cannot show yet: in plain words, with the one command that clears it when a
|
|
58
|
+
* person can. */
|
|
59
|
+
export type LingeringRun = {
|
|
60
|
+
run: number;
|
|
61
|
+
on: string;
|
|
62
|
+
action: string | null;
|
|
63
|
+
};
|
|
64
|
+
/** How long an update waits on a finished run it cannot show has ended before it stops waiting and says why. */
|
|
65
|
+
export declare const LINGERING_LIMIT_MS = 120000;
|
|
66
|
+
export declare function lingeringRun(db: DatabaseSync): LingeringRun | null;
|
|
51
67
|
export declare function lingeringWork(db: DatabaseSync): string | null;
|
|
68
|
+
export declare const lingeringWords: (l: LingeringRun) => string;
|
|
69
|
+
/** What an update says when it stops waiting on `l`: what is in the way, the command, and that new work resumed. */
|
|
70
|
+
export declare const stoppedWaitingWords: (l: LingeringRun) => string;
|
|
71
|
+
/** The next step once an update has stopped on `l`. */
|
|
72
|
+
export declare const clearWords: (l: LingeringRun) => string;
|
|
73
|
+
/** An update that is waiting or stopped, what on, and the one action that clears it: for `toolroll status` and the
|
|
74
|
+
* console. `app`: the desktop app's update rather than `toolroll update`. */
|
|
75
|
+
export type UpdateWaiting = {
|
|
76
|
+
app: boolean;
|
|
77
|
+
version: string;
|
|
78
|
+
stopped: boolean;
|
|
79
|
+
run: number | null;
|
|
80
|
+
on: string;
|
|
81
|
+
action: string | null;
|
|
82
|
+
words: string;
|
|
83
|
+
short: string;
|
|
84
|
+
};
|
|
85
|
+
export declare function updateWaitingWords(o: {
|
|
86
|
+
app: boolean;
|
|
87
|
+
version: string;
|
|
88
|
+
stopped: boolean;
|
|
89
|
+
lingering: LingeringRun | null;
|
|
90
|
+
}): UpdateWaiting;
|
|
91
|
+
/** An app updater that ended while letting work finish, with no automatic recovery coming to resume it, leaves new
|
|
92
|
+
* work paused with nobody left to lift the pause. Nothing was stopped or swapped yet, so the pause is lifted here and
|
|
93
|
+
* the update says it stopped. Returns what it said, or null. */
|
|
94
|
+
export declare function releaseStalledDesktopUpdate(stateDir: string): string | null;
|
|
95
|
+
/** The desktop app's update, when it is waiting on work in `databaseFile`'s installation, or stopped on a run still in
|
|
96
|
+
* the way. An app updater that ended mid-drain is released first, as `toolroll update`'s is. */
|
|
97
|
+
export declare function desktopUpdateWaitingOf(databaseFile: string, inTheWay: (run: number) => boolean, stateDirs?: readonly string[]): UpdateWaiting | null;
|
|
98
|
+
/** The same settling reconcile the background worker runs: a finished run's leftover record of a process that never
|
|
99
|
+
* started settles once the run's process groups are proven gone. A record nothing can prove either way stays. */
|
|
100
|
+
export declare function settleLeftoverRecords(db: DatabaseSync, now: Date): number;
|
|
101
|
+
/** Store.stopQuiescenceProblem's reasons, said for a person: no internal terms, and `toolroll run settle` only where it
|
|
102
|
+
* can clear the record (it refuses while anything of the run is alive). */
|
|
103
|
+
export declare function plainLingering(fallbackRun: number, problem: string): LingeringRun;
|
|
52
104
|
export declare function previewDesktopUpdate(stateDir: string, installed: string, candidate: string, label: string, hooks?: UpdateHooks): Promise<{
|
|
53
105
|
old: DesktopBundle;
|
|
54
106
|
next: DesktopBundle;
|