@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +1157 -1026
  2. package/README.md +123 -0
  3. package/dist/boot/guard.d.ts +135 -0
  4. package/dist/boot/guard.js +218 -0
  5. package/dist/boot/rollback.d.ts +86 -0
  6. package/dist/boot/rollback.js +185 -0
  7. package/dist/cli.js +376 -2
  8. package/dist/commands/builtins.js +118 -0
  9. package/dist/commands/perf.d.ts +5 -1
  10. package/dist/commands/perf.js +43 -1
  11. package/dist/commands/registry.d.ts +17 -0
  12. package/dist/config/app-paths.d.ts +31 -0
  13. package/dist/config/app-paths.js +37 -0
  14. package/dist/config/cli-commands.d.ts +27 -1
  15. package/dist/config/cli-commands.js +51 -2
  16. package/dist/config/env.d.ts +11 -1
  17. package/dist/config/env.js +58 -1
  18. package/dist/config/load.d.ts +30 -0
  19. package/dist/config/load.js +56 -1
  20. package/dist/config/schema.d.ts +91 -0
  21. package/dist/config/schema.js +107 -0
  22. package/dist/config/store.js +13 -1
  23. package/dist/launcher.d.ts +14 -0
  24. package/dist/launcher.js +43 -0
  25. package/dist/logging/logger.d.ts +1 -1
  26. package/dist/ui/App.d.ts +11 -1
  27. package/dist/ui/App.js +88 -2
  28. package/dist/ui/BottomStatusRow.d.ts +25 -3
  29. package/dist/ui/BottomStatusRow.js +10 -7
  30. package/dist/ui/PromptInput.d.ts +10 -0
  31. package/dist/ui/PromptInput.js +41 -48
  32. package/dist/ui/UpdateLine.d.ts +44 -0
  33. package/dist/ui/UpdateLine.js +129 -0
  34. package/dist/ui/editor-reducer.d.ts +62 -0
  35. package/dist/ui/editor-reducer.js +90 -0
  36. package/dist/ui/frame-differ.d.ts +95 -0
  37. package/dist/ui/frame-differ.js +230 -0
  38. package/dist/ui/stdout-frame-writer.d.ts +41 -0
  39. package/dist/ui/stdout-frame-writer.js +102 -0
  40. package/dist/update/classify-failure.d.ts +32 -0
  41. package/dist/update/classify-failure.js +60 -0
  42. package/dist/update/install-lock.d.ts +55 -0
  43. package/dist/update/install-lock.js +184 -0
  44. package/dist/update/install-source.d.ts +97 -0
  45. package/dist/update/install-source.js +218 -0
  46. package/dist/update/installer.d.ts +108 -0
  47. package/dist/update/installer.js +242 -0
  48. package/dist/update/limits.d.ts +111 -0
  49. package/dist/update/limits.js +111 -0
  50. package/dist/update/npm-view.d.ts +62 -0
  51. package/dist/update/npm-view.js +133 -0
  52. package/dist/update/registry.d.ts +52 -0
  53. package/dist/update/registry.js +161 -0
  54. package/dist/update/semver.d.ts +89 -0
  55. package/dist/update/semver.js +295 -0
  56. package/dist/update/service.d.ts +193 -0
  57. package/dist/update/service.js +636 -0
  58. package/dist/update/state.d.ts +103 -0
  59. package/dist/update/state.js +142 -0
  60. package/dist/update/types.d.ts +169 -0
  61. package/dist/update/types.js +68 -0
  62. 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
+ }