@aragon-agent/cli 0.5.9 → 0.5.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 +1157 -1026
- package/README.md +123 -0
- package/dist/boot/guard.d.ts +135 -0
- package/dist/boot/guard.js +218 -0
- package/dist/boot/rollback.d.ts +86 -0
- package/dist/boot/rollback.js +185 -0
- package/dist/cli.js +283 -2
- package/dist/commands/builtins.js +118 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +31 -0
- package/dist/config/app-paths.js +37 -0
- package/dist/config/cli-commands.d.ts +27 -1
- package/dist/config/cli-commands.js +51 -2
- package/dist/config/env.d.ts +10 -1
- package/dist/config/env.js +41 -1
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +25 -1
- package/dist/config/schema.d.ts +56 -0
- package/dist/config/schema.js +81 -0
- package/dist/config/store.js +13 -1
- package/dist/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +88 -2
- package/dist/ui/BottomStatusRow.d.ts +25 -3
- package/dist/ui/BottomStatusRow.js +10 -7
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/update/classify-failure.d.ts +32 -0
- package/dist/update/classify-failure.js +60 -0
- package/dist/update/install-lock.d.ts +55 -0
- package/dist/update/install-lock.js +184 -0
- package/dist/update/install-source.d.ts +97 -0
- package/dist/update/install-source.js +218 -0
- package/dist/update/installer.d.ts +108 -0
- package/dist/update/installer.js +242 -0
- package/dist/update/limits.d.ts +111 -0
- package/dist/update/limits.js +111 -0
- package/dist/update/npm-view.d.ts +62 -0
- package/dist/update/npm-view.js +133 -0
- package/dist/update/registry.d.ts +52 -0
- package/dist/update/registry.js +161 -0
- package/dist/update/semver.d.ts +89 -0
- package/dist/update/semver.js +295 -0
- package/dist/update/service.d.ts +193 -0
- package/dist/update/service.js +636 -0
- package/dist/update/state.d.ts +103 -0
- package/dist/update/state.js +142 -0
- package/dist/update/types.d.ts +169 -0
- package/dist/update/types.js +68 -0
- package/package.json +3 -3
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semver comparison and the update decision (cli-auto-update section 3.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
|
|
5
|
+
*
|
|
6
|
+
* HAND-WRITTEN AND NOT `semver`. The package has no such dependency
|
|
7
|
+
* (`package.json`), and this feature needs three questions answered - "is B
|
|
8
|
+
* newer than A", "is B a prerelease", "does this Node satisfy that range" - on a
|
|
9
|
+
* document the registry writes and npm itself only warns about. Adding a
|
|
10
|
+
* transitive dependency to an auto-updater, of all things, is the one place the
|
|
11
|
+
* supply-chain argument in R-1 cuts hardest.
|
|
12
|
+
*
|
|
13
|
+
* PURE: no I/O, no clock, no process. Every function here is total and none of
|
|
14
|
+
* them throws.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The form a version must have before it is interpolated into `npm install`'s
|
|
18
|
+
* argv (section 3.5 / AC-24).
|
|
19
|
+
*
|
|
20
|
+
* ANCHORED AND WITHOUT `v`: a registry that answered `"1.0.0; rm -rf /"` must
|
|
21
|
+
* never reach `execFile`. With `shell: false` this is not an injection surface
|
|
22
|
+
* to begin with, but the rejection is what makes that claim checkable rather
|
|
23
|
+
* than reasoned, and it also satisfies `fetch-source.ts` rule 3 (no leading `-`)
|
|
24
|
+
* by construction.
|
|
25
|
+
*/
|
|
26
|
+
export const STRICT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
27
|
+
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
28
|
+
/** Parse a semver string, or `null` when it is not one. Never throws. */
|
|
29
|
+
export function parseSemver(raw) {
|
|
30
|
+
if (typeof raw !== 'string')
|
|
31
|
+
return null;
|
|
32
|
+
const match = SEMVER_RE.exec(raw.trim());
|
|
33
|
+
if (!match)
|
|
34
|
+
return null;
|
|
35
|
+
const [, major, minor, patch, pre] = match;
|
|
36
|
+
return {
|
|
37
|
+
major: Number.parseInt(major, 10),
|
|
38
|
+
minor: Number.parseInt(minor, 10),
|
|
39
|
+
patch: Number.parseInt(patch, 10),
|
|
40
|
+
prerelease: pre ? pre.split('.') : [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function isPrerelease(raw) {
|
|
44
|
+
return (parseSemver(raw)?.prerelease.length ?? 0) > 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compare prerelease identifier lists per semver section 11.
|
|
48
|
+
*
|
|
49
|
+
* THE EMPTY LIST SORTS HIGHEST, which is the rule that keeps `1.0.0-rc.1` below
|
|
50
|
+
* `1.0.0`. Getting this backwards would let a `latest` tag pointing at a release
|
|
51
|
+
* candidate read as newer than the release it precedes.
|
|
52
|
+
*/
|
|
53
|
+
function comparePrerelease(a, b) {
|
|
54
|
+
if (a.length === 0 && b.length === 0)
|
|
55
|
+
return 0;
|
|
56
|
+
if (a.length === 0)
|
|
57
|
+
return 1;
|
|
58
|
+
if (b.length === 0)
|
|
59
|
+
return -1;
|
|
60
|
+
const len = Math.max(a.length, b.length);
|
|
61
|
+
for (let i = 0; i < len; i += 1) {
|
|
62
|
+
const x = a[i];
|
|
63
|
+
const y = b[i];
|
|
64
|
+
if (x === undefined)
|
|
65
|
+
return -1;
|
|
66
|
+
if (y === undefined)
|
|
67
|
+
return 1;
|
|
68
|
+
const xNum = /^\d+$/.test(x);
|
|
69
|
+
const yNum = /^\d+$/.test(y);
|
|
70
|
+
if (xNum && yNum) {
|
|
71
|
+
const diff = Number.parseInt(x, 10) - Number.parseInt(y, 10);
|
|
72
|
+
if (diff !== 0)
|
|
73
|
+
return diff < 0 ? -1 : 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Numeric identifiers always sort below alphanumeric ones.
|
|
77
|
+
if (xNum !== yNum)
|
|
78
|
+
return xNum ? -1 : 1;
|
|
79
|
+
if (x !== y)
|
|
80
|
+
return x < y ? -1 : 1;
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `-1 | 0 | 1`. An UNPARSEABLE version sorts BELOW a parseable one and equal to
|
|
86
|
+
* another unparseable one, so a garbage registry answer can never read as newer
|
|
87
|
+
* than what is installed.
|
|
88
|
+
*/
|
|
89
|
+
export function compareSemver(a, b) {
|
|
90
|
+
const left = parseSemver(a);
|
|
91
|
+
const right = parseSemver(b);
|
|
92
|
+
if (!left && !right)
|
|
93
|
+
return 0;
|
|
94
|
+
if (!left)
|
|
95
|
+
return -1;
|
|
96
|
+
if (!right)
|
|
97
|
+
return 1;
|
|
98
|
+
if (left.major !== right.major)
|
|
99
|
+
return left.major < right.major ? -1 : 1;
|
|
100
|
+
if (left.minor !== right.minor)
|
|
101
|
+
return left.minor < right.minor ? -1 : 1;
|
|
102
|
+
if (left.patch !== right.patch)
|
|
103
|
+
return left.patch < right.patch ? -1 : 1;
|
|
104
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
105
|
+
}
|
|
106
|
+
function parseClause(raw) {
|
|
107
|
+
const text = raw.trim();
|
|
108
|
+
if (text.length === 0)
|
|
109
|
+
return null;
|
|
110
|
+
const ops = [
|
|
111
|
+
['>=', '>='],
|
|
112
|
+
['<=', '<='],
|
|
113
|
+
['>', '>'],
|
|
114
|
+
['<', '<'],
|
|
115
|
+
['^', '^'],
|
|
116
|
+
['~', '~'],
|
|
117
|
+
['=', '='],
|
|
118
|
+
];
|
|
119
|
+
for (const [prefix, op] of ops) {
|
|
120
|
+
if (text.startsWith(prefix)) {
|
|
121
|
+
const rest = text.slice(prefix.length).trim();
|
|
122
|
+
const parsed = parsePartial(rest);
|
|
123
|
+
return parsed ? { op, ...parsed } : null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const parsed = parsePartial(text);
|
|
127
|
+
return parsed ? { op: '=', ...parsed } : null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `18`, `18.x`, `18.19`, `18.19.0` - the partial forms `engines.node` uses in
|
|
131
|
+
* practice. Missing components read as `0`, which is what makes `>=18` mean
|
|
132
|
+
* `>=18.0.0`, and `specified` records how many were really named.
|
|
133
|
+
*/
|
|
134
|
+
function parsePartial(raw) {
|
|
135
|
+
const text = raw.trim().replace(/^v/, '');
|
|
136
|
+
if (text.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const parts = text.split('.');
|
|
139
|
+
if (parts.length > 3)
|
|
140
|
+
return null;
|
|
141
|
+
const nums = [];
|
|
142
|
+
let specified = 0;
|
|
143
|
+
let sawWildcard = false;
|
|
144
|
+
for (const part of parts) {
|
|
145
|
+
if (part === 'x' || part === 'X' || part === '*') {
|
|
146
|
+
sawWildcard = true;
|
|
147
|
+
nums.push(0);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!/^\d+$/.test(part))
|
|
151
|
+
return null;
|
|
152
|
+
nums.push(Number.parseInt(part, 10));
|
|
153
|
+
// A concrete component AFTER a wildcard (`18.x.3`) names nothing further:
|
|
154
|
+
// the wildcard has already made everything below it free.
|
|
155
|
+
if (!sawWildcard)
|
|
156
|
+
specified += 1;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
version: {
|
|
160
|
+
major: nums[0] ?? 0,
|
|
161
|
+
minor: nums[1] ?? 0,
|
|
162
|
+
patch: nums[2] ?? 0,
|
|
163
|
+
prerelease: [],
|
|
164
|
+
},
|
|
165
|
+
specified,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function compareParsed(a, b) {
|
|
169
|
+
if (a.major !== b.major)
|
|
170
|
+
return a.major < b.major ? -1 : 1;
|
|
171
|
+
if (a.minor !== b.minor)
|
|
172
|
+
return a.minor < b.minor ? -1 : 1;
|
|
173
|
+
if (a.patch !== b.patch)
|
|
174
|
+
return a.patch < b.patch ? -1 : 1;
|
|
175
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
176
|
+
}
|
|
177
|
+
function satisfiesClause(actual, clause) {
|
|
178
|
+
const cmp = compareParsed(actual, clause.version);
|
|
179
|
+
switch (clause.op) {
|
|
180
|
+
case '>=':
|
|
181
|
+
return cmp >= 0;
|
|
182
|
+
case '>':
|
|
183
|
+
return cmp > 0;
|
|
184
|
+
case '<=':
|
|
185
|
+
return cmp <= 0;
|
|
186
|
+
case '<':
|
|
187
|
+
return cmp < 0;
|
|
188
|
+
case '=':
|
|
189
|
+
// A BARE PARTIAL IS A RANGE, NOT AN EQUALITY. `18` and `18.x` mean "any
|
|
190
|
+
// 18.y.z"; only a fully-specified `18.19.0` means that exact version.
|
|
191
|
+
if (clause.specified === 0)
|
|
192
|
+
return true;
|
|
193
|
+
if (actual.major !== clause.version.major)
|
|
194
|
+
return false;
|
|
195
|
+
if (clause.specified === 1)
|
|
196
|
+
return true;
|
|
197
|
+
if (actual.minor !== clause.version.minor)
|
|
198
|
+
return false;
|
|
199
|
+
if (clause.specified === 2)
|
|
200
|
+
return true;
|
|
201
|
+
return actual.patch === clause.version.patch;
|
|
202
|
+
case '^':
|
|
203
|
+
return cmp >= 0 && actual.major === clause.version.major;
|
|
204
|
+
case '~':
|
|
205
|
+
return (cmp >= 0 &&
|
|
206
|
+
actual.major === clause.version.major &&
|
|
207
|
+
actual.minor === clause.version.minor);
|
|
208
|
+
default:
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Does `nodeVersion` satisfy `engines.node`?
|
|
214
|
+
*
|
|
215
|
+
* FAILS OPEN, and that is a decision rather than an oversight (D-15). Handled
|
|
216
|
+
* forms are `>=X`, `>=X.Y.Z`, `>X`, `<X`, `<=X`, `=X`, `^X`, `~X`, `X.x`, and
|
|
217
|
+
* `||`-joined unions of those, with space-separated clauses inside a union
|
|
218
|
+
* ANDed together. Anything this cannot parse returns `true`, matching npm's own
|
|
219
|
+
* default of warning rather than refusing.
|
|
220
|
+
*
|
|
221
|
+
* Failing CLOSED here would mean a syntax we did not anticipate silently freezes
|
|
222
|
+
* every user's updates forever, with no message and no way to find out - the
|
|
223
|
+
* worse of the two errors by a wide margin. A range we DID understand and that
|
|
224
|
+
* genuinely excludes this Node yields `false`, and the caller turns that into a
|
|
225
|
+
* `node-too-old` notice so the user is told to upgrade Node rather than left
|
|
226
|
+
* wondering.
|
|
227
|
+
*/
|
|
228
|
+
export function satisfiesNodeRange(range, nodeVersion) {
|
|
229
|
+
if (range === undefined)
|
|
230
|
+
return true;
|
|
231
|
+
const text = range.trim();
|
|
232
|
+
if (text.length === 0 || text === '*')
|
|
233
|
+
return true;
|
|
234
|
+
const actual = parseSemver(nodeVersion);
|
|
235
|
+
if (!actual)
|
|
236
|
+
return true;
|
|
237
|
+
for (const union of text.split('||')) {
|
|
238
|
+
const clauses = union.trim().split(/\s+/).filter((c) => c.length > 0);
|
|
239
|
+
if (clauses.length === 0)
|
|
240
|
+
continue;
|
|
241
|
+
let allParsed = true;
|
|
242
|
+
let allSatisfied = true;
|
|
243
|
+
for (const raw of clauses) {
|
|
244
|
+
const clause = parseClause(raw);
|
|
245
|
+
if (!clause) {
|
|
246
|
+
allParsed = false;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
if (!satisfiesClause(actual, clause))
|
|
250
|
+
allSatisfied = false;
|
|
251
|
+
}
|
|
252
|
+
// An unparseable clause makes the WHOLE range unknown, not just this union:
|
|
253
|
+
// a partially-understood range is exactly the input that would make a
|
|
254
|
+
// fail-open guard silently strict.
|
|
255
|
+
if (!allParsed)
|
|
256
|
+
return true;
|
|
257
|
+
if (allSatisfied)
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Should we install, merely report, or say nothing? (section 3.4)
|
|
264
|
+
*
|
|
265
|
+
* ORDER IS SEMANTIC. `up-to-date` is answered first because it is the
|
|
266
|
+
* overwhelmingly common case and the cheapest; `skipped` comes before the
|
|
267
|
+
* quality gates so a user who typed `/update skip` is not told about a
|
|
268
|
+
* prerelease they already declined. Only a range we UNDERSTOOD and that excludes
|
|
269
|
+
* this Node produces `notify`; every other rejection is silent (`none`).
|
|
270
|
+
*
|
|
271
|
+
* The SOURCE and the MODE are deliberately NOT inputs here. This function
|
|
272
|
+
* answers "is that version worth having"; whether this machine may install it is
|
|
273
|
+
* the service's question, and folding the two would make a pure decision depend
|
|
274
|
+
* on the filesystem.
|
|
275
|
+
*/
|
|
276
|
+
export function decideUpdate(input) {
|
|
277
|
+
const { local, manifest, nodeVersion, skippedVersion } = input;
|
|
278
|
+
const remote = manifest.version;
|
|
279
|
+
if (compareSemver(remote, local) <= 0)
|
|
280
|
+
return { action: 'none', reason: 'up-to-date' };
|
|
281
|
+
if (skippedVersion.length > 0 && remote === skippedVersion) {
|
|
282
|
+
return { action: 'none', reason: 'skipped' };
|
|
283
|
+
}
|
|
284
|
+
// A stable install is never moved onto a prerelease, even if someone points
|
|
285
|
+
// `latest` at one. A local prerelease is already opted in, so it may move.
|
|
286
|
+
if (isPrerelease(remote) && !isPrerelease(local)) {
|
|
287
|
+
return { action: 'none', reason: 'prerelease' };
|
|
288
|
+
}
|
|
289
|
+
if (manifest.deprecated !== undefined)
|
|
290
|
+
return { action: 'none', reason: 'deprecated' };
|
|
291
|
+
if (!satisfiesNodeRange(manifest.engines?.node, nodeVersion)) {
|
|
292
|
+
return { action: 'notify', reason: 'node-too-old' };
|
|
293
|
+
}
|
|
294
|
+
return { action: 'install', reason: 'newer-available' };
|
|
295
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `UpdateService` - the state machine, the scheduler and the event stream
|
|
3
|
+
* (cli-auto-update section 3.6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
|
|
6
|
+
*
|
|
7
|
+
* idle --check()--> checking --none--> idle
|
|
8
|
+
* |
|
|
9
|
+
* |-notify--> available (terminal for the session)
|
|
10
|
+
* `-install-> installing --ok--> ready (terminal)
|
|
11
|
+
* `-err--> failed --backoff--> idle
|
|
12
|
+
*
|
|
13
|
+
* EVERY FAILURE IS SILENT (D-7). A background updater that cannot reach the
|
|
14
|
+
* registry behind a corporate proxy, or cannot write to `/usr/lib/node_modules`
|
|
15
|
+
* without sudo, must not turn into a recurring interruption: it logs, backs off
|
|
16
|
+
* exponentially, and stays quiet. The single exception is a failure the user can
|
|
17
|
+
* actually fix, after `UPDATE_LIMITS.failuresBeforeNotice` consecutive
|
|
18
|
+
* occurrences.
|
|
19
|
+
*
|
|
20
|
+
* THIS MODULE IS REACHED ONLY THROUGH A DYNAMIC `import()` (section 3.8 / D-25).
|
|
21
|
+
* `runOneShot` and `runInteractive` live in the same `cli.tsx` module and there
|
|
22
|
+
* is no bundler (C-16), so a static import here would put the whole subsystem in
|
|
23
|
+
* `dist/cli.js`'s graph and `aragon -p` would evaluate every module under
|
|
24
|
+
* `update/`.
|
|
25
|
+
*/
|
|
26
|
+
import type { ScopedLogger } from '../logging/logger.js';
|
|
27
|
+
import type { UpdateConfig } from '../config/schema.js';
|
|
28
|
+
import { type NpmViewDeps } from './npm-view.js';
|
|
29
|
+
import { type InstallDeps, type SpawnImpl } from './installer.js';
|
|
30
|
+
import type { InstallSource, UpdateCommandPort, UpdateSnapshot } from './types.js';
|
|
31
|
+
/** A timer handle we only ever cancel and `unref`. */
|
|
32
|
+
interface TimerHandle {
|
|
33
|
+
unref?: () => void;
|
|
34
|
+
}
|
|
35
|
+
export interface UpdateServiceDeps {
|
|
36
|
+
config: UpdateConfig;
|
|
37
|
+
/** The running version - `cli.tsx`'s `VERSION`. */
|
|
38
|
+
currentVersion: string;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
fetchImpl?: typeof fetch;
|
|
41
|
+
spawnImpl?: SpawnImpl;
|
|
42
|
+
logger?: ScopedLogger;
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
44
|
+
/** `process.versions.node`, injected so every engine test is offline. */
|
|
45
|
+
nodeVersion?: string;
|
|
46
|
+
/** Overrides `selfPackageRoot()`; tests point it at a synthetic tree. */
|
|
47
|
+
packageRoot?: string | null;
|
|
48
|
+
/** Overrides the name read from `<root>/package.json`. */
|
|
49
|
+
packageName?: string;
|
|
50
|
+
/** Pre-resolved classification, so a test need not build a real tree. */
|
|
51
|
+
source?: InstallSource;
|
|
52
|
+
setTimeoutImpl?: (fn: () => void, ms: number) => TimerHandle;
|
|
53
|
+
clearTimeoutImpl?: (handle: TimerHandle) => void;
|
|
54
|
+
/** Deterministic jitter for tests. */
|
|
55
|
+
random?: () => number;
|
|
56
|
+
/** Forwarded to `runNpmInstall` and `tryAcquireInstallLock`. */
|
|
57
|
+
install?: InstallDeps;
|
|
58
|
+
/** Forwarded to `fetchLatestViaNpm`, so no test spawns npm (H3). */
|
|
59
|
+
npmView?: NpmViewDeps;
|
|
60
|
+
lockPath?: string;
|
|
61
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
62
|
+
}
|
|
63
|
+
type Listener = (snapshot: UpdateSnapshot) => void;
|
|
64
|
+
export declare class UpdateService implements UpdateCommandPort {
|
|
65
|
+
private readonly deps;
|
|
66
|
+
private readonly now;
|
|
67
|
+
private readonly log;
|
|
68
|
+
private readonly listeners;
|
|
69
|
+
private phase;
|
|
70
|
+
private latestVersion;
|
|
71
|
+
private reason;
|
|
72
|
+
private advice;
|
|
73
|
+
/** Set only on the `node-too-old` path; both, or neither (section 6.2). */
|
|
74
|
+
private requiredNode;
|
|
75
|
+
private runningNode;
|
|
76
|
+
private nextAt;
|
|
77
|
+
private failures;
|
|
78
|
+
/**
|
|
79
|
+
* The rollback notice, held for THIS SESSION and cleared from the file the
|
|
80
|
+
* first time it is read (section 5.2a / D-38). Both, or neither.
|
|
81
|
+
*/
|
|
82
|
+
private rolledBackFrom;
|
|
83
|
+
private rolledBackTo;
|
|
84
|
+
/** What `aragon update --rollback` would reinstall, for `/update status`. */
|
|
85
|
+
private lastGoodVersion;
|
|
86
|
+
/** Which probe answered the last successful check (H3). */
|
|
87
|
+
private probe;
|
|
88
|
+
private timer;
|
|
89
|
+
private started;
|
|
90
|
+
private disposed;
|
|
91
|
+
private inFlight;
|
|
92
|
+
private resolved;
|
|
93
|
+
constructor(deps: UpdateServiceDeps);
|
|
94
|
+
/**
|
|
95
|
+
* Schedule the first check. Idempotent.
|
|
96
|
+
*
|
|
97
|
+
* The first check fires `UPDATE_LIMITS.startupDelayMs` after this call so it
|
|
98
|
+
* can never contend with first paint, and every timer is `unref()`ed so a
|
|
99
|
+
* pending check never keeps `aragon` from exiting.
|
|
100
|
+
*/
|
|
101
|
+
start(): void;
|
|
102
|
+
snapshot(): UpdateSnapshot;
|
|
103
|
+
subscribe(fn: Listener): () => void;
|
|
104
|
+
nextCheckAt(): number | null;
|
|
105
|
+
/**
|
|
106
|
+
* Run a check now, subject to the machine-wide throttle unless forced.
|
|
107
|
+
*
|
|
108
|
+
* SINGLE-FLIGHT. Two `/update now` in a row, or a slash command landing on top
|
|
109
|
+
* of the scheduled tick, must produce one registry request and one install -
|
|
110
|
+
* the in-process half of what `lastCheckAt` does across processes.
|
|
111
|
+
*/
|
|
112
|
+
checkNow(opts?: {
|
|
113
|
+
force?: boolean;
|
|
114
|
+
}): Promise<UpdateSnapshot>;
|
|
115
|
+
/**
|
|
116
|
+
* Install a specific version now, bypassing the decision gates.
|
|
117
|
+
*
|
|
118
|
+
* The explicit escape hatch behind `aragon update --to <version>` (non-goal 4:
|
|
119
|
+
* there is no AUTOMATIC downgrade, but a deliberate one is exactly what a user
|
|
120
|
+
* needs when a release misbehaves).
|
|
121
|
+
*/
|
|
122
|
+
installNow(version?: string): Promise<UpdateSnapshot>;
|
|
123
|
+
/** Persist `skippedVersion`; the line disappears until a newer one appears. */
|
|
124
|
+
skip(version: string): void;
|
|
125
|
+
/**
|
|
126
|
+
* Clear timers and drop listeners. NEVER KILLS THE INSTALLER CHILD (U-3).
|
|
127
|
+
*
|
|
128
|
+
* Killing a running `npm install -g` is the one action in this feature that
|
|
129
|
+
* can leave the user's global installation broken, and `dispose()` is called
|
|
130
|
+
* by the most ordinary event there is - the user pressing Ctrl+C. The child is
|
|
131
|
+
* detached and `unref()`ed precisely so it can finish without us.
|
|
132
|
+
*
|
|
133
|
+
* Idempotent, and called on BOTH of `runInteractive`'s teardown branches.
|
|
134
|
+
*/
|
|
135
|
+
dispose(): void;
|
|
136
|
+
private cancelTimer;
|
|
137
|
+
/**
|
|
138
|
+
* Arm the next check.
|
|
139
|
+
*
|
|
140
|
+
* The jitter is not decoration (D-12 / R-6): machine fleets started by the
|
|
141
|
+
* same automation otherwise arrive at the registry together, and the throttle
|
|
142
|
+
* in `update-state.json` only de-duplicates within ONE machine.
|
|
143
|
+
*/
|
|
144
|
+
private schedule;
|
|
145
|
+
/** `backoffBaseMs * 2^(n-1)`, capped. `n` is the consecutive-failure count. */
|
|
146
|
+
private backoffMs;
|
|
147
|
+
private scheduleNext;
|
|
148
|
+
private setPhase;
|
|
149
|
+
/**
|
|
150
|
+
* Step 11 of the happy path: the new version is now the running one, so the
|
|
151
|
+
* pending marker has served its purpose.
|
|
152
|
+
*
|
|
153
|
+
* Silent by design - the user restarted, which is the acknowledgement. The log
|
|
154
|
+
* record is what makes "when did this machine actually pick up 0.6.0"
|
|
155
|
+
* answerable after the fact.
|
|
156
|
+
*/
|
|
157
|
+
private reconcilePendingRestart;
|
|
158
|
+
/**
|
|
159
|
+
* Read the rollback notice, hold it for this session, and clear the file
|
|
160
|
+
* (cli-auto-update-hardening section 5.2a / D-38 / D-40).
|
|
161
|
+
*
|
|
162
|
+
* CONSUMING ON READ IS WHAT MAKES "ONE SESSION" TRUE WITHOUT A SECOND
|
|
163
|
+
* TIMESTAMP. It also means a launch that never constructs a service -
|
|
164
|
+
* `update.mode: 'off'`, a non-TTY, CI - does NOT burn the notice, so it
|
|
165
|
+
* survives to the first session that could actually have shown it.
|
|
166
|
+
*
|
|
167
|
+
* `rolledBackTo` is the running version and is therefore DERIVED, not stored:
|
|
168
|
+
* we are, by definition, the target of the rollback that wrote this field. The
|
|
169
|
+
* "both or neither" rule the pair inherits from `requiredNode` / `runningNode`
|
|
170
|
+
* is satisfied by construction rather than by discipline.
|
|
171
|
+
*
|
|
172
|
+
* `lastGoodVersion` is picked up in the same read - it is what
|
|
173
|
+
* `aragon update --rollback` would reinstall, and `/update status` is the only
|
|
174
|
+
* place a user can find that out before running the command.
|
|
175
|
+
*/
|
|
176
|
+
private consumeRollbackNotice;
|
|
177
|
+
/** Classify this installation once. Cheap, but it stats the filesystem. */
|
|
178
|
+
private resolve;
|
|
179
|
+
private runCheck;
|
|
180
|
+
/**
|
|
181
|
+
* Report an available version without installing it.
|
|
182
|
+
*
|
|
183
|
+
* `dev-monorepo` and `npx` are SILENT even here (AC-6 / AC-7): a developer's
|
|
184
|
+
* clone is not out of date, it is checked out, and `npx` resolved `latest`
|
|
185
|
+
* seconds ago. Both fall back to `idle`, which `shouldRenderUpdateLine`
|
|
186
|
+
* answers `false` for.
|
|
187
|
+
*/
|
|
188
|
+
private offer;
|
|
189
|
+
private performInstall;
|
|
190
|
+
private recordFailure;
|
|
191
|
+
private clearFailures;
|
|
192
|
+
}
|
|
193
|
+
export {};
|