@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,103 @@
1
+ /**
2
+ * `<home>/update-state.json` (cli-auto-update section 5.2).
3
+ *
4
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
5
+ *
6
+ * WHY NOT `state.json` (D-14 / C-6). That file is documented as UI bookkeeping
7
+ * SCALARS ONLY, and its header names itself "the most inviting drawer in the
8
+ * codebase". What lives here is CROSS-PROCESS COORDINATION state with a
9
+ * completely different lifetime and failure policy: ten terminals read it to
10
+ * decide whether anyone has already asked the registry today. The rule exists
11
+ * precisely to stop the merge that looks convenient today.
12
+ *
13
+ * FAILURE POLICY: copies `ui-state.ts` verbatim - per-field tolerance, whole
14
+ * object fallback on a wrong `schema`, atomic temp-file + rename + `chmod 0600`,
15
+ * and NEVER THROWS. Losing the file costs one extra registry request.
16
+ *
17
+ * NO PROCESS-LIFETIME CACHE, unlike `ui-state.ts`. The whole point of
18
+ * `lastCheckAt` is that a SECOND aragon process wrote it (section 3.7), so a
19
+ * value memoized at first read would defeat the throttle it exists to implement.
20
+ *
21
+ * ==========================================================================
22
+ * C-17 - THIS FILE IS A CROSS-VERSION CHANNEL, AND `readUpdateState` ERASES
23
+ * WHAT IT DOES NOT KNOW.
24
+ *
25
+ * The reader below rebuilds the object FIELD BY FIELD from a fixed list, so an
26
+ * OLDER `aragon` reading and writing this file destroys every field added after
27
+ * it shipped. Two consequences, and both have already been reasoned through
28
+ * (cli-auto-update-hardening section 7.1 / D-27 / D-28):
29
+ *
30
+ * 1. DO NOT BUMP `UPDATE_STATE_SCHEMA` TO ADD A FIELD. A bump makes the reader
31
+ * fall back to the WHOLE default object on the first read after an upgrade
32
+ * (see the `parsed.schema !== ...` line), which discards `skippedVersion` -
33
+ * the field that stops an `install-ineffective` loop (R-12) and a
34
+ * bad-release loop (H1) - on every machine at once. Per-field tolerance
35
+ * already handles absence: a missing field reads as its default.
36
+ * 2. A ROLLBACK MUST LATCH THROUGH `skippedVersion`, which is a schema-1 field
37
+ * every shipped version honours. Latching only in one of the four fields
38
+ * below would be invisible to a CLI that predates them, which would decide
39
+ * `install` on its next check and reinstall the release that just bricked
40
+ * the machine - once per interval, forever.
41
+ * ==========================================================================
42
+ */
43
+ export declare const UPDATE_STATE_FILENAME = "update-state.json";
44
+ export declare const UPDATE_STATE_SCHEMA = 1;
45
+ export interface UpdateState {
46
+ schema: number;
47
+ /** Epoch ms of the last registry request by ANY process; `0` = never. */
48
+ lastCheckAt: number;
49
+ /** `''` = unknown. */
50
+ lastKnownVersion: string;
51
+ /** Installed and awaiting a relaunch; `''` = none. */
52
+ pendingRestartVersion: string;
53
+ /** `''` = none. Set by `/update skip` and by an ineffective install (U-6). */
54
+ skippedVersion: string;
55
+ consecutiveFailures: number;
56
+ lastFailureAt: number;
57
+ /**
58
+ * The version THIS UPDATER installed and has not yet seen boot healthy.
59
+ * `''` = the boot guard is disarmed, which is every launch on every machine
60
+ * except the first one after an auto-update.
61
+ */
62
+ autoInstalledVersion: string;
63
+ /**
64
+ * The version that was running when the last auto-install ran, or the last
65
+ * version seen to boot healthy. THE ROLLBACK TARGET.
66
+ */
67
+ lastGoodVersion: string;
68
+ /**
69
+ * Launches of `autoInstalledVersion` that EXITED NON-ZERO. Reset by
70
+ * `markBootHealthy`, by a rollback, and by each new arming.
71
+ *
72
+ * NAMED FOR FAILURES AND NOT FOR ATTEMPTS, and the name is the specification
73
+ * (P1-5). A counter named for attempts invites an increment at boot, and an
74
+ * increment at boot has no per-process identity: three terminals starting
75
+ * inside the window between the first one's increment and its
76
+ * `markBootHealthy` walk it to the threshold, and the third rolls back a
77
+ * release running fine in the other two. A counter named for failures can only
78
+ * be incremented where a failure is observable, which is the exit hook.
79
+ */
80
+ bootFailures: number;
81
+ /**
82
+ * The version the last rollback moved away from. Written by `performRollback`
83
+ * and consumed EXACTLY ONCE by `UpdateService.start()`. `''` = nothing to
84
+ * report.
85
+ */
86
+ rolledBackFrom: string;
87
+ }
88
+ export declare const DEFAULT_UPDATE_STATE: UpdateState;
89
+ /**
90
+ * Per-field tolerance: one nonsense value must not take the others with it, and
91
+ * a field this version has never heard of reads as its default rather than
92
+ * failing the parse. A wrong SCHEMA is different - the shape is no longer known,
93
+ * so the whole object falls back (the rule `ui-state.ts` and `skills/usage.ts`
94
+ * follow). See C-17 in the header before adding a field OR touching the schema.
95
+ */
96
+ export declare function readUpdateState(): UpdateState;
97
+ /**
98
+ * Read-modify-write. READ-MODIFY-WRITE AND NOT WRITE, deliberately: another
99
+ * `aragon` may have recorded a check or an install between our read and this
100
+ * call, and clobbering its `lastCheckAt` is how ten terminals turn back into ten
101
+ * registry requests.
102
+ */
103
+ export declare function updateUpdateState(patch: Partial<Omit<UpdateState, 'schema'>>): UpdateState;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * `<home>/update-state.json` (cli-auto-update section 5.2).
3
+ *
4
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
5
+ *
6
+ * WHY NOT `state.json` (D-14 / C-6). That file is documented as UI bookkeeping
7
+ * SCALARS ONLY, and its header names itself "the most inviting drawer in the
8
+ * codebase". What lives here is CROSS-PROCESS COORDINATION state with a
9
+ * completely different lifetime and failure policy: ten terminals read it to
10
+ * decide whether anyone has already asked the registry today. The rule exists
11
+ * precisely to stop the merge that looks convenient today.
12
+ *
13
+ * FAILURE POLICY: copies `ui-state.ts` verbatim - per-field tolerance, whole
14
+ * object fallback on a wrong `schema`, atomic temp-file + rename + `chmod 0600`,
15
+ * and NEVER THROWS. Losing the file costs one extra registry request.
16
+ *
17
+ * NO PROCESS-LIFETIME CACHE, unlike `ui-state.ts`. The whole point of
18
+ * `lastCheckAt` is that a SECOND aragon process wrote it (section 3.7), so a
19
+ * value memoized at first read would defeat the throttle it exists to implement.
20
+ *
21
+ * ==========================================================================
22
+ * C-17 - THIS FILE IS A CROSS-VERSION CHANNEL, AND `readUpdateState` ERASES
23
+ * WHAT IT DOES NOT KNOW.
24
+ *
25
+ * The reader below rebuilds the object FIELD BY FIELD from a fixed list, so an
26
+ * OLDER `aragon` reading and writing this file destroys every field added after
27
+ * it shipped. Two consequences, and both have already been reasoned through
28
+ * (cli-auto-update-hardening section 7.1 / D-27 / D-28):
29
+ *
30
+ * 1. DO NOT BUMP `UPDATE_STATE_SCHEMA` TO ADD A FIELD. A bump makes the reader
31
+ * fall back to the WHOLE default object on the first read after an upgrade
32
+ * (see the `parsed.schema !== ...` line), which discards `skippedVersion` -
33
+ * the field that stops an `install-ineffective` loop (R-12) and a
34
+ * bad-release loop (H1) - on every machine at once. Per-field tolerance
35
+ * already handles absence: a missing field reads as its default.
36
+ * 2. A ROLLBACK MUST LATCH THROUGH `skippedVersion`, which is a schema-1 field
37
+ * every shipped version honours. Latching only in one of the four fields
38
+ * below would be invisible to a CLI that predates them, which would decide
39
+ * `install` on its next check and reinstall the release that just bricked
40
+ * the machine - once per interval, forever.
41
+ * ==========================================================================
42
+ */
43
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
44
+ import process from 'node:process';
45
+ import { getHomeRoot, getUpdateStatePath } from '../config/app-paths.js';
46
+ export const UPDATE_STATE_FILENAME = 'update-state.json';
47
+ export const UPDATE_STATE_SCHEMA = 1;
48
+ export const DEFAULT_UPDATE_STATE = {
49
+ schema: UPDATE_STATE_SCHEMA,
50
+ lastCheckAt: 0,
51
+ lastKnownVersion: '',
52
+ pendingRestartVersion: '',
53
+ skippedVersion: '',
54
+ consecutiveFailures: 0,
55
+ lastFailureAt: 0,
56
+ autoInstalledVersion: '',
57
+ lastGoodVersion: '',
58
+ bootFailures: 0,
59
+ rolledBackFrom: '',
60
+ };
61
+ function count(value, fallback) {
62
+ const n = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
63
+ if (!Number.isFinite(n) || n < 0)
64
+ return fallback;
65
+ return Math.floor(n);
66
+ }
67
+ function text(value, fallback) {
68
+ return typeof value === 'string' ? value : fallback;
69
+ }
70
+ /**
71
+ * Per-field tolerance: one nonsense value must not take the others with it, and
72
+ * a field this version has never heard of reads as its default rather than
73
+ * failing the parse. A wrong SCHEMA is different - the shape is no longer known,
74
+ * so the whole object falls back (the rule `ui-state.ts` and `skills/usage.ts`
75
+ * follow). See C-17 in the header before adding a field OR touching the schema.
76
+ */
77
+ export function readUpdateState() {
78
+ const path = getUpdateStatePath();
79
+ if (!existsSync(path))
80
+ return { ...DEFAULT_UPDATE_STATE };
81
+ try {
82
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
83
+ if (!parsed || typeof parsed !== 'object')
84
+ return { ...DEFAULT_UPDATE_STATE };
85
+ if (parsed.schema !== UPDATE_STATE_SCHEMA)
86
+ return { ...DEFAULT_UPDATE_STATE };
87
+ return {
88
+ schema: UPDATE_STATE_SCHEMA,
89
+ lastCheckAt: count(parsed.lastCheckAt, DEFAULT_UPDATE_STATE.lastCheckAt),
90
+ lastKnownVersion: text(parsed.lastKnownVersion, DEFAULT_UPDATE_STATE.lastKnownVersion),
91
+ pendingRestartVersion: text(parsed.pendingRestartVersion, DEFAULT_UPDATE_STATE.pendingRestartVersion),
92
+ skippedVersion: text(parsed.skippedVersion, DEFAULT_UPDATE_STATE.skippedVersion),
93
+ consecutiveFailures: count(parsed.consecutiveFailures, DEFAULT_UPDATE_STATE.consecutiveFailures),
94
+ lastFailureAt: count(parsed.lastFailureAt, DEFAULT_UPDATE_STATE.lastFailureAt),
95
+ autoInstalledVersion: text(parsed.autoInstalledVersion, DEFAULT_UPDATE_STATE.autoInstalledVersion),
96
+ lastGoodVersion: text(parsed.lastGoodVersion, DEFAULT_UPDATE_STATE.lastGoodVersion),
97
+ bootFailures: count(parsed.bootFailures, DEFAULT_UPDATE_STATE.bootFailures),
98
+ rolledBackFrom: text(parsed.rolledBackFrom, DEFAULT_UPDATE_STATE.rolledBackFrom),
99
+ };
100
+ }
101
+ catch {
102
+ return { ...DEFAULT_UPDATE_STATE };
103
+ }
104
+ }
105
+ /**
106
+ * Atomic write: temp file, rename, re-chmod. Never throws.
107
+ *
108
+ * `mkdirSync` first for the reason `prompt-history.ts` and `ui-state.ts` both
109
+ * record: `<home>` is not guaranteed to exist, and without it the throttle would
110
+ * silently never persist on a fresh install - which presents as one registry
111
+ * request per launch forever.
112
+ */
113
+ function writeUpdateState(state) {
114
+ const path = getUpdateStatePath();
115
+ const tmp = `${path}.tmp-${process.pid}`;
116
+ try {
117
+ mkdirSync(getHomeRoot(), { recursive: true });
118
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
119
+ renameSync(tmp, path);
120
+ if (process.platform !== 'win32')
121
+ chmodSync(path, 0o600);
122
+ }
123
+ catch {
124
+ try {
125
+ rmSync(tmp, { force: true });
126
+ }
127
+ catch {
128
+ // Nothing further to try; the value simply stays in memory.
129
+ }
130
+ }
131
+ }
132
+ /**
133
+ * Read-modify-write. READ-MODIFY-WRITE AND NOT WRITE, deliberately: another
134
+ * `aragon` may have recorded a check or an install between our read and this
135
+ * call, and clobbering its `lastCheckAt` is how ten terminals turn back into ten
136
+ * registry requests.
137
+ */
138
+ export function updateUpdateState(patch) {
139
+ const next = { ...readUpdateState(), ...patch, schema: UPDATE_STATE_SCHEMA };
140
+ writeUpdateState(next);
141
+ return next;
142
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The updater's runtime shapes, plus the ONE pure predicate the renderer needs
3
+ * (cli-auto-update sections 5.3 / 5.3a).
4
+ *
5
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
6
+ *
7
+ * THE ONLY VALUE EXPORTED HERE IS `shouldRenderUpdateLine`. Everything else is a
8
+ * type, so `ui/UpdateLine.tsx` and `commands/registry.ts` can import from this
9
+ * module with `import type` and tsc erases the specifier entirely - no runtime
10
+ * edge from the renderer or the command registry into `update/` (section 3.1
11
+ * rule 1 / P1-7). This module and `limits.ts` import nothing but each other, are
12
+ * side-effect-free, and open no socket, spawn no process and create no timer.
13
+ */
14
+ export type UpdatePhase = 'idle' | 'checking' | 'available' | 'installing' | 'ready' | 'failed';
15
+ /**
16
+ * How the running copy of `aragon` got onto this machine (section 3.2).
17
+ *
18
+ * `npm-global` is the ONLY value that may be auto-installed, because it is the
19
+ * only one whose install target is unambiguous and verifiable from this machine
20
+ * (D-5). Everything else is advice, not execution.
21
+ */
22
+ export type InstallSource = 'dev-monorepo' | 'npx' | 'pnpm' | 'yarn' | 'bun' | 'volta' | 'npm-local' | 'npm-global' | 'unknown';
23
+ /** Why we are not installing, when we are not. Drives the advice line. */
24
+ export type UpdateReason = 'up-to-date' | 'prerelease' | 'deprecated' | 'node-too-old' | 'skipped' | 'not-writable' | 'source-ineligible' | 'network' | 'install-failed' | 'locked'
25
+ /** npm exited 0 but THIS root did not move - see U-6. */
26
+ | 'install-ineffective'
27
+ /** EPERM / EBUSY / ETXTBSY - something holds the file we must replace. */
28
+ | 'blocked-by-os'
29
+ /** ENOSPC. */
30
+ | 'no-space';
31
+ /** The slice of the registry's dist-tag document this feature reads. */
32
+ export interface LatestManifest {
33
+ name: string;
34
+ version: string;
35
+ engines?: {
36
+ node?: string;
37
+ };
38
+ /** Present ONLY when the version is deprecated; the string is the reason. */
39
+ deprecated?: string;
40
+ }
41
+ export interface UpdateSnapshot {
42
+ phase: UpdatePhase;
43
+ currentVersion: string;
44
+ latestVersion: string | null;
45
+ source: InstallSource;
46
+ reason?: UpdateReason;
47
+ /** Copy-pasteable command for an ineligible source. */
48
+ advice?: string;
49
+ /**
50
+ * The `engines.node` range the offered version demands, and the Node actually
51
+ * running - set ONLY on the `node-too-old` path.
52
+ *
53
+ * They exist because section 6.2 specifies that row as
54
+ * `<warn> 0.6.0 needs Node >=20 (running 18.19.0)`, and neither number is
55
+ * derivable from anything else on this object. "Needs a newer Node" without
56
+ * saying WHICH is the kind of notice a user cannot act on, which is exactly
57
+ * what D-7 spends the rest of this feature avoiding (IF-2).
58
+ */
59
+ requiredNode?: string;
60
+ runningNode?: string;
61
+ /**
62
+ * Set for the WHOLE session after a rollback, and deliberately NOT a `reason`
63
+ * (cli-auto-update-hardening section 5.2a / D-39).
64
+ *
65
+ * `reason` is rewritten by `setPhase` on EVERY transition, so the first
66
+ * scheduled check three seconds into the session would erase it before anyone
67
+ * read it. A fact that is true for a whole session needs a carrier the state
68
+ * machine does not own.
69
+ *
70
+ * BOTH, OR NEITHER, on the `requiredNode` / `runningNode` precedent above -
71
+ * half the pair is a notice nobody can read. Here it holds BY CONSTRUCTION
72
+ * rather than by discipline: `rolledBackTo` is the running version, assigned
73
+ * in the same two lines that read `rolledBackFrom` out of the state file.
74
+ */
75
+ rolledBackFrom?: string;
76
+ rolledBackTo?: string;
77
+ /**
78
+ * What `aragon update --rollback` would reinstall, read from the state file.
79
+ *
80
+ * On the snapshot rather than derived because `/update status` is the only
81
+ * place a user can find out what that command would DO before running it
82
+ * (section 6.3). See IF-1 in the hardening spec.
83
+ */
84
+ lastGoodVersion?: string;
85
+ /** Which probe answered the last successful check (H3). */
86
+ probe?: 'http' | 'npm';
87
+ nextCheckAt: number | null;
88
+ consecutiveFailures: number;
89
+ }
90
+ export type UpdateEvent = {
91
+ type: 'phase';
92
+ snapshot: UpdateSnapshot;
93
+ } | {
94
+ type: 'log';
95
+ level: 'info' | 'warn';
96
+ msg: string;
97
+ };
98
+ /**
99
+ * The narrow surface `/update` sees (section 4.4 / C-14 / P0-2).
100
+ *
101
+ * `CommandContext` carries this rather than the service itself, and carries it
102
+ * as an OPTIONAL member: the service is legitimately absent under
103
+ * `update.mode: 'off'`, on a non-TTY and in CI, and a required field would break
104
+ * every existing `CommandContext` fixture at compile time.
105
+ *
106
+ * `UpdateService` satisfies it STRUCTURALLY, so nothing implements it explicitly
107
+ * and there is no adapter to keep in sync.
108
+ */
109
+ export interface UpdateCommandPort {
110
+ snapshot(): UpdateSnapshot;
111
+ checkNow(opts?: {
112
+ force?: boolean;
113
+ }): Promise<UpdateSnapshot>;
114
+ skip(version: string): void;
115
+ nextCheckAt(): number | null;
116
+ }
117
+ /** What the renderer needs on top of the command port: a subscription. */
118
+ export interface UpdateServiceHandle extends UpdateCommandPort {
119
+ subscribe(fn: (snapshot: UpdateSnapshot) => void): () => void;
120
+ }
121
+ /**
122
+ * The late-arrival channel between `cli.tsx` and `App` (IF-4).
123
+ *
124
+ * The service is constructed through a DYNAMIC `import()` fired AFTER
125
+ * `render()` (section 3.8 / D-25), so it does not exist when `App` mounts and
126
+ * cannot be an ordinary prop. This is the same shape `ConfirmBridge` and
127
+ * `HumanInputBridge` already use for exactly the same reason: a mutable object
128
+ * created before `render()`, populated by whichever side arrives second.
129
+ *
130
+ * A PLAIN INTERFACE AND NOT A CLASS, so `cli.tsx` builds one with an object
131
+ * literal and needs no runtime import from `update/` (AC-1).
132
+ */
133
+ export interface UpdateBridge {
134
+ /** Set once the dynamically-imported service exists; `null` until then. */
135
+ service: UpdateServiceHandle | null;
136
+ /** Set by `App` on mount, so a service that arrives later can wake it. */
137
+ onAttach: ((service: UpdateServiceHandle) => void) | null;
138
+ }
139
+ /**
140
+ * The reasons allowed to break the three-strike silence rule (D-7 / D-33),
141
+ * because each is both ACTIONABLE BY THE USER and NOT SELF-HEALING.
142
+ *
143
+ * THE MEMBERSHIP RULE IS THE WHOLE MECHANISM, not the list. `network` fails both
144
+ * halves - the user cannot fix the registry and the next check may well succeed -
145
+ * so it stays silent and backs off, which is the entire reason a background
146
+ * updater is tolerable at all. The three below will otherwise repeat identically
147
+ * until the user does something, which is the definition of a notice worth
148
+ * showing once. Anything added here without passing both halves is the first
149
+ * step of the notification fatigue the rest of this feature is built to avoid
150
+ * (R-20).
151
+ */
152
+ export declare const IMMEDIATE_NOTICE_REASONS: ReadonlySet<UpdateReason>;
153
+ /**
154
+ * Whether the bottom row has anything to say. PURE, and deliberately NOT
155
+ * expressed as `UpdateLine` returning `null` (C-15 / P0-1 / D-19).
156
+ *
157
+ * A component that returns `null` still satisfies `if (update)` at the call site
158
+ * - the prop is a React ELEMENT, and an element is truthy however it renders.
159
+ * The row would become `<Box flexShrink={0}>{nothing}</Box>`, which in
160
+ * full-screen is ZERO rows, and the transcript silently gains a row for as long
161
+ * as the updater is idle and loses it again the moment it has news. That is
162
+ * precisely the layout shift `BottomStatusRow` exists to prevent, and
163
+ * `bottom-status-row.test.tsx`'s "holds one row in all four states" is the
164
+ * assertion it would defeat.
165
+ *
166
+ * `ActivityLine` already settles the pattern: it ALWAYS returns an element, and
167
+ * `App.tsx` decides presence at the call site with `running && !overlayNode`.
168
+ */
169
+ export declare function shouldRenderUpdateLine(snapshot: UpdateSnapshot): boolean;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The updater's runtime shapes, plus the ONE pure predicate the renderer needs
3
+ * (cli-auto-update sections 5.3 / 5.3a).
4
+ *
5
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
6
+ *
7
+ * THE ONLY VALUE EXPORTED HERE IS `shouldRenderUpdateLine`. Everything else is a
8
+ * type, so `ui/UpdateLine.tsx` and `commands/registry.ts` can import from this
9
+ * module with `import type` and tsc erases the specifier entirely - no runtime
10
+ * edge from the renderer or the command registry into `update/` (section 3.1
11
+ * rule 1 / P1-7). This module and `limits.ts` import nothing but each other, are
12
+ * side-effect-free, and open no socket, spawn no process and create no timer.
13
+ */
14
+ import { UPDATE_LIMITS } from './limits.js';
15
+ /**
16
+ * The reasons allowed to break the three-strike silence rule (D-7 / D-33),
17
+ * because each is both ACTIONABLE BY THE USER and NOT SELF-HEALING.
18
+ *
19
+ * THE MEMBERSHIP RULE IS THE WHOLE MECHANISM, not the list. `network` fails both
20
+ * halves - the user cannot fix the registry and the next check may well succeed -
21
+ * so it stays silent and backs off, which is the entire reason a background
22
+ * updater is tolerable at all. The three below will otherwise repeat identically
23
+ * until the user does something, which is the definition of a notice worth
24
+ * showing once. Anything added here without passing both halves is the first
25
+ * step of the notification fatigue the rest of this feature is built to avoid
26
+ * (R-20).
27
+ */
28
+ export const IMMEDIATE_NOTICE_REASONS = new Set([
29
+ 'blocked-by-os',
30
+ 'no-space',
31
+ 'not-writable',
32
+ ]);
33
+ /**
34
+ * Whether the bottom row has anything to say. PURE, and deliberately NOT
35
+ * expressed as `UpdateLine` returning `null` (C-15 / P0-1 / D-19).
36
+ *
37
+ * A component that returns `null` still satisfies `if (update)` at the call site
38
+ * - the prop is a React ELEMENT, and an element is truthy however it renders.
39
+ * The row would become `<Box flexShrink={0}>{nothing}</Box>`, which in
40
+ * full-screen is ZERO rows, and the transcript silently gains a row for as long
41
+ * as the updater is idle and loses it again the moment it has news. That is
42
+ * precisely the layout shift `BottomStatusRow` exists to prevent, and
43
+ * `bottom-status-row.test.tsx`'s "holds one row in all four states" is the
44
+ * assertion it would defeat.
45
+ *
46
+ * `ActivityLine` already settles the pattern: it ALWAYS returns an element, and
47
+ * `App.tsx` decides presence at the call site with `running && !overlayNode`.
48
+ */
49
+ export function shouldRenderUpdateLine(snapshot) {
50
+ if (snapshot.phase === 'available' ||
51
+ snapshot.phase === 'installing' ||
52
+ snapshot.phase === 'ready') {
53
+ return true;
54
+ }
55
+ if (snapshot.phase === 'failed') {
56
+ if (snapshot.reason && IMMEDIATE_NOTICE_REASONS.has(snapshot.reason))
57
+ return true;
58
+ return snapshot.consecutiveFailures >= UPDATE_LIMITS.failuresBeforeNotice;
59
+ }
60
+ // `idle` / `checking` - the two phases the user must never be told about,
61
+ // EXCEPT for a rollback this session has not yet reported. Placed LAST on
62
+ // purpose: any real news above owns the row first, and the budget is one row
63
+ // (C-15). It is therefore possible for the notice never to appear on a machine
64
+ // that has an update waiting on the very next launch, which is the correct
65
+ // trade - the newer fact is the more actionable one, and the rollback is still
66
+ // in `/update status` and in the log.
67
+ return Boolean(snapshot.rolledBackFrom);
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "type": "module",
30
30
  "bin": {
31
- "aragon": "./dist/cli.js"
31
+ "aragon": "./dist/launcher.js"
32
32
  },
33
33
  "files": [
34
34
  "dist",
@@ -52,7 +52,7 @@
52
52
  "prepublishOnly": "npm run build"
53
53
  },
54
54
  "dependencies": {
55
- "@aragon-agent/core": "^0.2.9",
55
+ "@aragon-agent/core": "^0.2.11",
56
56
  "chalk": "^5.6.2",
57
57
  "cli-highlight": "^2.0.0",
58
58
  "commander": "^12.1.0",