@aragon-agent/cli 0.5.9 → 0.5.11
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 +376 -2
- package/dist/commands/builtins.js +118 -0
- package/dist/commands/perf.d.ts +5 -1
- package/dist/commands/perf.js +43 -1
- 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 +11 -1
- package/dist/config/env.js +58 -1
- package/dist/config/load.d.ts +30 -0
- package/dist/config/load.js +56 -1
- package/dist/config/schema.d.ts +91 -0
- package/dist/config/schema.js +107 -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/PromptInput.d.ts +10 -0
- package/dist/ui/PromptInput.js +41 -48
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/ui/editor-reducer.d.ts +62 -0
- package/dist/ui/editor-reducer.js +90 -0
- package/dist/ui/frame-differ.d.ts +95 -0
- package/dist/ui/frame-differ.js +230 -0
- package/dist/ui/stdout-frame-writer.d.ts +41 -0
- package/dist/ui/stdout-frame-writer.js +102 -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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rollback (cli-auto-update-hardening H1 / section 5.1.3).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-19).
|
|
5
|
+
*
|
|
6
|
+
* EVERY IMPORT THAT DOES REAL WORK IS DYNAMIC, AND FOR A REASON THAT IS NOT THE
|
|
7
|
+
* USUAL ONE. This function runs inside a build we have CONCRETE EVIDENCE is
|
|
8
|
+
* broken - it was reached because that build failed to start twice - so every
|
|
9
|
+
* one of `install-lock`, `installer` and `install-source` can reject on import.
|
|
10
|
+
* The whole body is inside one `try`, and the `catch` records one line and
|
|
11
|
+
* returns. Static `import type` specifiers below are erased by tsc and are not
|
|
12
|
+
* runtime edges, which is what keeps `guard.ts`'s own graph inside the list its
|
|
13
|
+
* header pins down.
|
|
14
|
+
*
|
|
15
|
+
* `../update/state.js` IS THE ONE EXCEPTION and is imported statically: the
|
|
16
|
+
* guard has already loaded and read it before it can possibly call us, so a
|
|
17
|
+
* static edge here adds no failure mode that has not already been survived.
|
|
18
|
+
*/
|
|
19
|
+
import process from 'node:process';
|
|
20
|
+
import { readUpdateState, updateUpdateState } from '../update/state.js';
|
|
21
|
+
/** Best-effort, and deliberately so: a logger that fails must not stop a
|
|
22
|
+
* rollback that is the user's only way back to a working CLI. */
|
|
23
|
+
async function record(deps, level, msg, data) {
|
|
24
|
+
try {
|
|
25
|
+
if (deps.log) {
|
|
26
|
+
deps.log(level, msg, data);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const { getLogger } = await import('../logging/logger.js');
|
|
30
|
+
getLogger().child('update')[level](msg, data);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// No logger, or a broken one. The stderr line the guard already wrote is
|
|
34
|
+
// the part the user needs.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function defaultResolveTarget() {
|
|
38
|
+
const { classifyInstallSource, isAutoInstallable, probeWritable, readSelfManifest, selfPackageRoot } = await import('../update/install-source.js');
|
|
39
|
+
const root = selfPackageRoot();
|
|
40
|
+
if (!root)
|
|
41
|
+
return { root: null, packageName: '', autoInstallable: false, writable: false };
|
|
42
|
+
return {
|
|
43
|
+
root,
|
|
44
|
+
packageName: readSelfManifest(root)?.name ?? '',
|
|
45
|
+
autoInstallable: isAutoInstallable(classifyInstallSource(root)),
|
|
46
|
+
writable: probeWritable(root),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reinstall `good` and latch `bad` so nothing puts it back.
|
|
51
|
+
*
|
|
52
|
+
* THE ORDER OF OPERATIONS IS LOAD-BEARING.
|
|
53
|
+
*
|
|
54
|
+
* 1. LATCH FIRST, INSTALL SECOND. If the process dies between the latch and the
|
|
55
|
+
* spawn, the machine is left running a broken version that WILL NOT BE
|
|
56
|
+
* REINSTALLED - recoverable by hand. In the other order, a death between the
|
|
57
|
+
* spawn and the latch leaves a good version installed with no latch, and the
|
|
58
|
+
* next check reinstalls the bad one.
|
|
59
|
+
*
|
|
60
|
+
* `skippedVersion` is part of that write and is the reason it works across
|
|
61
|
+
* versions (C-17 / D-28): `readUpdateState` rebuilds from a fixed field list,
|
|
62
|
+
* so an OLDER `aragon` writing this file erases every field it does not know.
|
|
63
|
+
* `skippedVersion` is schema-1 and every shipped version honours it through
|
|
64
|
+
* `decideUpdate`. Per section 1.2 the ordinary rollback target is itself an H1
|
|
65
|
+
* build and preserves all four new fields; this covers the cases that
|
|
66
|
+
* argument does not reach - a hand-edited state file, a user who manually
|
|
67
|
+
* installs a pre-H1 version, and any future field that matters across the
|
|
68
|
+
* boundary.
|
|
69
|
+
*
|
|
70
|
+
* `rolledBackFrom` IS WRITTEN HERE AND NOWHERE ELSE (P0-1d). It is consumed
|
|
71
|
+
* exactly once, by `UpdateService.start()`, which is the only thing that can
|
|
72
|
+
* decide the notice has actually been shown.
|
|
73
|
+
*
|
|
74
|
+
* 2. RE-CLASSIFY BEFORE SPAWNING. The machine may have changed since the
|
|
75
|
+
* install; we do not run `npm i -g` on a tree we would not have installed
|
|
76
|
+
* into.
|
|
77
|
+
*
|
|
78
|
+
* 3. ONE NON-BLOCKING LOCK ATTEMPT (U-5b). If another process holds it, return:
|
|
79
|
+
* it is either installing or already rolling back, and two concurrent
|
|
80
|
+
* `npm i -g` on one prefix is the one thing the lock exists to stop.
|
|
81
|
+
*
|
|
82
|
+
* 4. `runNpmInstall` VERBATIM. It already validates the version against
|
|
83
|
+
* `STRICT_VERSION_RE`, spawns detached through `process.execPath`, writes to
|
|
84
|
+
* a real fd and verifies the classified root afterwards (U-6). A rollback is
|
|
85
|
+
* an install; it does not deserve a second code path.
|
|
86
|
+
*
|
|
87
|
+
* The stderr line belongs to `runBootGuard` and is written before this is called
|
|
88
|
+
* (P2-9). NEVER THROWS: the guard invokes it without `await` and cannot handle a
|
|
89
|
+
* rejection.
|
|
90
|
+
*/
|
|
91
|
+
export async function performRollback(bad, good, deps = {}) {
|
|
92
|
+
const write = deps.writeState ?? updateUpdateState;
|
|
93
|
+
const read = deps.readState ?? readUpdateState;
|
|
94
|
+
try {
|
|
95
|
+
// Read BEFORE the latch: the write below zeroes `bootFailures`, and the
|
|
96
|
+
// count is the one number worth keeping in the log record.
|
|
97
|
+
const failures = read().bootFailures;
|
|
98
|
+
// Step 1 - the latch, in ONE write.
|
|
99
|
+
write({
|
|
100
|
+
skippedVersion: bad,
|
|
101
|
+
pendingRestartVersion: '',
|
|
102
|
+
bootFailures: 0,
|
|
103
|
+
autoInstalledVersion: '',
|
|
104
|
+
rolledBackFrom: bad,
|
|
105
|
+
});
|
|
106
|
+
// Step 2 - re-classify.
|
|
107
|
+
const target = await (deps.resolveTarget ?? defaultResolveTarget)();
|
|
108
|
+
if (!target.root || target.packageName.length === 0) {
|
|
109
|
+
await record(deps, 'warn', 'update_rollback_failed', {
|
|
110
|
+
from: bad,
|
|
111
|
+
to: good,
|
|
112
|
+
reason: 'no-package-root',
|
|
113
|
+
});
|
|
114
|
+
return { ok: false, failure: 'no-package-root' };
|
|
115
|
+
}
|
|
116
|
+
if (!target.autoInstallable) {
|
|
117
|
+
await record(deps, 'warn', 'update_rollback_failed', {
|
|
118
|
+
from: bad,
|
|
119
|
+
to: good,
|
|
120
|
+
reason: 'source-ineligible',
|
|
121
|
+
});
|
|
122
|
+
return { ok: false, failure: 'source-ineligible' };
|
|
123
|
+
}
|
|
124
|
+
if (!target.writable) {
|
|
125
|
+
await record(deps, 'warn', 'update_rollback_failed', {
|
|
126
|
+
from: bad,
|
|
127
|
+
to: good,
|
|
128
|
+
reason: 'not-writable',
|
|
129
|
+
});
|
|
130
|
+
return { ok: false, failure: 'not-writable' };
|
|
131
|
+
}
|
|
132
|
+
// Step 3 - the ordinary install lock.
|
|
133
|
+
const acquire = deps.acquireLock ??
|
|
134
|
+
(await import('../update/install-lock.js')).tryAcquireInstallLock;
|
|
135
|
+
const lock = acquire();
|
|
136
|
+
if (!lock) {
|
|
137
|
+
await record(deps, 'warn', 'update_rollback_failed', {
|
|
138
|
+
from: bad,
|
|
139
|
+
to: good,
|
|
140
|
+
reason: 'locked',
|
|
141
|
+
});
|
|
142
|
+
return { ok: false, failure: 'locked' };
|
|
143
|
+
}
|
|
144
|
+
// Step 4 - the install, reused.
|
|
145
|
+
const install = deps.install ?? (await import('../update/installer.js')).runNpmInstall;
|
|
146
|
+
let result;
|
|
147
|
+
try {
|
|
148
|
+
result = await install({
|
|
149
|
+
packageName: target.packageName,
|
|
150
|
+
version: good,
|
|
151
|
+
root: target.root,
|
|
152
|
+
// Phase two of U-4, exactly as `performInstall` does it: the lock had to
|
|
153
|
+
// precede the spawn, and it is rewritten to the child's pid the instant
|
|
154
|
+
// the child exists. `adoptChild` closes over its own state and may be
|
|
155
|
+
// passed unbound.
|
|
156
|
+
onSpawn: lock.adoptChild,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
lock.release();
|
|
161
|
+
}
|
|
162
|
+
if (!result.ok) {
|
|
163
|
+
await record(deps, 'warn', 'update_rollback_failed', {
|
|
164
|
+
from: bad,
|
|
165
|
+
to: good,
|
|
166
|
+
reason: result.failure ?? 'install-failed',
|
|
167
|
+
});
|
|
168
|
+
return { ok: false, failure: 'install-failed' };
|
|
169
|
+
}
|
|
170
|
+
await record(deps, 'warn', 'update_rolled_back', { from: bad, to: good, failures });
|
|
171
|
+
return { ok: true };
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
// The one place this module writes to the terminal itself, and only because
|
|
175
|
+
// the alternative is a silent no-op on the path the user is already stuck on.
|
|
176
|
+
try {
|
|
177
|
+
process.stderr.write(`aragon: rollback to ${good} could not run (${err instanceof Error ? err.message : String(err)}).\n`);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// stderr is gone too. Nothing further is available to us.
|
|
181
|
+
}
|
|
182
|
+
await record(deps, 'warn', 'update_rollback_failed', { from: bad, to: good, reason: 'error' });
|
|
183
|
+
return { ok: false, failure: 'error' };
|
|
184
|
+
}
|
|
185
|
+
}
|