allaboard 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/agent/claude/backend.js +124 -0
- package/dist/agent/claude/capabilities.js +30 -0
- package/dist/agent/claude/chrome.js +42 -0
- package/dist/agent/claude/hooks.js +37 -0
- package/dist/agent/claude/info.js +61 -0
- package/dist/agent/claude/listing.js +230 -0
- package/dist/agent/claude/module.js +82 -0
- package/dist/agent/claude/prompt.js +24 -0
- package/dist/agent/claude/record.js +9 -0
- package/dist/agent/claude/requests.js +122 -0
- package/dist/agent/claude/session.js +441 -0
- package/dist/agent/claude/translate.js +581 -0
- package/dist/agent/claude/trust.js +576 -0
- package/dist/agent/claude/usage-probe.js +103 -0
- package/dist/agent/claude/usage.js +110 -0
- package/dist/agent/claude/wire.js +22 -0
- package/dist/agent/seam.js +21 -0
- package/dist/chat/seam.js +10 -0
- package/dist/chat/slack/app/answers.js +209 -0
- package/dist/chat/slack/app/app.js +262 -0
- package/dist/chat/slack/app/clip.js +191 -0
- package/dist/chat/slack/app/guards.js +162 -0
- package/dist/chat/slack/app/home-controls.js +91 -0
- package/dist/chat/slack/app/messages.js +181 -0
- package/dist/chat/slack/app/open.js +392 -0
- package/dist/chat/slack/app/order.js +37 -0
- package/dist/chat/slack/app/requests.js +482 -0
- package/dist/chat/slack/app/resume-bind.js +71 -0
- package/dist/chat/slack/app/wire.js +83 -0
- package/dist/chat/slack/attachments.js +348 -0
- package/dist/chat/slack/bind.js +67 -0
- package/dist/chat/slack/channel.js +67 -0
- package/dist/chat/slack/channels.js +43 -0
- package/dist/chat/slack/delete.js +278 -0
- package/dist/chat/slack/footer.js +43 -0
- package/dist/chat/slack/hold.js +37 -0
- package/dist/chat/slack/home.js +1069 -0
- package/dist/chat/slack/module.js +174 -0
- package/dist/chat/slack/notice.js +64 -0
- package/dist/chat/slack/openfile/modal.js +362 -0
- package/dist/chat/slack/openfile/read.js +29 -0
- package/dist/chat/slack/quiet-logger.js +52 -0
- package/dist/chat/slack/repair.js +123 -0
- package/dist/chat/slack/reply/blocks.js +372 -0
- package/dist/chat/slack/reply/chars.js +59 -0
- package/dist/chat/slack/reply/clients.js +171 -0
- package/dist/chat/slack/reply/errors.js +125 -0
- package/dist/chat/slack/reply/escape.js +25 -0
- package/dist/chat/slack/reply/limiter.js +86 -0
- package/dist/chat/slack/reply/markdown.js +210 -0
- package/dist/chat/slack/reply/places.js +262 -0
- package/dist/chat/slack/reply/sinks.js +2403 -0
- package/dist/chat/slack/reply/status.js +433 -0
- package/dist/chat/slack/reply/tasks.js +11 -0
- package/dist/chat/slack/requests.js +552 -0
- package/dist/chat/slack/resume.js +127 -0
- package/dist/chat/slack/setup.js +114 -0
- package/dist/chat/slack/texts.js +185 -0
- package/dist/chat/slack/thread.js +537 -0
- package/dist/chat/slack/update.js +71 -0
- package/dist/chat/slack/voice.js +84 -0
- package/dist/cli.js +326 -0
- package/dist/clock.js +21 -0
- package/dist/core/cleanup.js +145 -0
- package/dist/core/commands.js +172 -0
- package/dist/core/config.js +174 -0
- package/dist/core/files/changed.js +125 -0
- package/dist/core/files/listing.js +382 -0
- package/dist/core/files/paths.js +9 -0
- package/dist/core/files/read.js +191 -0
- package/dist/core/files/waiting.js +34 -0
- package/dist/core/flows/answers.js +133 -0
- package/dist/core/flows/ask-owner.js +95 -0
- package/dist/core/flows/asking.js +14 -0
- package/dist/core/flows/bind.js +95 -0
- package/dist/core/flows/contract.js +1 -0
- package/dist/core/flows/flows.js +35 -0
- package/dist/core/flows/hold.js +22 -0
- package/dist/core/flows/inbound.js +24 -0
- package/dist/core/flows/lists.js +21 -0
- package/dist/core/flows/messages.js +293 -0
- package/dist/core/flows/notices.js +51 -0
- package/dist/core/flows/repair.js +106 -0
- package/dist/core/flows/resume.js +163 -0
- package/dist/core/flows/setup.js +130 -0
- package/dist/core/flows/status.js +98 -0
- package/dist/core/flows/talk.js +207 -0
- package/dist/core/flows/update.js +370 -0
- package/dist/core/flows/words.js +171 -0
- package/dist/core/folders.js +93 -0
- package/dist/core/footer.js +313 -0
- package/dist/core/guide.js +50 -0
- package/dist/core/hold.js +126 -0
- package/dist/core/instance.js +46 -0
- package/dist/core/lock.js +412 -0
- package/dist/core/models.js +10 -0
- package/dist/core/paths.js +75 -0
- package/dist/core/port/file-flows.js +1 -0
- package/dist/core/port/home.js +1 -0
- package/dist/core/port/requests.js +1 -0
- package/dist/core/port/threads.js +1 -0
- package/dist/core/port/updates.js +1 -0
- package/dist/core/port.js +17 -0
- package/dist/core/reply/fold.js +175 -0
- package/dist/core/reply/previews.js +258 -0
- package/dist/core/reply/renderer.js +846 -0
- package/dist/core/reply/tail.js +24 -0
- package/dist/core/reply/words.js +32 -0
- package/dist/core/requests.js +158 -0
- package/dist/core/sessions/constants.js +61 -0
- package/dist/core/sessions/deps.js +1 -0
- package/dist/core/sessions/directory.js +86 -0
- package/dist/core/sessions/errors.js +79 -0
- package/dist/core/sessions/manager.js +378 -0
- package/dist/core/sessions/session.js +3297 -0
- package/dist/core/sessions/tasks.js +266 -0
- package/dist/core/sessions/turn.js +45 -0
- package/dist/core/setup.js +28 -0
- package/dist/core/state.js +724 -0
- package/dist/core/texts.js +451 -0
- package/dist/core/updates.js +332 -0
- package/dist/daemon.js +566 -0
- package/dist/host/npm.js +134 -0
- package/dist/host/program.js +49 -0
- package/dist/host/run.js +52 -0
- package/dist/host/service/launchd.js +385 -0
- package/dist/host/service/listing.js +42 -0
- package/dist/host/service/seam.js +1 -0
- package/dist/log.js +37 -0
- package/dist/main.js +29 -0
- package/dist/node-version.js +40 -0
- package/dist/registry.js +12 -0
- package/dist/semver.js +53 -0
- package/dist/text.js +89 -0
- package/dist/updates-host.js +113 -0
- package/dist/values.js +30 -0
- package/dist/version.js +54 -0
- package/package.json +32 -4
- package/slack-app-manifest.json +50 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { getLogger } from "../log.js";
|
|
2
|
+
import { isNewer } from "../semver.js";
|
|
3
|
+
import { errorName } from "../values.js";
|
|
4
|
+
import * as texts from "./texts.js";
|
|
5
|
+
import { fill } from "./texts.js";
|
|
6
|
+
export const logger = getLogger("allaboard.core.updates");
|
|
7
|
+
/** The registry is asked at start and every 6 hours. */
|
|
8
|
+
export const UPDATE_CHECK_SECONDS = 6 * 60 * 60;
|
|
9
|
+
/** A check that could not finish (the copy was being replaced) is asked again after this. */
|
|
10
|
+
export const UPDATE_RETRY_SECONDS = 30;
|
|
11
|
+
/**
|
|
12
|
+
* How many checks in a row may find the entry file missing before the short retries end: a swap
|
|
13
|
+
* by another process lasts seconds, and a file that is gone for good (`npm uninstall -g` under a
|
|
14
|
+
* live daemon) must not be asked about every 30 seconds for as long as the daemon runs.
|
|
15
|
+
*/
|
|
16
|
+
export const UPDATE_RETRY_LIMIT = 10;
|
|
17
|
+
export class Updates {
|
|
18
|
+
host;
|
|
19
|
+
/**
|
|
20
|
+
* Called after a change a page may show: a check, a phase. The daemon sets it to the chat's
|
|
21
|
+
* index refresh. A throw is logged here.
|
|
22
|
+
*/
|
|
23
|
+
onChange = null;
|
|
24
|
+
#now;
|
|
25
|
+
#latest = null;
|
|
26
|
+
#checkedAt = null;
|
|
27
|
+
#failure = null;
|
|
28
|
+
#updatable = null;
|
|
29
|
+
#phase = { kind: "idle" };
|
|
30
|
+
#checking = null;
|
|
31
|
+
// Checks in a row that found the copy being replaced (`UPDATE_RETRY_LIMIT`).
|
|
32
|
+
#swaps = 0;
|
|
33
|
+
// Ended at the start of the shutdown: a registry question has no use then.
|
|
34
|
+
#checkEnd = new AbortController();
|
|
35
|
+
// Ended when a drain begins: a click that has not reached `npm install` stops asking.
|
|
36
|
+
#drainStart = new AbortController();
|
|
37
|
+
// Ended when the stop will not wait for an install any more: npm gets SIGTERM and rolls back.
|
|
38
|
+
#installEnd = new AbortController();
|
|
39
|
+
// The clicks under way (`track`): the drain waits for them.
|
|
40
|
+
#clicks = new Set();
|
|
41
|
+
constructor(host, options) {
|
|
42
|
+
this.host = host;
|
|
43
|
+
this.#now = options.now;
|
|
44
|
+
}
|
|
45
|
+
/** The version this process runs. */
|
|
46
|
+
get installed() {
|
|
47
|
+
return this.host.installed;
|
|
48
|
+
}
|
|
49
|
+
/** The newest version the last successful check found; null before one. */
|
|
50
|
+
get latest() {
|
|
51
|
+
return this.#latest;
|
|
52
|
+
}
|
|
53
|
+
/** Wall-clock seconds of the last check, successful or not; null before one. */
|
|
54
|
+
get checkedAt() {
|
|
55
|
+
return this.#checkedAt;
|
|
56
|
+
}
|
|
57
|
+
/** Why the last check could not say; null when it could. */
|
|
58
|
+
get checkFailure() {
|
|
59
|
+
return this.#failure;
|
|
60
|
+
}
|
|
61
|
+
/** Why the copy is not one an update replaces; null when it is, when unknown, or when npm could not say. */
|
|
62
|
+
get refusal() {
|
|
63
|
+
const found = this.#updatable;
|
|
64
|
+
if (found === null || !("refusal" in found) || found.refusal.kind === "npm")
|
|
65
|
+
return null;
|
|
66
|
+
return found.refusal;
|
|
67
|
+
}
|
|
68
|
+
/** The package folder of the copy the last check found to be one an update replaces. */
|
|
69
|
+
get copy() {
|
|
70
|
+
return this.#updatable !== null && "copy" in this.#updatable ? this.#updatable.copy : null;
|
|
71
|
+
}
|
|
72
|
+
/** Whether the last check found the copy to be one an update replaces. */
|
|
73
|
+
get canUpdate() {
|
|
74
|
+
return this.copy !== null;
|
|
75
|
+
}
|
|
76
|
+
get phase() {
|
|
77
|
+
return this.#phase;
|
|
78
|
+
}
|
|
79
|
+
/** The latest version when it is newer than the one that runs. */
|
|
80
|
+
get newer() {
|
|
81
|
+
return this.#latest !== null && isNewer(this.#latest, this.installed) ? this.#latest : null;
|
|
82
|
+
}
|
|
83
|
+
/** The signal that cancels the install of the click (`cancelInstall`). */
|
|
84
|
+
get installing() {
|
|
85
|
+
return this.#installEnd.signal;
|
|
86
|
+
}
|
|
87
|
+
/** Whether a click is at work (an install, or what follows it). */
|
|
88
|
+
get working() {
|
|
89
|
+
return this.#clicks.size > 0;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Runs the work of a click so that a stop can wait for it (`settled`, `cancelInstall`). `work`
|
|
93
|
+
* starts at once, so its first lines run before this returns.
|
|
94
|
+
*/
|
|
95
|
+
track(work) {
|
|
96
|
+
const running = work().finally(() => this.#clicks.delete(running));
|
|
97
|
+
this.#clicks.add(running);
|
|
98
|
+
return running;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The drain's wait for the update: resolves once no click is at work, or when `cutShort`
|
|
102
|
+
* aborts. A click inside `validate` (still asking npm whether the copy updates) is ended at the
|
|
103
|
+
* start of the wait, since a question that hangs would hold the drain for its whole limit; any
|
|
104
|
+
* other click, an install under way included, is waited for.
|
|
105
|
+
*/
|
|
106
|
+
async settled(cutShort) {
|
|
107
|
+
this.#drainStart.abort();
|
|
108
|
+
const cut = new Promise((resolve) => {
|
|
109
|
+
if (cutShort.aborted)
|
|
110
|
+
resolve();
|
|
111
|
+
else
|
|
112
|
+
cutShort.addEventListener("abort", () => resolve(), { once: true });
|
|
113
|
+
});
|
|
114
|
+
while (this.working && !cutShort.aborted) {
|
|
115
|
+
await Promise.race([Promise.allSettled([...this.#clicks]), cut]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The stop will not wait for the install: npm under way is sent SIGTERM (it finishes its step,
|
|
120
|
+
* puts the old folder back and ends), and no other install starts. Resolves once every click
|
|
121
|
+
* has ended; never rejects.
|
|
122
|
+
*/
|
|
123
|
+
async cancelInstall() {
|
|
124
|
+
this.#installEnd.abort();
|
|
125
|
+
await Promise.allSettled([...this.#clicks]);
|
|
126
|
+
}
|
|
127
|
+
/** The shutdown began: a question to the registry under way is ended. */
|
|
128
|
+
endChecks() {
|
|
129
|
+
this.#checkEnd.abort();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Asks the host for the newest version and for the copy; one at a time, never rejects. Resolves
|
|
133
|
+
* false when the answer is not complete (the copy was being replaced by another process: the
|
|
134
|
+
* previous answer about the copy stands and the caller asks again soon, up to
|
|
135
|
+
* `UPDATE_RETRY_LIMIT` times in a row), true otherwise.
|
|
136
|
+
*/
|
|
137
|
+
check() {
|
|
138
|
+
this.#checking ??= this.#check().finally(() => {
|
|
139
|
+
this.#checking = null;
|
|
140
|
+
});
|
|
141
|
+
return this.#checking;
|
|
142
|
+
}
|
|
143
|
+
async #check() {
|
|
144
|
+
let complete = true;
|
|
145
|
+
try {
|
|
146
|
+
const signal = this.#checkEnd.signal;
|
|
147
|
+
const [latest, updatable] = await Promise.all([
|
|
148
|
+
this.host.latest(signal),
|
|
149
|
+
this.host.updatable(signal),
|
|
150
|
+
]);
|
|
151
|
+
if ("swapping" in updatable) {
|
|
152
|
+
this.#swaps += 1;
|
|
153
|
+
if (this.#swaps < UPDATE_RETRY_LIMIT)
|
|
154
|
+
complete = false;
|
|
155
|
+
// The file is not coming back soon: the answer about the copy is not kept, so that
|
|
156
|
+
// nothing offers an update of a copy that is gone, and the long wait begins.
|
|
157
|
+
else
|
|
158
|
+
this.#updatable = null;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.#swaps = 0;
|
|
162
|
+
this.#updatable = updatable;
|
|
163
|
+
}
|
|
164
|
+
if ("version" in latest) {
|
|
165
|
+
this.#latest = latest.version;
|
|
166
|
+
this.#failure = null;
|
|
167
|
+
// A version newer than the one that failed is a new offer, not the old failure.
|
|
168
|
+
if (this.#phase.kind === "failed" && isNewer(latest.version, this.#phase.version)) {
|
|
169
|
+
this.#phase = { kind: "idle" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.#failure = latest.failure;
|
|
174
|
+
}
|
|
175
|
+
// npm that could not say what the copy is leaves the update unproven, as an unknown latest does.
|
|
176
|
+
if (this.#failure === null && "refusal" in updatable && updatable.refusal.kind === "npm") {
|
|
177
|
+
this.#failure = updatable.refusal.failure;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
logger.warning(`the update check failed: ${errorName(error)}`);
|
|
182
|
+
this.#failure = { kind: "other", status: null };
|
|
183
|
+
}
|
|
184
|
+
this.#checkedAt = this.#now();
|
|
185
|
+
this.#changed();
|
|
186
|
+
return complete;
|
|
187
|
+
}
|
|
188
|
+
/** The state of a check, made once if none has run yet. */
|
|
189
|
+
async current() {
|
|
190
|
+
if (this.#checkedAt === null || this.#checking !== null)
|
|
191
|
+
await this.check();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Asks the host again whether the copy updates, and keeps the answer; `swapping` (the copy is
|
|
195
|
+
* being replaced by another process) keeps the previous one and the count of swaps; any other
|
|
196
|
+
* answer restarts that count (`UPDATE_RETRY_LIMIT`). Null when a drain or the shutdown
|
|
197
|
+
* ended the question: what npm answered to its SIGTERM is no answer about the copy.
|
|
198
|
+
*/
|
|
199
|
+
async validate() {
|
|
200
|
+
const signal = AbortSignal.any([this.#checkEnd.signal, this.#drainStart.signal]);
|
|
201
|
+
const found = await this.host.updatable(signal);
|
|
202
|
+
if (signal.aborted)
|
|
203
|
+
return null;
|
|
204
|
+
if (!("swapping" in found)) {
|
|
205
|
+
this.#updatable = found;
|
|
206
|
+
this.#swaps = 0;
|
|
207
|
+
}
|
|
208
|
+
this.#changed();
|
|
209
|
+
return found;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Claims the update of `version`: false while another is installing or waiting to restart, and
|
|
213
|
+
* once the stop cancelled the install.
|
|
214
|
+
*/
|
|
215
|
+
begin(version) {
|
|
216
|
+
if (this.#installEnd.signal.aborted)
|
|
217
|
+
return false;
|
|
218
|
+
if (this.#phase.kind === "installing" || this.#phase.kind === "restarting")
|
|
219
|
+
return false;
|
|
220
|
+
this.#phase = { kind: "installing", version };
|
|
221
|
+
this.#changed();
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
/** The claim ended with nothing installed and nothing to say: back to what a check found. */
|
|
225
|
+
abandon() {
|
|
226
|
+
this.#phase = { kind: "idle" };
|
|
227
|
+
this.#changed();
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* `version` is installed: the daemon waits for running turns to finish, then restarts. `note`
|
|
231
|
+
* tells the instances that did not restart.
|
|
232
|
+
*/
|
|
233
|
+
restarting(version, note = null) {
|
|
234
|
+
this.#phase = { kind: "restarting", version, note };
|
|
235
|
+
this.#changed();
|
|
236
|
+
}
|
|
237
|
+
/** The update of `version` did not happen; `text` says why, for a notice. */
|
|
238
|
+
fail(version, text) {
|
|
239
|
+
this.#phase = { kind: "failed", version, text };
|
|
240
|
+
this.#changed();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The version the owner has not been told of yet, once per version (`told` is the last one he
|
|
244
|
+
* was told); null when up to date, for a copy that does not update, while an update is under
|
|
245
|
+
* way, for a daemon no service runs, and before a check.
|
|
246
|
+
*/
|
|
247
|
+
noticeDue(told) {
|
|
248
|
+
const latest = this.newer;
|
|
249
|
+
if (latest === null || !this.canUpdate || latest === told)
|
|
250
|
+
return null;
|
|
251
|
+
if (this.#phase.kind === "installing" || this.#phase.kind === "restarting")
|
|
252
|
+
return null;
|
|
253
|
+
// Asked last: it runs the service manager. A daemon no service runs cannot update, as a copy
|
|
254
|
+
// that updates outside the chat cannot (`!allaboard` says why).
|
|
255
|
+
return this.host.supervised() ? latest : null;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The line of the chat's own page; null when there is nothing to offer, the copy does not
|
|
259
|
+
* update, or no service runs this daemon.
|
|
260
|
+
*/
|
|
261
|
+
line() {
|
|
262
|
+
if (!this.canUpdate)
|
|
263
|
+
return null;
|
|
264
|
+
const phase = this.#phase;
|
|
265
|
+
const latest = phase.kind === "idle" ? this.newer : phase.version;
|
|
266
|
+
if (latest === null || !this.host.supervised())
|
|
267
|
+
return null;
|
|
268
|
+
return {
|
|
269
|
+
installed: this.installed,
|
|
270
|
+
latest,
|
|
271
|
+
changelog: this.host.changelog(latest),
|
|
272
|
+
phase: phase.kind === "idle" ? "available" : phase.kind,
|
|
273
|
+
failure: phase.kind === "failed" ? phase.text : null,
|
|
274
|
+
note: phase.kind === "restarting" ? phase.note : null,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
#changed() {
|
|
278
|
+
try {
|
|
279
|
+
this.onChange?.();
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
logger.warning(`the page was not told of an update change: ${errorName(error)}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The scheduled check: one at start, then one every `seconds`, none once a stop has begun (the
|
|
288
|
+
* daemon is leaving, and the next start checks anyway). A check that could not finish (`check`
|
|
289
|
+
* resolved false) is asked again after `retrySeconds`. Ends when `signal` aborts; a check in
|
|
290
|
+
* flight ends with the shutdown (`Updates.endChecks`).
|
|
291
|
+
*/
|
|
292
|
+
export async function checkEvery(options) {
|
|
293
|
+
for (;;) {
|
|
294
|
+
if (options.signal.aborted || options.sessions.draining)
|
|
295
|
+
return;
|
|
296
|
+
const complete = await options.check();
|
|
297
|
+
try {
|
|
298
|
+
await options.clock.sleep(complete ? options.seconds : (options.retrySeconds ?? options.seconds), options.signal);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return; // the stop: the only thing that ends a sleep
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** Why npm could not say what it was asked, as a clause after a colon. */
|
|
306
|
+
export function whyUnknown(failure) {
|
|
307
|
+
switch (failure.kind) {
|
|
308
|
+
case "network":
|
|
309
|
+
return texts.UPDATE_WHY_NETWORK;
|
|
310
|
+
case "permission":
|
|
311
|
+
return texts.UPDATE_WHY_PERMISSION;
|
|
312
|
+
case "other":
|
|
313
|
+
return fill(texts.UPDATE_WHY_STATUS, { status: failure.status ?? "unknown" });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* The sentence that says which instances restart with this one, or null when none does: the
|
|
318
|
+
* other loaded instances on the copy (`UpdatesHost.sharing`), null being the default instance.
|
|
319
|
+
*/
|
|
320
|
+
export function sharedCopyLine(sharing) {
|
|
321
|
+
const [only, second] = sharing;
|
|
322
|
+
if (only === undefined)
|
|
323
|
+
return null;
|
|
324
|
+
if (second === undefined) {
|
|
325
|
+
return only === null
|
|
326
|
+
? texts.UPDATE_SHARED_DEFAULT
|
|
327
|
+
: fill(texts.UPDATE_SHARED_ONE, { name: only });
|
|
328
|
+
}
|
|
329
|
+
const names = sharing.map((name) => name === null ? texts.UPDATE_DEFAULT_INSTANCE : `\`${name}\``);
|
|
330
|
+
const joined = `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
331
|
+
return fill(texts.UPDATE_SHARED_MANY, { names: joined });
|
|
332
|
+
}
|