charness 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +612 -0
- package/DEVELOPMENT.md +76 -0
- package/README.md +5 -2
- package/SETUP.md +65 -19
- package/dist/adapters/openCodeAdapter.d.ts +11 -0
- package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/openCodeAdapter.js +24 -9
- package/dist/adapters/openCodeAdapter.js.map +1 -1
- package/dist/agentLogin/agentLogin.d.ts +1 -0
- package/dist/agentLogin/agentLogin.d.ts.map +1 -1
- package/dist/agentLogin/agentLogin.js +5 -0
- package/dist/agentLogin/agentLogin.js.map +1 -1
- package/dist/bot.d.ts +0 -3
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +311 -159
- package/dist/bot.js.map +1 -1
- package/dist/changelogEntries.d.ts +25 -0
- package/dist/changelogEntries.d.ts.map +1 -0
- package/dist/changelogEntries.js +66 -0
- package/dist/changelogEntries.js.map +1 -0
- package/dist/cli/bot.d.ts +6 -2
- package/dist/cli/bot.d.ts.map +1 -1
- package/dist/cli/bot.js +34 -2
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/hot.d.ts +7 -0
- package/dist/cli/hot.d.ts.map +1 -1
- package/dist/cli/hot.js +53 -3
- package/dist/cli/hot.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/connectors/telegram/bindPicker.d.ts +64 -0
- package/dist/connectors/telegram/bindPicker.d.ts.map +1 -0
- package/dist/connectors/telegram/bindPicker.js +97 -0
- package/dist/connectors/telegram/bindPicker.js.map +1 -0
- package/dist/connectors/telegram/commands/selfUpdate.d.ts +16 -0
- package/dist/connectors/telegram/commands/selfUpdate.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/selfUpdate.js +210 -0
- package/dist/connectors/telegram/commands/selfUpdate.js.map +1 -0
- package/dist/connectors/telegram/commands/stt.d.ts +1 -0
- package/dist/connectors/telegram/commands/stt.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/stt.js +5 -0
- package/dist/connectors/telegram/commands/stt.js.map +1 -1
- package/dist/connectors/telegram/commands/topicCommand.d.ts +75 -0
- package/dist/connectors/telegram/commands/topicCommand.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/topicCommand.js +391 -0
- package/dist/connectors/telegram/commands/topicCommand.js.map +1 -0
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +108 -2
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +108 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +108 -2
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +108 -2
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +108 -2
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +108 -2
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +108 -2
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +108 -2
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +108 -2
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +108 -2
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +108 -2
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +108 -2
- package/dist/i18n/zh.js.map +1 -1
- package/dist/instanceSettings.d.ts +12 -1
- package/dist/instanceSettings.d.ts.map +1 -1
- package/dist/instanceSettings.js +40 -4
- package/dist/instanceSettings.js.map +1 -1
- package/dist/selfUpdate/bootRollback.d.ts +22 -0
- package/dist/selfUpdate/bootRollback.d.ts.map +1 -0
- package/dist/selfUpdate/bootRollback.js +79 -0
- package/dist/selfUpdate/bootRollback.js.map +1 -0
- package/dist/selfUpdate/commandRunner.d.ts +21 -0
- package/dist/selfUpdate/commandRunner.d.ts.map +1 -0
- package/dist/selfUpdate/commandRunner.js +32 -0
- package/dist/selfUpdate/commandRunner.js.map +1 -0
- package/dist/selfUpdate/constants.d.ts +26 -0
- package/dist/selfUpdate/constants.d.ts.map +1 -0
- package/dist/selfUpdate/constants.js +31 -0
- package/dist/selfUpdate/constants.js.map +1 -0
- package/dist/selfUpdate/gitSource.d.ts +58 -0
- package/dist/selfUpdate/gitSource.d.ts.map +1 -0
- package/dist/selfUpdate/gitSource.js +183 -0
- package/dist/selfUpdate/gitSource.js.map +1 -0
- package/dist/selfUpdate/hostLock.d.ts +16 -0
- package/dist/selfUpdate/hostLock.d.ts.map +1 -0
- package/dist/selfUpdate/hostLock.js +101 -0
- package/dist/selfUpdate/hostLock.js.map +1 -0
- package/dist/selfUpdate/hotSupervisor.d.ts +47 -0
- package/dist/selfUpdate/hotSupervisor.d.ts.map +1 -0
- package/dist/selfUpdate/hotSupervisor.js +125 -0
- package/dist/selfUpdate/hotSupervisor.js.map +1 -0
- package/dist/selfUpdate/installKind.d.ts +45 -0
- package/dist/selfUpdate/installKind.d.ts.map +1 -0
- package/dist/selfUpdate/installKind.js +99 -0
- package/dist/selfUpdate/installKind.js.map +1 -0
- package/dist/selfUpdate/launchEnvironment.d.ts +3 -0
- package/dist/selfUpdate/launchEnvironment.d.ts.map +1 -0
- package/dist/selfUpdate/launchEnvironment.js +16 -0
- package/dist/selfUpdate/launchEnvironment.js.map +1 -0
- package/dist/selfUpdate/npmSource.d.ts +34 -0
- package/dist/selfUpdate/npmSource.d.ts.map +1 -0
- package/dist/selfUpdate/npmSource.js +107 -0
- package/dist/selfUpdate/npmSource.js.map +1 -0
- package/dist/selfUpdate/releaseNotes.d.ts +33 -0
- package/dist/selfUpdate/releaseNotes.d.ts.map +1 -0
- package/dist/selfUpdate/releaseNotes.js +115 -0
- package/dist/selfUpdate/releaseNotes.js.map +1 -0
- package/dist/selfUpdate/restart.d.ts +33 -0
- package/dist/selfUpdate/restart.d.ts.map +1 -0
- package/dist/selfUpdate/restart.js +94 -0
- package/dist/selfUpdate/restart.js.map +1 -0
- package/dist/selfUpdate/selfCheck.d.ts +20 -0
- package/dist/selfUpdate/selfCheck.d.ts.map +1 -0
- package/dist/selfUpdate/selfCheck.js +126 -0
- package/dist/selfUpdate/selfCheck.js.map +1 -0
- package/dist/selfUpdate/semver.d.ts +24 -0
- package/dist/selfUpdate/semver.d.ts.map +1 -0
- package/dist/selfUpdate/semver.js +165 -0
- package/dist/selfUpdate/semver.js.map +1 -0
- package/dist/selfUpdate/types.d.ts +51 -0
- package/dist/selfUpdate/types.d.ts.map +1 -0
- package/dist/selfUpdate/types.js +3 -0
- package/dist/selfUpdate/types.js.map +1 -0
- package/dist/selfUpdate/updateEngine.d.ts +110 -0
- package/dist/selfUpdate/updateEngine.d.ts.map +1 -0
- package/dist/selfUpdate/updateEngine.js +727 -0
- package/dist/selfUpdate/updateEngine.js.map +1 -0
- package/dist/selfUpdate/updateMessages.d.ts +84 -0
- package/dist/selfUpdate/updateMessages.d.ts.map +1 -0
- package/dist/selfUpdate/updateMessages.js +115 -0
- package/dist/selfUpdate/updateMessages.js.map +1 -0
- package/dist/selfUpdate/updateModes.d.ts +17 -0
- package/dist/selfUpdate/updateModes.d.ts.map +1 -0
- package/dist/selfUpdate/updateModes.js +14 -0
- package/dist/selfUpdate/updateModes.js.map +1 -0
- package/dist/selfUpdate/updatePending.d.ts +76 -0
- package/dist/selfUpdate/updatePending.d.ts.map +1 -0
- package/dist/selfUpdate/updatePending.js +131 -0
- package/dist/selfUpdate/updatePending.js.map +1 -0
- package/dist/selfUpdate/updatePolicy.d.ts +115 -0
- package/dist/selfUpdate/updatePolicy.d.ts.map +1 -0
- package/dist/selfUpdate/updatePolicy.js +83 -0
- package/dist/selfUpdate/updatePolicy.js.map +1 -0
- package/dist/selfUpdate/updateRuntime.d.ts +22 -0
- package/dist/selfUpdate/updateRuntime.d.ts.map +1 -0
- package/dist/selfUpdate/updateRuntime.js +148 -0
- package/dist/selfUpdate/updateRuntime.js.map +1 -0
- package/dist/startupPromptBuffer.d.ts +2 -0
- package/dist/startupPromptBuffer.d.ts.map +1 -1
- package/dist/startupPromptBuffer.js +4 -0
- package/dist/startupPromptBuffer.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +18 -0
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/updateScreenPlan.d.ts +38 -0
- package/dist/utils/updateScreenPlan.d.ts.map +1 -0
- package/dist/utils/updateScreenPlan.js +143 -0
- package/dist/utils/updateScreenPlan.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUpdateEngine = createUpdateEngine;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
|
+
const gitSource_1 = require("./gitSource");
|
|
6
|
+
const restart_1 = require("./restart");
|
|
7
|
+
const installKind_1 = require("./installKind");
|
|
8
|
+
const npmSource_1 = require("./npmSource");
|
|
9
|
+
const releaseNotes_1 = require("./releaseNotes");
|
|
10
|
+
const semver_1 = require("./semver");
|
|
11
|
+
const updatePolicy_1 = require("./updatePolicy");
|
|
12
|
+
/**
|
|
13
|
+
* The update engine: finds a new version, installs it when nobody is using the bot, restarts invisibly and says in General
|
|
14
|
+
* what happened. It decides nothing itself — `updatePolicy.ts` does — and touches nothing directly: every effect (the clock,
|
|
15
|
+
* the registry, the checkout, the process, Telegram) is a port, so the whole flow runs in a test against fakes.
|
|
16
|
+
*
|
|
17
|
+
* One engine per process. `runBootStep` is what a start does (confirm or settle the last update, announce a version change,
|
|
18
|
+
* notice a newer install on disk or a stale supervisor); `start` arms the hourly check; the idle loop installs / restarts /
|
|
19
|
+
* signals the supervisor at the first idle moment.
|
|
20
|
+
*/
|
|
21
|
+
const maxRememberedTargets = 50;
|
|
22
|
+
const maxRememberedFirstSeen = 20;
|
|
23
|
+
function getManualCommand(installKind) {
|
|
24
|
+
if (installKind.kind === 'notifyOnly')
|
|
25
|
+
return installKind.manualCommand;
|
|
26
|
+
if (installKind.kind === 'checkout')
|
|
27
|
+
return `${installKind.root}/scripts/self-update.sh`;
|
|
28
|
+
return installKind_1.npmManualCommand;
|
|
29
|
+
}
|
|
30
|
+
function limitLength(items, limit) {
|
|
31
|
+
return items.slice(-limit);
|
|
32
|
+
}
|
|
33
|
+
function createUpdateEngine(deps) {
|
|
34
|
+
let checkTimer = null;
|
|
35
|
+
let idleTimer = null;
|
|
36
|
+
let confirmTimer = null;
|
|
37
|
+
let hotDeadlineTimer = null;
|
|
38
|
+
let isCheckRunning = false;
|
|
39
|
+
let phase = 'idle';
|
|
40
|
+
let pendingInstall = null;
|
|
41
|
+
let inProgressTarget = null;
|
|
42
|
+
/** The newer install on disk (another instance's, or a manual one) this process will restart onto at its idle point. */
|
|
43
|
+
let isRestartOntoDiskWanted = false;
|
|
44
|
+
let isSupervisorReexecWanted = false;
|
|
45
|
+
let isRestartBlocked = false;
|
|
46
|
+
const canInstall = deps.installKind.kind === 'npmGlobal' || deps.installKind.kind === 'checkout';
|
|
47
|
+
/** A checkout that cannot update itself is not watched either: `/update` names the reason and the command. */
|
|
48
|
+
const isUnwatchedCheckout = deps.installKind.kind === 'notifyOnly' && deps.installKind.isCheckout;
|
|
49
|
+
function updateRecord(change) {
|
|
50
|
+
deps.state.setRecord(change({ ...deps.state.getRecord() }));
|
|
51
|
+
}
|
|
52
|
+
function rememberTarget(listName, target) {
|
|
53
|
+
updateRecord((record) => ({ ...record, [listName]: limitLength([...(record[listName] ?? []).filter((entry) => entry !== target), target], maxRememberedTargets) }));
|
|
54
|
+
}
|
|
55
|
+
async function say(message) {
|
|
56
|
+
try {
|
|
57
|
+
await deps.postToGeneral(message);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
deps.log(`[update] posting to General failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ── observing ──
|
|
64
|
+
function recordObservation(observation, isFailure) {
|
|
65
|
+
updateRecord((record) => ({
|
|
66
|
+
...record,
|
|
67
|
+
lastCheckAt: deps.now(),
|
|
68
|
+
lastResult: { at: deps.now(), observation },
|
|
69
|
+
isCheckFailing: isFailure,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
/** Remember when this bot first saw `version`; forget the versions the running one has caught up with. */
|
|
73
|
+
function noteFirstSeen(version) {
|
|
74
|
+
const record = deps.state.getRecord();
|
|
75
|
+
const firstSeen = record.firstSeenAt ?? {};
|
|
76
|
+
if (firstSeen[version] !== undefined)
|
|
77
|
+
return firstSeen[version];
|
|
78
|
+
const current = Object.fromEntries(Object.entries(firstSeen)
|
|
79
|
+
.filter(([seenVersion]) => ((0, semver_1.compareVersionTexts)(seenVersion, deps.running.version) ?? 1) > 0)
|
|
80
|
+
.slice(-maxRememberedFirstSeen));
|
|
81
|
+
deps.state.setRecord({ ...record, firstSeenAt: { ...current, [version]: deps.now() } });
|
|
82
|
+
return deps.now();
|
|
83
|
+
}
|
|
84
|
+
/** What the screen shows is what is out, also with the mode `off` (which only silences the schedule): `off` reads as `notify` there. */
|
|
85
|
+
function getEffectiveMode(isForDisplay) {
|
|
86
|
+
const mode = deps.getSettings().autoUpdate;
|
|
87
|
+
return isForDisplay && mode === 'off' ? 'notify' : mode;
|
|
88
|
+
}
|
|
89
|
+
function decideNpm(latest, isUserRequested, isForDisplay = false) {
|
|
90
|
+
const record = deps.state.getRecord();
|
|
91
|
+
return (0, updatePolicy_1.decideNpmUpdate)({
|
|
92
|
+
runningVersion: deps.running.version,
|
|
93
|
+
runningNode: deps.runningNode,
|
|
94
|
+
latest,
|
|
95
|
+
firstSeenAt: record.firstSeenAt?.[latest.version],
|
|
96
|
+
now: deps.now(),
|
|
97
|
+
mode: getEffectiveMode(isForDisplay),
|
|
98
|
+
failedTargets: record.failedTargets ?? [],
|
|
99
|
+
canInstall,
|
|
100
|
+
isUserRequested,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function decideGit(check, isUserRequested, isForDisplay = false) {
|
|
104
|
+
return (0, updatePolicy_1.decideGitUpdate)({
|
|
105
|
+
check,
|
|
106
|
+
mode: getEffectiveMode(isForDisplay),
|
|
107
|
+
failedTargets: deps.state.getRecord().failedTargets ?? [],
|
|
108
|
+
canInstall,
|
|
109
|
+
isUserRequested,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Act on what an automatic check saw: record it, announce it once, queue the install. `isManual` posts nothing. */
|
|
113
|
+
async function actOnNpm(latest, isManual) {
|
|
114
|
+
noteFirstSeen(latest.version);
|
|
115
|
+
const decision = decideNpm(latest, false);
|
|
116
|
+
if (isManual)
|
|
117
|
+
return;
|
|
118
|
+
const notified = deps.state.getRecord().notifiedTargets ?? [];
|
|
119
|
+
switch (decision.action) {
|
|
120
|
+
case 'install':
|
|
121
|
+
queueInstall({ target: latest.version, isUserRequested: false, isBreakingAllowed: false });
|
|
122
|
+
break;
|
|
123
|
+
case 'notify':
|
|
124
|
+
if (notified.includes(latest.version))
|
|
125
|
+
break;
|
|
126
|
+
rememberTarget('notifiedTargets', latest.version);
|
|
127
|
+
await say({ kind: 'notify', version: latest.version, running: deps.running.version, canInstall, manualCommand: getManualCommand(deps.installKind) });
|
|
128
|
+
break;
|
|
129
|
+
case 'offerMajor':
|
|
130
|
+
if (notified.includes(latest.version))
|
|
131
|
+
break;
|
|
132
|
+
rememberTarget('notifiedTargets', latest.version);
|
|
133
|
+
await say({ kind: 'offerMajor', version: latest.version, running: deps.running.version });
|
|
134
|
+
break;
|
|
135
|
+
case 'needsNode':
|
|
136
|
+
if (notified.includes(latest.version))
|
|
137
|
+
break;
|
|
138
|
+
rememberTarget('notifiedTargets', latest.version);
|
|
139
|
+
await say({ kind: 'needsNode', version: latest.version, required: decision.required, runningNode: deps.runningNode });
|
|
140
|
+
break;
|
|
141
|
+
case 'waitHold':
|
|
142
|
+
case 'none':
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function actOnGit(check, isManual) {
|
|
147
|
+
const decision = decideGit(check, false);
|
|
148
|
+
if (isManual)
|
|
149
|
+
return;
|
|
150
|
+
const notified = deps.state.getRecord().notifiedTargets ?? [];
|
|
151
|
+
switch (decision.action) {
|
|
152
|
+
case 'install':
|
|
153
|
+
queueInstall({ target: check.to, isUserRequested: false, isBreakingAllowed: false });
|
|
154
|
+
break;
|
|
155
|
+
case 'notify':
|
|
156
|
+
if (notified.includes(check.to))
|
|
157
|
+
break;
|
|
158
|
+
rememberTarget('notifiedTargets', check.to);
|
|
159
|
+
await say({ kind: 'notify', version: check.to, running: deps.running.commit ?? deps.running.version, canInstall, manualCommand: getManualCommand(deps.installKind) });
|
|
160
|
+
break;
|
|
161
|
+
case 'offerBreaking':
|
|
162
|
+
if (notified.includes(check.to))
|
|
163
|
+
break;
|
|
164
|
+
rememberTarget('notifiedTargets', check.to);
|
|
165
|
+
await say({ kind: 'offerBreaking', from: check.from, to: check.to, migrationGuides: decision.migrationGuides });
|
|
166
|
+
break;
|
|
167
|
+
case 'skipped':
|
|
168
|
+
case 'none':
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function runCheck(options) {
|
|
173
|
+
if (isCheckRunning)
|
|
174
|
+
return;
|
|
175
|
+
isCheckRunning = true;
|
|
176
|
+
try {
|
|
177
|
+
noteInstalledVersion();
|
|
178
|
+
if (getPendingKind() !== null)
|
|
179
|
+
armIdleLoop();
|
|
180
|
+
if (deps.installKind.kind === 'ephemeral')
|
|
181
|
+
return;
|
|
182
|
+
const isFailing = deps.state.getRecord().isCheckFailing === true;
|
|
183
|
+
try {
|
|
184
|
+
if (deps.installKind.kind === 'checkout' && deps.git) {
|
|
185
|
+
const check = await deps.git.check();
|
|
186
|
+
const observation = { kind: 'git', status: check.status, reason: check.reason, from: check.from, to: check.to, behind: check.behind, migrationGuides: check.migrationGuides };
|
|
187
|
+
if (check.status === 'skip' && check.reason === 'fetchFailed')
|
|
188
|
+
throw new npmSource_1.UpdateSourceError('the remote did not answer');
|
|
189
|
+
recordObservation(observation, false);
|
|
190
|
+
await actOnGit(check, options.isManual);
|
|
191
|
+
}
|
|
192
|
+
else if (deps.installKind.kind !== 'checkout' && !isUnwatchedCheckout && deps.npm) {
|
|
193
|
+
const latest = await deps.npm.getLatest();
|
|
194
|
+
recordObservation({ kind: 'npm', latestVersion: latest.version, engines: latest.engines, isDeprecated: latest.isDeprecated }, false);
|
|
195
|
+
await actOnNpm(latest, options.isManual);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
200
|
+
deps.log(`[update] check failed: ${reason}`);
|
|
201
|
+
recordObservation({ kind: 'error', reason }, true);
|
|
202
|
+
if (!isFailing && !options.isManual)
|
|
203
|
+
await say({ kind: 'checkFailing', reason });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
isCheckRunning = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ── scheduling ──
|
|
211
|
+
function scheduleNextCheck(delayMs) {
|
|
212
|
+
if (checkTimer !== null)
|
|
213
|
+
deps.clearTimer(checkTimer);
|
|
214
|
+
checkTimer = deps.setTimer(() => {
|
|
215
|
+
checkTimer = null;
|
|
216
|
+
void runScheduledCheck();
|
|
217
|
+
}, delayMs);
|
|
218
|
+
}
|
|
219
|
+
async function runScheduledCheck() {
|
|
220
|
+
try {
|
|
221
|
+
// A newer install on disk is no update decision, only the running process catching up with its own install, so it is
|
|
222
|
+
// noticed whatever the mode. `off` makes no registry or git traffic: the timer keeps ticking so a hand edit to `on` applies.
|
|
223
|
+
noteInstalledVersion();
|
|
224
|
+
if (deps.getSettings().autoUpdate !== 'off')
|
|
225
|
+
await runCheck({ isManual: false });
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
if (getPendingKind() !== null)
|
|
229
|
+
armIdleLoop();
|
|
230
|
+
scheduleNextCheck(constants_1.checkIntervalMs);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function getFirstCheckDelayMs() {
|
|
234
|
+
const lastCheckAt = deps.state.getRecord().lastCheckAt;
|
|
235
|
+
const sinceLastCheck = lastCheckAt === undefined ? Infinity : deps.now() - lastCheckAt;
|
|
236
|
+
return Math.max(constants_1.firstCheckDelayMs, constants_1.checkIntervalMs - sinceLastCheck);
|
|
237
|
+
}
|
|
238
|
+
function armIdleLoop() {
|
|
239
|
+
if (idleTimer !== null)
|
|
240
|
+
return;
|
|
241
|
+
idleTimer = deps.setTimer(() => {
|
|
242
|
+
idleTimer = null;
|
|
243
|
+
void runIdleStep();
|
|
244
|
+
}, constants_1.idlePollIntervalMs);
|
|
245
|
+
}
|
|
246
|
+
function queueInstall(request) {
|
|
247
|
+
if (phase === 'installing' || phase === 'restartPending')
|
|
248
|
+
return;
|
|
249
|
+
pendingInstall = request;
|
|
250
|
+
inProgressTarget = request.target;
|
|
251
|
+
phase = 'waitingForIdle';
|
|
252
|
+
armIdleLoop();
|
|
253
|
+
}
|
|
254
|
+
function getPendingKind() {
|
|
255
|
+
// In hot mode nodemon replaces the worker by itself after a pull: there is no restart to perform, only the deadline to watch.
|
|
256
|
+
if (!isRestartBlocked && ((phase === 'restartPending' && !deps.isHotMode) || isRestartOntoDiskWanted))
|
|
257
|
+
return 'restart';
|
|
258
|
+
if (pendingInstall !== null && phase === 'waitingForIdle')
|
|
259
|
+
return 'install';
|
|
260
|
+
if (isSupervisorReexecWanted)
|
|
261
|
+
return 'supervisor';
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
/** One tick of the idle loop: do whichever pending thing the bot's idleness now allows. */
|
|
265
|
+
async function runIdleStep() {
|
|
266
|
+
if (phase === 'installing')
|
|
267
|
+
return;
|
|
268
|
+
// Another instance's install (or a manual one) may have landed on disk since the last tick.
|
|
269
|
+
noteInstalledVersion();
|
|
270
|
+
const pending = getPendingKind();
|
|
271
|
+
if (pending === null)
|
|
272
|
+
return;
|
|
273
|
+
const isUserRequested = (pending === 'install' || pending === 'restart') && (pendingInstall?.isUserRequested ?? false);
|
|
274
|
+
const verdict = deps.getIdleVerdict(isUserRequested);
|
|
275
|
+
if (!verdict.isIdle) {
|
|
276
|
+
armIdleLoop();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (pending === 'install' && pendingInstall) {
|
|
280
|
+
await performInstall(pendingInstall);
|
|
281
|
+
// The install took minutes: whether the bot is STILL idle is asked at once — not a poll interval later.
|
|
282
|
+
if (phase === 'restartPending' && !deps.isHotMode) {
|
|
283
|
+
await runIdleStep();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else if (pending === 'restart') {
|
|
288
|
+
await performRestart();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
await performSupervisorReexec();
|
|
292
|
+
}
|
|
293
|
+
if (getPendingKind() !== null)
|
|
294
|
+
armIdleLoop();
|
|
295
|
+
}
|
|
296
|
+
function noteInstalledVersion() {
|
|
297
|
+
// A root-owned prefix is updated by hand (`sudo npm i -g`): the restart onto it is the bot's to do, the install is not.
|
|
298
|
+
const isNpmInstall = deps.installKind.kind === 'npmGlobal' || (deps.installKind.kind === 'notifyOnly' && deps.installKind.reason === 'readOnlyPrefix');
|
|
299
|
+
if (!isNpmInstall)
|
|
300
|
+
return;
|
|
301
|
+
const installed = deps.readInstalledVersion();
|
|
302
|
+
if (installed !== null && ((0, semver_1.compareVersionTexts)(installed, deps.running.version) ?? 0) > 0)
|
|
303
|
+
isRestartOntoDiskWanted = true;
|
|
304
|
+
}
|
|
305
|
+
// ── installing ──
|
|
306
|
+
function createMarker(kind, to, isUserRequested) {
|
|
307
|
+
return { kind, status: 'pending', from: deps.running, to, attempts: 0, isUserRequested };
|
|
308
|
+
}
|
|
309
|
+
/** The install failed or did not start: the target is never retried by itself, one line says so. */
|
|
310
|
+
async function failInstall(target, reason, detail, rollbackFailure) {
|
|
311
|
+
deps.log(`[update] ${target} failed (${reason}): ${detail}${rollbackFailure ? `; going back failed: ${rollbackFailure}` : ''}`);
|
|
312
|
+
deps.marker.clear();
|
|
313
|
+
rememberTarget('failedTargets', target);
|
|
314
|
+
pendingInstall = null;
|
|
315
|
+
inProgressTarget = null;
|
|
316
|
+
phase = 'idle';
|
|
317
|
+
if (rollbackFailure !== null) {
|
|
318
|
+
await say({ kind: 'rollbackFailed', target, reason, manualCommand: getManualCommand(deps.installKind) });
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
await say({ kind: 'installFailed', target, reason, stillRunning: deps.running.commit ?? deps.running.version, manualCommand: getManualCommand(deps.installKind) });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function installNpm(request) {
|
|
325
|
+
if (!deps.npm || deps.installKind.kind !== 'npmGlobal')
|
|
326
|
+
return;
|
|
327
|
+
const lock = deps.hostLock?.acquire() ?? null;
|
|
328
|
+
if (lock === null) {
|
|
329
|
+
// Another instance on this install is installing: we will find the new version on disk and restart onto it.
|
|
330
|
+
deps.log('[update] another instance holds the install lock — waiting for it to finish');
|
|
331
|
+
phase = 'waitingForIdle';
|
|
332
|
+
armIdleLoop();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
phase = 'installing';
|
|
336
|
+
try {
|
|
337
|
+
try {
|
|
338
|
+
// An operator's own `npm i -g` may already have put the target there: it is checked and restarted onto, not installed twice.
|
|
339
|
+
if (deps.readInstalledVersion() !== request.target)
|
|
340
|
+
await deps.npm.install(request.target);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
await failInstall(request.target, 'install', error instanceof Error ? error.message : String(error), null);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const check = await deps.runSelfCheck(request.target);
|
|
347
|
+
if (!check.ok) {
|
|
348
|
+
let rollbackFailure = null;
|
|
349
|
+
try {
|
|
350
|
+
await deps.npm.install(deps.running.version, { isRollback: true });
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
rollbackFailure = error instanceof Error ? error.message : String(error);
|
|
354
|
+
}
|
|
355
|
+
await failInstall(request.target, 'start', check.detail, rollbackFailure);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
deps.marker.write(createMarker('npm', { version: request.target, commit: null }, request.isUserRequested));
|
|
359
|
+
phase = 'restartPending';
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
lock.release();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function installGit(request) {
|
|
366
|
+
if (!deps.git || deps.installKind.kind !== 'checkout')
|
|
367
|
+
return;
|
|
368
|
+
phase = 'installing';
|
|
369
|
+
deps.marker.write(createMarker('git', { version: deps.running.version, commit: request.target }, request.isUserRequested));
|
|
370
|
+
let result;
|
|
371
|
+
try {
|
|
372
|
+
result = await deps.git.apply({ holdBreaking: !request.isBreakingAllowed });
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
// The script may have fast-forwarded before its install or build failed: put the checkout back on the commit that ran.
|
|
376
|
+
// Resetting to where it already is changes nothing, so a failure before the pull needs no special case.
|
|
377
|
+
let rollbackFailure = null;
|
|
378
|
+
if (deps.running.commit !== null) {
|
|
379
|
+
try {
|
|
380
|
+
await deps.git.rollback({ from: deps.running.commit, to: request.target, isHot: deps.isHotMode });
|
|
381
|
+
}
|
|
382
|
+
catch (rollbackError) {
|
|
383
|
+
rollbackFailure = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
await failInstall(request.target, 'install', error instanceof Error ? error.message : String(error), rollbackFailure);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (result.status === 'held') {
|
|
390
|
+
deps.marker.clear();
|
|
391
|
+
phase = 'idle';
|
|
392
|
+
pendingInstall = null;
|
|
393
|
+
inProgressTarget = null;
|
|
394
|
+
const notified = deps.state.getRecord().notifiedTargets ?? [];
|
|
395
|
+
if (!notified.includes(result.to)) {
|
|
396
|
+
rememberTarget('notifiedTargets', result.to);
|
|
397
|
+
await say({ kind: 'offerBreaking', from: result.from, to: result.to, migrationGuides: result.migrationGuides });
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (result.status !== 'updated') {
|
|
402
|
+
deps.marker.clear();
|
|
403
|
+
phase = 'idle';
|
|
404
|
+
pendingInstall = null;
|
|
405
|
+
inProgressTarget = null;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const marker = createMarker('git', { version: deps.running.version, commit: result.to }, request.isUserRequested);
|
|
409
|
+
deps.marker.write({ ...marker, from: { version: deps.running.version, commit: result.from } });
|
|
410
|
+
if (deps.isHotMode) {
|
|
411
|
+
// The watcher rebuilds and nodemon replaces this worker by itself. If no new worker has replaced us by the deadline,
|
|
412
|
+
// the build is taken as failed and the checkout goes back.
|
|
413
|
+
phase = 'restartPending';
|
|
414
|
+
await requestHotWorkerReloadIfNoRebuild(deps.git, result.from, result.to);
|
|
415
|
+
hotDeadlineTimer = deps.setTimer(() => {
|
|
416
|
+
hotDeadlineTimer = null;
|
|
417
|
+
void rollBackHotBuild(result.from, result.to);
|
|
418
|
+
}, constants_1.hotBuildDeadlineMs);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const check = await deps.runSelfCheck(null);
|
|
422
|
+
if (!check.ok) {
|
|
423
|
+
let rollbackFailure = null;
|
|
424
|
+
try {
|
|
425
|
+
await deps.git.rollback({ from: result.from, to: result.to, isHot: false });
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
rollbackFailure = error instanceof Error ? error.message : String(error);
|
|
429
|
+
}
|
|
430
|
+
await failInstall(result.to, 'start', check.detail, rollbackFailure);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
phase = 'restartPending';
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* nodemon watches only `dist/`, and `tsc -w` writes there only for a compiled source: a pull of docs, the CHANGELOG, scripts or
|
|
437
|
+
* the manifests rebuilds nothing, so no worker would replace this one — and the deadline would take that for a failed build and
|
|
438
|
+
* put the checkout back. Such a pull asks for the reload itself, so the new worker announces the update like any other. A range
|
|
439
|
+
* that cannot be read is treated the same way: a reload nobody needed is harmless, a rollback of a good pull is not.
|
|
440
|
+
*/
|
|
441
|
+
async function requestHotWorkerReloadIfNoRebuild(git, from, to) {
|
|
442
|
+
let isRebuildExpected = false;
|
|
443
|
+
try {
|
|
444
|
+
isRebuildExpected = (0, gitSource_1.checkChangesEmittedSource)(await git.getChangedFiles(from, to));
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
deps.log(`[update] cannot list what ${to} changed (${error instanceof Error ? error.message : String(error)}): asking the worker to reload`);
|
|
448
|
+
}
|
|
449
|
+
if (isRebuildExpected)
|
|
450
|
+
return;
|
|
451
|
+
try {
|
|
452
|
+
await git.reloadHotWorker();
|
|
453
|
+
deps.log(`[update] ${to}: no compiled source changed, so no rebuild would replace the worker — asked it to reload`);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
deps.log(`[update] ${error instanceof Error ? error.message : String(error)}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function rollBackHotBuild(from, to) {
|
|
460
|
+
if (!deps.git)
|
|
461
|
+
return;
|
|
462
|
+
let rollbackFailure = null;
|
|
463
|
+
try {
|
|
464
|
+
await deps.git.rollback({ from, to, isHot: true });
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
rollbackFailure = error instanceof Error ? error.message : String(error);
|
|
468
|
+
}
|
|
469
|
+
await failInstall(to, 'build', 'no new worker started after the pull', rollbackFailure);
|
|
470
|
+
}
|
|
471
|
+
async function performInstall(request) {
|
|
472
|
+
if (deps.installKind.kind === 'checkout')
|
|
473
|
+
await installGit(request);
|
|
474
|
+
else
|
|
475
|
+
await installNpm(request);
|
|
476
|
+
}
|
|
477
|
+
async function performRestart() {
|
|
478
|
+
const problems = (0, restart_1.getReexecProblems)(deps.reexecTarget);
|
|
479
|
+
if (problems.length > 0) {
|
|
480
|
+
isRestartBlocked = true;
|
|
481
|
+
deps.log(`[update] cannot restart: ${problems.join('; ')}`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (phase !== 'restartPending') {
|
|
485
|
+
// A newer install already on disk (a manual `npm i -g`, another instance's install): probed first, like one this bot
|
|
486
|
+
// made — a broken one is not restarted onto — and the restart gets the same marker, so the new boot says "Updated" and a
|
|
487
|
+
// version that cannot start is rolled back.
|
|
488
|
+
const installed = deps.readInstalledVersion();
|
|
489
|
+
if (installed !== null) {
|
|
490
|
+
const check = await deps.runSelfCheck(installed);
|
|
491
|
+
if (!check.ok) {
|
|
492
|
+
isRestartBlocked = true;
|
|
493
|
+
deps.log(`[update] the install on disk (${installed}) fails its self-check, not restarting onto it: ${check.detail}`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
deps.marker.write(createMarker('npm', { version: installed, commit: null }, pendingInstall?.isUserRequested ?? false));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
phase = 'restartPending';
|
|
500
|
+
try {
|
|
501
|
+
await deps.restartInPlace();
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
isRestartBlocked = true;
|
|
505
|
+
deps.log(`[update] restart failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function performSupervisorReexec() {
|
|
509
|
+
const record = deps.supervisor.readRecord();
|
|
510
|
+
if (record === null) {
|
|
511
|
+
isSupervisorReexecWanted = false;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const result = deps.supervisor.requestReexec(record);
|
|
515
|
+
isSupervisorReexecWanted = false;
|
|
516
|
+
deps.log(`[update] supervisor re-exec requested: ${result}`);
|
|
517
|
+
// The re-exec ends this boot ON PURPOSE (and runs synchronously before the supervisor's signal reaches this worker): it is not a
|
|
518
|
+
// start that failed, so it must not use up the boots the update may fail — a restart soon after would roll a healthy version back.
|
|
519
|
+
if (result === 'signaled')
|
|
520
|
+
releaseBootAttempt();
|
|
521
|
+
}
|
|
522
|
+
function releaseBootAttempt() {
|
|
523
|
+
const marker = deps.marker.read();
|
|
524
|
+
if (marker?.status === 'pending' && marker.attempts > 0)
|
|
525
|
+
deps.marker.write({ ...marker, attempts: marker.attempts - 1 });
|
|
526
|
+
}
|
|
527
|
+
// ── booting ──
|
|
528
|
+
function scheduleBootConfirmation() {
|
|
529
|
+
confirmTimer = deps.setTimer(() => {
|
|
530
|
+
confirmTimer = null;
|
|
531
|
+
const marker = deps.marker.read();
|
|
532
|
+
if (marker?.status === 'pending') {
|
|
533
|
+
deps.marker.clear();
|
|
534
|
+
deps.log('[update] the new version is confirmed');
|
|
535
|
+
}
|
|
536
|
+
}, constants_1.bootConfirmMs);
|
|
537
|
+
}
|
|
538
|
+
async function getNotes(from, to) {
|
|
539
|
+
if (!deps.getSettings().whatsNew)
|
|
540
|
+
return { summary: null, commitCount: await getCommitCount(from, to) };
|
|
541
|
+
if (to.commit !== null && from.commit !== null && deps.git) {
|
|
542
|
+
try {
|
|
543
|
+
return { summary: (0, releaseNotes_1.getCheckoutReleaseNotes)(await deps.git.getChangelogDiff(from.commit, to.commit)), commitCount: await getCommitCount(from, to) };
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
deps.log(`[update] no what's-new list: ${error instanceof Error ? error.message : String(error)}`);
|
|
547
|
+
return { summary: null, commitCount: null };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const changelog = deps.readInstalledChangelog();
|
|
551
|
+
return { summary: changelog === null ? null : (0, releaseNotes_1.getNpmReleaseNotes)(changelog, from.version, to.version), commitCount: null };
|
|
552
|
+
}
|
|
553
|
+
async function getCommitCount(from, to) {
|
|
554
|
+
return from.commit !== null && to.commit !== null ? deps.countCommits(from.commit, to.commit) : null;
|
|
555
|
+
}
|
|
556
|
+
function describeVersionPoint(point, isCheckout) {
|
|
557
|
+
return isCheckout && point.commit !== null ? point.commit : point.version;
|
|
558
|
+
}
|
|
559
|
+
async function announceVersionChange(lastRun, marker) {
|
|
560
|
+
const isCheckout = deps.running.commit !== null;
|
|
561
|
+
const { summary, commitCount } = await getNotes(lastRun, deps.running);
|
|
562
|
+
const from = describeVersionPoint(lastRun, isCheckout);
|
|
563
|
+
const to = describeVersionPoint(deps.running, isCheckout);
|
|
564
|
+
// A bot that updated itself ALWAYS says so; a version change someone else made says only what changed, when asked to.
|
|
565
|
+
const isSelfUpdate = marker !== null && marker.status === 'pending';
|
|
566
|
+
if (isSelfUpdate) {
|
|
567
|
+
await say({ kind: 'updated', from, to, commitCount, summary });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (summary !== null)
|
|
571
|
+
await say({ kind: 'nowRunning', from, to, summary });
|
|
572
|
+
}
|
|
573
|
+
async function settleFailedUpdate(marker) {
|
|
574
|
+
const target = marker.to.commit ?? marker.to.version;
|
|
575
|
+
rememberTarget('failedTargets', target);
|
|
576
|
+
deps.marker.clear();
|
|
577
|
+
const reason = 'boot';
|
|
578
|
+
if (marker.status === 'rollbackFailed') {
|
|
579
|
+
await say({ kind: 'rollbackFailed', target, reason, manualCommand: getManualCommand(deps.installKind) });
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
await say({ kind: 'installFailed', target, reason, stillRunning: deps.running.commit ?? deps.running.version, manualCommand: getManualCommand(deps.installKind) });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function runBootStep() {
|
|
586
|
+
const marker = deps.marker.read();
|
|
587
|
+
if (marker !== null && marker.status !== 'pending') {
|
|
588
|
+
await settleFailedUpdate(marker);
|
|
589
|
+
}
|
|
590
|
+
else if (marker !== null) {
|
|
591
|
+
scheduleBootConfirmation();
|
|
592
|
+
}
|
|
593
|
+
const lastRun = deps.state.getLastRun();
|
|
594
|
+
const isFirstRun = lastRun === undefined;
|
|
595
|
+
const hasChanged = lastRun !== undefined && (lastRun.version !== deps.running.version || lastRun.commit !== deps.running.commit);
|
|
596
|
+
await deps.state.setLastRun(deps.running);
|
|
597
|
+
if (hasChanged && marker?.status !== 'rolledBack' && marker?.status !== 'rollbackFailed') {
|
|
598
|
+
await announceVersionChange(lastRun, marker);
|
|
599
|
+
}
|
|
600
|
+
const settings = deps.getSettings();
|
|
601
|
+
const record = deps.state.getRecord();
|
|
602
|
+
if (isFirstRun && !record.isFeatureAnnounced && settings.autoUpdate !== 'off') {
|
|
603
|
+
updateRecord((current) => ({ ...current, isFeatureAnnounced: true }));
|
|
604
|
+
await say({ kind: 'featureAnnounced', mode: settings.autoUpdate });
|
|
605
|
+
}
|
|
606
|
+
noteInstalledVersion();
|
|
607
|
+
await noteSupervisor(hasChanged);
|
|
608
|
+
if (getPendingKind() !== null)
|
|
609
|
+
armIdleLoop();
|
|
610
|
+
}
|
|
611
|
+
/** A hot supervisor that loaded a file the update changed must be replaced; an old one cannot be, and says so once. */
|
|
612
|
+
async function noteSupervisor(hasVersionChanged) {
|
|
613
|
+
if (!deps.isHotMode)
|
|
614
|
+
return;
|
|
615
|
+
const record = deps.supervisor.readRecord();
|
|
616
|
+
if (record === null) {
|
|
617
|
+
if (hasVersionChanged)
|
|
618
|
+
await say({ kind: 'restartSupervisor' });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (deps.supervisor.getStaleModules(record).length > 0)
|
|
622
|
+
isSupervisorReexecWanted = true;
|
|
623
|
+
}
|
|
624
|
+
// ── the screen ──
|
|
625
|
+
function getOffer() {
|
|
626
|
+
const observation = deps.state.getRecord().lastResult?.observation;
|
|
627
|
+
if (observation?.kind === 'npm') {
|
|
628
|
+
const latest = { version: observation.latestVersion, engines: observation.engines, isDeprecated: observation.isDeprecated };
|
|
629
|
+
// What a TAP could do now (no hold, a major allowed) decides the button — a version this Node cannot run has none — and a
|
|
630
|
+
// Node that is too old is said at once, not after the hold that only delays what the bot itself would announce.
|
|
631
|
+
const tapDecision = decideNpm(latest, true, true);
|
|
632
|
+
const base = { target: latest.version, until: null, required: null, skipReason: null, canInstallNow: canInstall && tapDecision.action === 'install' };
|
|
633
|
+
if (tapDecision.action === 'needsNode')
|
|
634
|
+
return { ...base, action: 'needsNode', required: tapDecision.required, canInstallNow: false };
|
|
635
|
+
const decision = decideNpm(latest, false, true);
|
|
636
|
+
const isMajor = ((0, semver_1.parseSemVer)(latest.version)?.major ?? 0) > ((0, semver_1.parseSemVer)(deps.running.version)?.major ?? 0);
|
|
637
|
+
switch (decision.action) {
|
|
638
|
+
case 'none':
|
|
639
|
+
return null;
|
|
640
|
+
case 'waitHold':
|
|
641
|
+
// A major is never installed by itself, hold or not: say that, not "will install by itself".
|
|
642
|
+
return isMajor ? { ...base, action: 'offerMajor' } : { ...base, action: 'waitHold', until: decision.until };
|
|
643
|
+
case 'install':
|
|
644
|
+
return { ...base, action: 'installWhenIdle' };
|
|
645
|
+
case 'notify':
|
|
646
|
+
return { ...base, action: 'notify' };
|
|
647
|
+
case 'offerMajor':
|
|
648
|
+
return { ...base, action: 'offerMajor' };
|
|
649
|
+
case 'needsNode':
|
|
650
|
+
return { ...base, action: 'needsNode', required: decision.required, canInstallNow: false };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (observation?.kind === 'git') {
|
|
654
|
+
const check = { status: observation.status, reason: observation.reason, from: observation.from, to: observation.to, behind: observation.behind, migrationGuides: observation.migrationGuides };
|
|
655
|
+
const decision = decideGit(check, false, true);
|
|
656
|
+
const base = { target: check.to, until: null, required: null, skipReason: null, canInstallNow: canInstall };
|
|
657
|
+
switch (decision.action) {
|
|
658
|
+
case 'none':
|
|
659
|
+
return null;
|
|
660
|
+
case 'skipped':
|
|
661
|
+
return { ...base, action: 'skipped', skipReason: decision.reason, canInstallNow: false };
|
|
662
|
+
case 'install':
|
|
663
|
+
return { ...base, action: 'installWhenIdle' };
|
|
664
|
+
case 'notify':
|
|
665
|
+
return { ...base, action: 'notify' };
|
|
666
|
+
case 'offerBreaking':
|
|
667
|
+
return { ...base, action: 'offerBreaking' };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
function getStatus() {
|
|
673
|
+
const record = deps.state.getRecord();
|
|
674
|
+
return {
|
|
675
|
+
running: deps.running,
|
|
676
|
+
installKind: deps.installKind,
|
|
677
|
+
isHotMode: deps.isHotMode,
|
|
678
|
+
runningNode: deps.runningNode,
|
|
679
|
+
settings: deps.getSettings(),
|
|
680
|
+
lastCheckAt: record.lastCheckAt ?? null,
|
|
681
|
+
observation: record.lastResult?.observation ?? null,
|
|
682
|
+
isCheckFailing: record.isCheckFailing === true,
|
|
683
|
+
offer: getOffer(),
|
|
684
|
+
phase,
|
|
685
|
+
inProgressTarget,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
start() {
|
|
690
|
+
scheduleNextCheck(getFirstCheckDelayMs());
|
|
691
|
+
},
|
|
692
|
+
stop() {
|
|
693
|
+
for (const timer of [checkTimer, idleTimer, confirmTimer, hotDeadlineTimer])
|
|
694
|
+
if (timer !== null)
|
|
695
|
+
deps.clearTimer(timer);
|
|
696
|
+
checkTimer = idleTimer = confirmTimer = hotDeadlineTimer = null;
|
|
697
|
+
},
|
|
698
|
+
runBootStep,
|
|
699
|
+
async checkNow() {
|
|
700
|
+
await runCheck({ isManual: true });
|
|
701
|
+
return getStatus();
|
|
702
|
+
},
|
|
703
|
+
async requestInstall(target) {
|
|
704
|
+
const offer = getOffer();
|
|
705
|
+
if (phase === 'installing' || phase === 'restartPending')
|
|
706
|
+
return 'busy';
|
|
707
|
+
if (!canInstall)
|
|
708
|
+
return 'cannotInstall';
|
|
709
|
+
if (offer === null)
|
|
710
|
+
return 'alreadyUpToDate';
|
|
711
|
+
if (!offer.canInstallNow)
|
|
712
|
+
return 'blocked';
|
|
713
|
+
if (offer.target !== target)
|
|
714
|
+
return 'alreadyUpToDate';
|
|
715
|
+
queueInstall({ target, isUserRequested: true, isBreakingAllowed: true });
|
|
716
|
+
// Do not wait a poll interval for an idle bot: try at once.
|
|
717
|
+
if (idleTimer !== null) {
|
|
718
|
+
deps.clearTimer(idleTimer);
|
|
719
|
+
idleTimer = null;
|
|
720
|
+
}
|
|
721
|
+
void runIdleStep();
|
|
722
|
+
return 'started';
|
|
723
|
+
},
|
|
724
|
+
getStatus,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
//# sourceMappingURL=updateEngine.js.map
|