@aragon-agent/cli 0.5.8 → 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 -747
- package/README.md +1552 -1163
- package/dist/agent/coalesce.d.ts +13 -0
- package/dist/agent/coalesce.js +50 -11
- package/dist/agent/controller.d.ts +141 -2
- package/dist/agent/controller.js +396 -4
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +245 -3
- package/dist/agent/reducer.js +490 -17
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -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 +368 -3
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +426 -2
- 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 +79 -2
- package/dist/config/cli-commands.js +185 -3
- package/dist/config/env.d.ts +15 -3
- package/dist/config/env.js +143 -3
- package/dist/config/load.d.ts +66 -0
- package/dist/config/load.js +107 -1
- package/dist/config/schema.d.ts +268 -2
- package/dist/config/schema.js +288 -1
- package/dist/config/store.js +32 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +11 -4
- package/dist/ui/App.js +359 -37
- package/dist/ui/BottomStatusRow.d.ts +65 -0
- package/dist/ui/BottomStatusRow.js +17 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +37 -0
- package/dist/ui/StatusBar.js +5 -3
- package/dist/ui/TeamPanel.d.ts +5 -4
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/TodoPanel.d.ts +5 -4
- package/dist/ui/Transcript.d.ts +26 -3
- package/dist/ui/Transcript.js +76 -10
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +20 -0
- package/dist/ui/entries/AssistantEntry.js +3 -2
- package/dist/ui/entries/DiffView.d.ts +67 -0
- package/dist/ui/entries/DiffView.js +103 -0
- package/dist/ui/entries/FastCard.d.ts +53 -0
- package/dist/ui/entries/FastCard.js +87 -0
- package/dist/ui/entries/RetryCard.d.ts +56 -0
- package/dist/ui/entries/RetryCard.js +60 -0
- package/dist/ui/entries/TeamCard.js +6 -1
- package/dist/ui/entries/ToolCard.d.ts +38 -1
- package/dist/ui/entries/ToolCard.js +68 -5
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/AppShell.d.ts +13 -22
- package/dist/ui/layout/AppShell.js +3 -38
- package/dist/ui/layout/virtual-window.d.ts +1 -1
- package/dist/ui/layout/virtual-window.js +109 -5
- package/dist/ui/overlays/HelpOverlay.js +16 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
- package/dist/ui/overlays/SettingsScreen.js +147 -1
- package/dist/ui/transcript-text.js +27 -0
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- 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
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry presentation — pure, React-free, unit-tested (llm-api-retry-backoff
|
|
3
|
+
* §6.5 / §6.6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/agent/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`). Every non-ASCII character in the rendered line comes from
|
|
7
|
+
* the `Glyphs` set the caller passes in, never from a literal here.
|
|
8
|
+
*
|
|
9
|
+
* The COUNTDOWN is derived from `resumeAt`, an absolute instant, and never from
|
|
10
|
+
* `delayMs`. Core deliberately sends the instant rather than a tick stream, so
|
|
11
|
+
* the clock belongs to whoever renders — and a card that recomputed from
|
|
12
|
+
* `delayMs` would freeze at the announced number the moment a frame was skipped.
|
|
13
|
+
*/
|
|
14
|
+
import type { Glyphs } from '../ui/glyphs.js';
|
|
15
|
+
/**
|
|
16
|
+
* Presentation bounds. NAMED `RETRY_UI`, NOT `RETRY_LIMITS_UI`: core's
|
|
17
|
+
* `RETRY_LIMITS` bounds what the mechanism can physically do, this is a column
|
|
18
|
+
* breakpoint, and a name that read as its sibling would invite the next reader to
|
|
19
|
+
* look here for the missing keys.
|
|
20
|
+
*/
|
|
21
|
+
export declare const RETRY_UI: {
|
|
22
|
+
/** Below this many columns the status-bar chip degrades to `[r3]`. */
|
|
23
|
+
readonly statusCompactCols: 100;
|
|
24
|
+
};
|
|
25
|
+
/** The five phases of the one card a turn gets. */
|
|
26
|
+
export type RetryPhase = 'waiting' | 'retrying' | 'recovered' | 'exhausted' | 'interrupted';
|
|
27
|
+
/** The shape `formatRetryLine` reads — the `kind: 'retry'` entry, structurally. */
|
|
28
|
+
export interface RetryLineInput {
|
|
29
|
+
attempt: number;
|
|
30
|
+
maxRetries: number;
|
|
31
|
+
errorType: string;
|
|
32
|
+
phase: RetryPhase;
|
|
33
|
+
resumeAt?: number;
|
|
34
|
+
totalRetries?: number;
|
|
35
|
+
elapsedMs?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whole seconds until `resumeAt`, floored at 0.
|
|
39
|
+
*
|
|
40
|
+
* `Math.ceil` rather than `round`: a countdown that shows `0s` for half a second
|
|
41
|
+
* before firing reads as a stall, and showing `1s` for slightly too long does
|
|
42
|
+
* not.
|
|
43
|
+
*/
|
|
44
|
+
export declare function secondsLeft(resumeAt: number | undefined, now: number): number;
|
|
45
|
+
/**
|
|
46
|
+
* A short headline for a provider failure.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately shorter than `formatStreamError`'s: that one is a standalone
|
|
49
|
+
* notice and has to explain what to do, while this shares one line with a
|
|
50
|
+
* counter and a countdown. Derived from `errorType` ONLY — nothing here parses
|
|
51
|
+
* the provider's message, because a human-facing sentence must never become a
|
|
52
|
+
* machine-readable contract (the rule `SubagentRun.retryable` records).
|
|
53
|
+
*/
|
|
54
|
+
export declare function retryHeadline(errorType: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* The card's single line, WITHOUT the rail glyph — `EntryView` supplies that, the
|
|
57
|
+
* same way it does for every other entry kind.
|
|
58
|
+
*
|
|
59
|
+
* `now` is a parameter rather than a `Date.now()` call so the function stays pure
|
|
60
|
+
* and the render can be asserted without a clock.
|
|
61
|
+
*/
|
|
62
|
+
export declare function formatRetryLine(entry: RetryLineInput, glyphs: Glyphs, now: number): string;
|
|
63
|
+
/** The status-bar chip text: full at width, `[r3]` when the row is tight. */
|
|
64
|
+
export declare function formatRetryChip(active: {
|
|
65
|
+
attempt: number;
|
|
66
|
+
max: number;
|
|
67
|
+
secondsLeft: number;
|
|
68
|
+
}, cols: number): string;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry presentation — pure, React-free, unit-tested (llm-api-retry-backoff
|
|
3
|
+
* §6.5 / §6.6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/agent/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`). Every non-ASCII character in the rendered line comes from
|
|
7
|
+
* the `Glyphs` set the caller passes in, never from a literal here.
|
|
8
|
+
*
|
|
9
|
+
* The COUNTDOWN is derived from `resumeAt`, an absolute instant, and never from
|
|
10
|
+
* `delayMs`. Core deliberately sends the instant rather than a tick stream, so
|
|
11
|
+
* the clock belongs to whoever renders — and a card that recomputed from
|
|
12
|
+
* `delayMs` would freeze at the announced number the moment a frame was skipped.
|
|
13
|
+
*/
|
|
14
|
+
import { formatDuration } from './usage.js';
|
|
15
|
+
/**
|
|
16
|
+
* Presentation bounds. NAMED `RETRY_UI`, NOT `RETRY_LIMITS_UI`: core's
|
|
17
|
+
* `RETRY_LIMITS` bounds what the mechanism can physically do, this is a column
|
|
18
|
+
* breakpoint, and a name that read as its sibling would invite the next reader to
|
|
19
|
+
* look here for the missing keys.
|
|
20
|
+
*/
|
|
21
|
+
export const RETRY_UI = {
|
|
22
|
+
/** Below this many columns the status-bar chip degrades to `[r3]`. */
|
|
23
|
+
statusCompactCols: 100,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Whole seconds until `resumeAt`, floored at 0.
|
|
27
|
+
*
|
|
28
|
+
* `Math.ceil` rather than `round`: a countdown that shows `0s` for half a second
|
|
29
|
+
* before firing reads as a stall, and showing `1s` for slightly too long does
|
|
30
|
+
* not.
|
|
31
|
+
*/
|
|
32
|
+
export function secondsLeft(resumeAt, now) {
|
|
33
|
+
if (resumeAt === undefined)
|
|
34
|
+
return 0;
|
|
35
|
+
return Math.max(0, Math.ceil((resumeAt - now) / 1000));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A short headline for a provider failure.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately shorter than `formatStreamError`'s: that one is a standalone
|
|
41
|
+
* notice and has to explain what to do, while this shares one line with a
|
|
42
|
+
* counter and a countdown. Derived from `errorType` ONLY — nothing here parses
|
|
43
|
+
* the provider's message, because a human-facing sentence must never become a
|
|
44
|
+
* machine-readable contract (the rule `SubagentRun.retryable` records).
|
|
45
|
+
*/
|
|
46
|
+
export function retryHeadline(errorType) {
|
|
47
|
+
switch (errorType) {
|
|
48
|
+
case 'rate_limit':
|
|
49
|
+
return 'Rate limited by the provider';
|
|
50
|
+
case 'overloaded':
|
|
51
|
+
return 'Provider overloaded';
|
|
52
|
+
case 'server_error':
|
|
53
|
+
return 'Provider error';
|
|
54
|
+
case 'network_error':
|
|
55
|
+
return 'Network error reaching the provider';
|
|
56
|
+
case 'timeout':
|
|
57
|
+
return 'The request timed out';
|
|
58
|
+
default:
|
|
59
|
+
return 'Provider call failed';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function plural(n) {
|
|
63
|
+
return n === 1 ? 'retry' : 'retries';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The card's single line, WITHOUT the rail glyph — `EntryView` supplies that, the
|
|
67
|
+
* same way it does for every other entry kind.
|
|
68
|
+
*
|
|
69
|
+
* `now` is a parameter rather than a `Date.now()` call so the function stays pure
|
|
70
|
+
* and the render can be asserted without a clock.
|
|
71
|
+
*/
|
|
72
|
+
export function formatRetryLine(entry, glyphs, now) {
|
|
73
|
+
const dot = ` ${glyphs.midDot} `;
|
|
74
|
+
const counter = `retry ${entry.attempt}/${entry.maxRetries}`;
|
|
75
|
+
const settled = entry.totalRetries ?? entry.attempt;
|
|
76
|
+
switch (entry.phase) {
|
|
77
|
+
case 'waiting':
|
|
78
|
+
return `${retryHeadline(entry.errorType)}${dot}${counter} in ${secondsLeft(entry.resumeAt, now)}s`;
|
|
79
|
+
case 'retrying':
|
|
80
|
+
return `${retryHeadline(entry.errorType)}${dot}${counter} ${glyphs.ellipsis}`;
|
|
81
|
+
case 'recovered':
|
|
82
|
+
return `recovered after ${settled} ${plural(settled)}${entry.elapsedMs === undefined ? '' : `${dot}${formatDuration(entry.elapsedMs)}`}`;
|
|
83
|
+
case 'exhausted':
|
|
84
|
+
return `${retryHeadline(entry.errorType)}${dot}gave up after ${settled} ${plural(settled)}${entry.elapsedMs === undefined ? '' : `${dot}${formatDuration(entry.elapsedMs)}`}`;
|
|
85
|
+
case 'interrupted':
|
|
86
|
+
return `interrupted after ${settled} ${plural(settled)}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The status-bar chip text: full at width, `[r3]` when the row is tight. */
|
|
90
|
+
export function formatRetryChip(active, cols) {
|
|
91
|
+
if (cols >= RETRY_UI.statusCompactCols) {
|
|
92
|
+
return `retry ${active.attempt}/${active.max} ${active.secondsLeft}s`;
|
|
93
|
+
}
|
|
94
|
+
return `[r${active.attempt}]`;
|
|
95
|
+
}
|
|
@@ -61,6 +61,17 @@ export interface SystemPromptParams {
|
|
|
61
61
|
* (and `--no-todo`) produce the pre-todo prompt BYTE FOR BYTE.
|
|
62
62
|
*/
|
|
63
63
|
todoBlock?: string;
|
|
64
|
+
/**
|
|
65
|
+
* The rendered `<fast_tier>` block, or `''` (fast-model-tier §3.8).
|
|
66
|
+
*
|
|
67
|
+
* Spliced conditionally exactly as `skillsBlock`, the plan block, `teamBlock`
|
|
68
|
+
* and `todoBlock` are, which is what keeps I-2 true: `fast.enabled: false`
|
|
69
|
+
* (and `--no-fast`) produce the pre-feature prompt BYTE FOR BYTE.
|
|
70
|
+
*
|
|
71
|
+
* A CHILD'S PROMPT NEVER CARRIES IT: children cannot delegate and are not
|
|
72
|
+
* reviewed, so `buildSubagentBlock`'s caller leaves this unset.
|
|
73
|
+
*/
|
|
74
|
+
fastBlock?: string;
|
|
64
75
|
}
|
|
65
76
|
/** Describe the active shell so the model does not emit incompatible syntax. */
|
|
66
77
|
export declare function describeShell(): {
|
|
@@ -40,6 +40,7 @@ export function buildSystemPrompt(params) {
|
|
|
40
40
|
const teamBlock = params.teamBlock ?? '';
|
|
41
41
|
const subagentBlock = params.subagentBlock ?? '';
|
|
42
42
|
const todoBlock = params.todoBlock ?? '';
|
|
43
|
+
const fastBlock = params.fastBlock ?? '';
|
|
43
44
|
return [
|
|
44
45
|
'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
|
|
45
46
|
'You help the user accomplish software-engineering tasks by reasoning step by step and using the provided tools.',
|
|
@@ -56,6 +57,7 @@ export function buildSystemPrompt(params) {
|
|
|
56
57
|
...(teamBlock ? ['', teamBlock] : []),
|
|
57
58
|
...(subagentBlock ? ['', subagentBlock] : []),
|
|
58
59
|
...(todoBlock ? ['', todoBlock] : []),
|
|
60
|
+
...(fastBlock ? ['', fastBlock] : []),
|
|
59
61
|
'',
|
|
60
62
|
'Operating guidance:',
|
|
61
63
|
'- Prefer reading files and inspecting the workspace before making changes.',
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boot guard (cli-auto-update-hardening H1 / section 5.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-19).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS LIVES BELOW `cli.tsx` AND NOT INSIDE `main()` (D-26). The instinct is
|
|
7
|
+
* to put crash recovery in `cli.tsx::main()`. That covers a crash AFTER the
|
|
8
|
+
* module graph loads and misses the class of failure that actually bricks npm
|
|
9
|
+
* CLIs: `dist/cli.js` statically imports ~60 modules, and a release that fails to
|
|
10
|
+
* IMPORT never reaches any line anyone wrote inside `main()`. An undeclared
|
|
11
|
+
* dependency, a Node API that moved, a syntax error past the transpile target -
|
|
12
|
+
* all of them raise before `main` exists.
|
|
13
|
+
*
|
|
14
|
+
* ==========================================================================
|
|
15
|
+
* THE IMPORT LIST HERE IS A CONSTRAINT, NOT A COINCIDENCE (section 4 rule 2).
|
|
16
|
+
*
|
|
17
|
+
* This module may import `node:*`, `../update/state.js` and its own sibling in
|
|
18
|
+
* `boot/` - AND NOTHING ELSE. Every module it pulls in is a module that can
|
|
19
|
+
* crash before the guard that exists to survive crashes has run.
|
|
20
|
+
*
|
|
21
|
+
* IN PARTICULAR IT MUST NOT IMPORT `logging/logger.js`. `getLogger()` resolves
|
|
22
|
+
* config on its way to a sink, so wiring it in here would drag the whole config
|
|
23
|
+
* graph into the one module that has to survive a broken build - which is
|
|
24
|
+
* exactly the failure this guard is for. So the guard is SILENT BY CONSTRUCTION
|
|
25
|
+
* (D-42); the two facts worth having after the event are recorded by
|
|
26
|
+
* `rollback.ts` and by `UpdateService`, both of which already have a logger.
|
|
27
|
+
*
|
|
28
|
+
* THE CHAIN IS NOT `node:*`-ONLY AND THE LIST MUST NOT BE READ AS CLAIMING SO
|
|
29
|
+
* (P1-2). `../update/state.js` imports `../config/app-paths.js`, which imports
|
|
30
|
+
* and CALLS `env-paths` at module scope. That third-party module is in this
|
|
31
|
+
* graph, which is why `launcher.ts` imports this file DYNAMICALLY and inside a
|
|
32
|
+
* `try` - so a release whose `node_modules` lost `env-paths` degrades to "no
|
|
33
|
+
* guard, and the CLI reports its own crash normally" rather than to an
|
|
34
|
+
* `ERR_MODULE_NOT_FOUND` from the bin entry. The residual hole is real and is
|
|
35
|
+
* stated in section 5.1.5 rather than argued away; this list is what keeps it
|
|
36
|
+
* from growing.
|
|
37
|
+
* ==========================================================================
|
|
38
|
+
*/
|
|
39
|
+
import { type UpdateState } from '../update/state.js';
|
|
40
|
+
/**
|
|
41
|
+
* `<root>/package.json`'s `version`, or `''`.
|
|
42
|
+
*
|
|
43
|
+
* Resolves the root the way `install-source.ts` does - `dirname(fileURLToPath(
|
|
44
|
+
* import.meta.url))` then up two - which from `dist/boot/` lands on the package
|
|
45
|
+
* root exactly as it does from `dist/update/`. DUPLICATED RATHER THAN IMPORTED
|
|
46
|
+
* because the import list above keeps `install-source.js` out of this graph; it
|
|
47
|
+
* is four lines of `node:*` with no path NAME in it (P2-4).
|
|
48
|
+
*
|
|
49
|
+
* The level count is load-bearing for the same reason it is there: this package
|
|
50
|
+
* is compiled by plain `tsc` with `rootDir: ./src` and NO BUNDLER (C-16), so
|
|
51
|
+
* `src/boot/guard.ts` really does land at `dist/boot/guard.js`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readOwnVersion(): string;
|
|
54
|
+
/** Injected wholesale by the tests; `runBootGuard()` uses the real ones. */
|
|
55
|
+
export interface BootGuardDeps {
|
|
56
|
+
readState?: () => UpdateState;
|
|
57
|
+
writeState?: (patch: Partial<Omit<UpdateState, 'schema'>>) => unknown;
|
|
58
|
+
version?: string;
|
|
59
|
+
/** Called instead of `performRollback`, so no test spawns npm. */
|
|
60
|
+
rollback?: (bad: string, good: string) => void;
|
|
61
|
+
onExit?: (listener: (code: number) => void) => void;
|
|
62
|
+
writeStderr?: (text: string) => void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Arm, count, disarm, or decide a rollback. Called once, from `launcher.ts`,
|
|
66
|
+
* before `dist/cli.js` is imported.
|
|
67
|
+
*
|
|
68
|
+
* READ THE ORDER AGAINST THE FAILURE IT DEFENDS:
|
|
69
|
+
*
|
|
70
|
+
* - `autoInstalledVersion !== version` IS THE ARMING CONDITION AND IT IS FIRST.
|
|
71
|
+
* That field is written by `performInstall` on success and cleared by
|
|
72
|
+
* `markBootHealthy`, so the guard is armed for exactly the window between "we
|
|
73
|
+
* installed X" and "X has been seen to start", and for no other launch of any
|
|
74
|
+
* version. Steady state - every launch on every machine except the first one
|
|
75
|
+
* after an auto-update - is ONE `readFileSync` of ~300 bytes and one string
|
|
76
|
+
* compare, with NO WRITE AT ALL. Reordering this check after anything else
|
|
77
|
+
* costs a write on every `aragon --version`.
|
|
78
|
+
*
|
|
79
|
+
* - THE GUARD ONLY CLEANS UP DAMAGE WE CAUSED (D-29). If a user's own
|
|
80
|
+
* `npm i -g @aragon-agent/cli@broken` crashes, that is their install and
|
|
81
|
+
* their decision; an updater that silently reverts it is a worse actor than
|
|
82
|
+
* one that does nothing. It also bounds the blast radius of a bug in the
|
|
83
|
+
* guard itself to one unnecessary reinstall of a version the user was running
|
|
84
|
+
* an hour ago.
|
|
85
|
+
*
|
|
86
|
+
* - THE THRESHOLD IS TESTED BEFORE THIS LAUNCH CAN CONTRIBUTE, so `2` means
|
|
87
|
+
* "two PREVIOUS launches of X exited non-zero" and the rollback happens on the
|
|
88
|
+
* third (D-30).
|
|
89
|
+
*
|
|
90
|
+
* - THE ROLLBACK DOES NOT ABORT THE BOOT (D-31). Refusing to start would be the
|
|
91
|
+
* wrong trade: if the crash was transient the user is now locked out of a
|
|
92
|
+
* working CLI by our guess. The detached child completes regardless, so the
|
|
93
|
+
* worst case is one more crash followed by a good launch.
|
|
94
|
+
*
|
|
95
|
+
* - THE COUNTER MOVES IN THE EXIT HOOK AND ONLY ON A NON-ZERO CODE (D-30a /
|
|
96
|
+
* P1-5). See `UpdateState.bootFailures`. The cost is stated rather than
|
|
97
|
+
* hidden: a release that dies WITHOUT running exit handlers (`SIGKILL`, a
|
|
98
|
+
* native segfault, `process.abort()`) is never counted and never rolled back.
|
|
99
|
+
* For a pure-JS CLI whose target failure is a module that will not import -
|
|
100
|
+
* which raises, and therefore exits `1` through the launcher's `.catch` -
|
|
101
|
+
* that is the right side of the trade.
|
|
102
|
+
*
|
|
103
|
+
* NEVER THROWS. `launcher.ts` wraps the call anyway, because a guard that throws
|
|
104
|
+
* must never be the reason the CLI will not start.
|
|
105
|
+
*/
|
|
106
|
+
export declare function runBootGuard(deps?: BootGuardDeps): void;
|
|
107
|
+
/**
|
|
108
|
+
* Disarm the guard and record this version as the one to roll back TO.
|
|
109
|
+
*
|
|
110
|
+
* Called from two places, and both matter:
|
|
111
|
+
*
|
|
112
|
+
* - `cli.tsx::runInteractive`, immediately after `render()` returns - the
|
|
113
|
+
* earliest moment at which "this build starts" is PROVEN: the module graph
|
|
114
|
+
* loaded, config resolved, Ink mounted. This is the primary, and it is what
|
|
115
|
+
* makes a long session that is later `SIGKILL`ed (terminal window closed,
|
|
116
|
+
* machine slept badly, OOM killer) still count as healthy.
|
|
117
|
+
* - the exit hook installed by `runBootGuard`, when the code is `0` - the
|
|
118
|
+
* backstop for `aragon -p`, `aragon config set`, `aragon --version` and every
|
|
119
|
+
* other path that never mounts Ink.
|
|
120
|
+
*
|
|
121
|
+
* IDEMPOTENT through the first-line guard, which is also the fast path: on every
|
|
122
|
+
* ordinary launch this is one read and one string compare.
|
|
123
|
+
*
|
|
124
|
+
* `lastGoodVersion` is set here as well as at install time, deliberately: a
|
|
125
|
+
* machine whose state file was deleted between the install and the next launch
|
|
126
|
+
* converges to a correct rollback target after one healthy run.
|
|
127
|
+
*
|
|
128
|
+
* IT DOES NOT TOUCH `rolledBackFrom`, AND IT MUST NOT (P0-1e). An earlier draft
|
|
129
|
+
* had this function clear the rollback notice; it could never have done so,
|
|
130
|
+
* because in the session AFTER a rollback `autoInstalledVersion` is `''` and this
|
|
131
|
+
* function returns on its first line before touching anything. The notice is
|
|
132
|
+
* consumed by `UpdateService.start()` instead, which is also the only place that
|
|
133
|
+
* can decide it has actually been shown (D-40).
|
|
134
|
+
*/
|
|
135
|
+
export declare function markBootHealthy(version: string, deps?: BootGuardDeps): void;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boot guard (cli-auto-update-hardening H1 / section 5.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-19).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS LIVES BELOW `cli.tsx` AND NOT INSIDE `main()` (D-26). The instinct is
|
|
7
|
+
* to put crash recovery in `cli.tsx::main()`. That covers a crash AFTER the
|
|
8
|
+
* module graph loads and misses the class of failure that actually bricks npm
|
|
9
|
+
* CLIs: `dist/cli.js` statically imports ~60 modules, and a release that fails to
|
|
10
|
+
* IMPORT never reaches any line anyone wrote inside `main()`. An undeclared
|
|
11
|
+
* dependency, a Node API that moved, a syntax error past the transpile target -
|
|
12
|
+
* all of them raise before `main` exists.
|
|
13
|
+
*
|
|
14
|
+
* ==========================================================================
|
|
15
|
+
* THE IMPORT LIST HERE IS A CONSTRAINT, NOT A COINCIDENCE (section 4 rule 2).
|
|
16
|
+
*
|
|
17
|
+
* This module may import `node:*`, `../update/state.js` and its own sibling in
|
|
18
|
+
* `boot/` - AND NOTHING ELSE. Every module it pulls in is a module that can
|
|
19
|
+
* crash before the guard that exists to survive crashes has run.
|
|
20
|
+
*
|
|
21
|
+
* IN PARTICULAR IT MUST NOT IMPORT `logging/logger.js`. `getLogger()` resolves
|
|
22
|
+
* config on its way to a sink, so wiring it in here would drag the whole config
|
|
23
|
+
* graph into the one module that has to survive a broken build - which is
|
|
24
|
+
* exactly the failure this guard is for. So the guard is SILENT BY CONSTRUCTION
|
|
25
|
+
* (D-42); the two facts worth having after the event are recorded by
|
|
26
|
+
* `rollback.ts` and by `UpdateService`, both of which already have a logger.
|
|
27
|
+
*
|
|
28
|
+
* THE CHAIN IS NOT `node:*`-ONLY AND THE LIST MUST NOT BE READ AS CLAIMING SO
|
|
29
|
+
* (P1-2). `../update/state.js` imports `../config/app-paths.js`, which imports
|
|
30
|
+
* and CALLS `env-paths` at module scope. That third-party module is in this
|
|
31
|
+
* graph, which is why `launcher.ts` imports this file DYNAMICALLY and inside a
|
|
32
|
+
* `try` - so a release whose `node_modules` lost `env-paths` degrades to "no
|
|
33
|
+
* guard, and the CLI reports its own crash normally" rather than to an
|
|
34
|
+
* `ERR_MODULE_NOT_FOUND` from the bin entry. The residual hole is real and is
|
|
35
|
+
* stated in section 5.1.5 rather than argued away; this list is what keeps it
|
|
36
|
+
* from growing.
|
|
37
|
+
* ==========================================================================
|
|
38
|
+
*/
|
|
39
|
+
import { readFileSync } from 'node:fs';
|
|
40
|
+
import { dirname, resolve } from 'node:path';
|
|
41
|
+
import process from 'node:process';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
import { readUpdateState, updateUpdateState } from '../update/state.js';
|
|
44
|
+
import { performRollback } from './rollback.js';
|
|
45
|
+
/**
|
|
46
|
+
* Non-zero exits of a version we installed before it is rolled back.
|
|
47
|
+
*
|
|
48
|
+
* DUPLICATED FROM `UPDATE_LIMITS.crashesBeforeRollback` RATHER THAN IMPORTED,
|
|
49
|
+
* and the duplication is the point: `update/limits.ts` is pure and dependency-
|
|
50
|
+
* free today, but it is a shared table that anyone may add an import to, and the
|
|
51
|
+
* import list above is worth more than one deduplicated integer. It is a bare
|
|
52
|
+
* number with no path and no name in it, so it is not the duplication
|
|
53
|
+
* `config/app-paths.ts`'s header forbids.
|
|
54
|
+
*/
|
|
55
|
+
const CRASHES_BEFORE_ROLLBACK = 2;
|
|
56
|
+
let cachedVersion = null;
|
|
57
|
+
/**
|
|
58
|
+
* `<root>/package.json`'s `version`, or `''`.
|
|
59
|
+
*
|
|
60
|
+
* Resolves the root the way `install-source.ts` does - `dirname(fileURLToPath(
|
|
61
|
+
* import.meta.url))` then up two - which from `dist/boot/` lands on the package
|
|
62
|
+
* root exactly as it does from `dist/update/`. DUPLICATED RATHER THAN IMPORTED
|
|
63
|
+
* because the import list above keeps `install-source.js` out of this graph; it
|
|
64
|
+
* is four lines of `node:*` with no path NAME in it (P2-4).
|
|
65
|
+
*
|
|
66
|
+
* The level count is load-bearing for the same reason it is there: this package
|
|
67
|
+
* is compiled by plain `tsc` with `rootDir: ./src` and NO BUNDLER (C-16), so
|
|
68
|
+
* `src/boot/guard.ts` really does land at `dist/boot/guard.js`.
|
|
69
|
+
*/
|
|
70
|
+
export function readOwnVersion() {
|
|
71
|
+
if (cachedVersion !== null)
|
|
72
|
+
return cachedVersion;
|
|
73
|
+
try {
|
|
74
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
const parsed = JSON.parse(readFileSync(resolve(here, '..', '..', 'package.json'), 'utf-8'));
|
|
76
|
+
cachedVersion = typeof parsed.version === 'string' ? parsed.version : '';
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
cachedVersion = '';
|
|
80
|
+
}
|
|
81
|
+
return cachedVersion;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Arm, count, disarm, or decide a rollback. Called once, from `launcher.ts`,
|
|
85
|
+
* before `dist/cli.js` is imported.
|
|
86
|
+
*
|
|
87
|
+
* READ THE ORDER AGAINST THE FAILURE IT DEFENDS:
|
|
88
|
+
*
|
|
89
|
+
* - `autoInstalledVersion !== version` IS THE ARMING CONDITION AND IT IS FIRST.
|
|
90
|
+
* That field is written by `performInstall` on success and cleared by
|
|
91
|
+
* `markBootHealthy`, so the guard is armed for exactly the window between "we
|
|
92
|
+
* installed X" and "X has been seen to start", and for no other launch of any
|
|
93
|
+
* version. Steady state - every launch on every machine except the first one
|
|
94
|
+
* after an auto-update - is ONE `readFileSync` of ~300 bytes and one string
|
|
95
|
+
* compare, with NO WRITE AT ALL. Reordering this check after anything else
|
|
96
|
+
* costs a write on every `aragon --version`.
|
|
97
|
+
*
|
|
98
|
+
* - THE GUARD ONLY CLEANS UP DAMAGE WE CAUSED (D-29). If a user's own
|
|
99
|
+
* `npm i -g @aragon-agent/cli@broken` crashes, that is their install and
|
|
100
|
+
* their decision; an updater that silently reverts it is a worse actor than
|
|
101
|
+
* one that does nothing. It also bounds the blast radius of a bug in the
|
|
102
|
+
* guard itself to one unnecessary reinstall of a version the user was running
|
|
103
|
+
* an hour ago.
|
|
104
|
+
*
|
|
105
|
+
* - THE THRESHOLD IS TESTED BEFORE THIS LAUNCH CAN CONTRIBUTE, so `2` means
|
|
106
|
+
* "two PREVIOUS launches of X exited non-zero" and the rollback happens on the
|
|
107
|
+
* third (D-30).
|
|
108
|
+
*
|
|
109
|
+
* - THE ROLLBACK DOES NOT ABORT THE BOOT (D-31). Refusing to start would be the
|
|
110
|
+
* wrong trade: if the crash was transient the user is now locked out of a
|
|
111
|
+
* working CLI by our guess. The detached child completes regardless, so the
|
|
112
|
+
* worst case is one more crash followed by a good launch.
|
|
113
|
+
*
|
|
114
|
+
* - THE COUNTER MOVES IN THE EXIT HOOK AND ONLY ON A NON-ZERO CODE (D-30a /
|
|
115
|
+
* P1-5). See `UpdateState.bootFailures`. The cost is stated rather than
|
|
116
|
+
* hidden: a release that dies WITHOUT running exit handlers (`SIGKILL`, a
|
|
117
|
+
* native segfault, `process.abort()`) is never counted and never rolled back.
|
|
118
|
+
* For a pure-JS CLI whose target failure is a module that will not import -
|
|
119
|
+
* which raises, and therefore exits `1` through the launcher's `.catch` -
|
|
120
|
+
* that is the right side of the trade.
|
|
121
|
+
*
|
|
122
|
+
* NEVER THROWS. `launcher.ts` wraps the call anyway, because a guard that throws
|
|
123
|
+
* must never be the reason the CLI will not start.
|
|
124
|
+
*/
|
|
125
|
+
export function runBootGuard(deps = {}) {
|
|
126
|
+
try {
|
|
127
|
+
const read = deps.readState ?? readUpdateState;
|
|
128
|
+
const write = deps.writeState ?? updateUpdateState;
|
|
129
|
+
const version = deps.version ?? readOwnVersion();
|
|
130
|
+
if (!version)
|
|
131
|
+
return;
|
|
132
|
+
const state = read();
|
|
133
|
+
// FAST PATH: not our install, or already seen healthy. No write, no hook.
|
|
134
|
+
if (state.autoInstalledVersion !== version)
|
|
135
|
+
return;
|
|
136
|
+
if (state.bootFailures >= CRASHES_BEFORE_ROLLBACK &&
|
|
137
|
+
state.lastGoodVersion &&
|
|
138
|
+
state.lastGoodVersion !== version) {
|
|
139
|
+
// WRITTEN HERE, SYNCHRONOUSLY, AND NOT INSIDE `performRollback` (P2-9).
|
|
140
|
+
// That function is `async` and its first statement is a dynamic `import`,
|
|
141
|
+
// which yields - so a line written in there would race the
|
|
142
|
+
// `await import('./cli.js')` that follows and could land below whatever
|
|
143
|
+
// the CLI prints. Written before the call, the ordering is a property of
|
|
144
|
+
// the code rather than of the module loader's timing. C-10 permits this
|
|
145
|
+
// one visible message because the TUI is not mounted yet: there is no
|
|
146
|
+
// fixed frame to shift.
|
|
147
|
+
const stderr = deps.writeStderr ?? ((text) => process.stderr.write(text));
|
|
148
|
+
stderr(`aragon ${version} failed to start twice; rolling back to ${state.lastGoodVersion}.\n`);
|
|
149
|
+
const roll = deps.rollback ??
|
|
150
|
+
((bad, good) => {
|
|
151
|
+
void performRollback(bad, good);
|
|
152
|
+
});
|
|
153
|
+
roll(version, state.lastGoodVersion);
|
|
154
|
+
// No exit hook: the rollback has already disarmed the guard, and this
|
|
155
|
+
// launch's own crash must not be counted against a cycle that is over.
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const on = deps.onExit ?? ((listener) => process.on('exit', listener));
|
|
159
|
+
on((code) => {
|
|
160
|
+
try {
|
|
161
|
+
if (code === 0)
|
|
162
|
+
markBootHealthy(version, { readState: read, writeState: write });
|
|
163
|
+
else
|
|
164
|
+
write({ bootFailures: state.bootFailures + 1 });
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// An exit hook that throws prints an unactionable stack over whatever
|
|
168
|
+
// the CLI last said. The bookkeeping is not worth that.
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// See the contract above: never throws.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Disarm the guard and record this version as the one to roll back TO.
|
|
178
|
+
*
|
|
179
|
+
* Called from two places, and both matter:
|
|
180
|
+
*
|
|
181
|
+
* - `cli.tsx::runInteractive`, immediately after `render()` returns - the
|
|
182
|
+
* earliest moment at which "this build starts" is PROVEN: the module graph
|
|
183
|
+
* loaded, config resolved, Ink mounted. This is the primary, and it is what
|
|
184
|
+
* makes a long session that is later `SIGKILL`ed (terminal window closed,
|
|
185
|
+
* machine slept badly, OOM killer) still count as healthy.
|
|
186
|
+
* - the exit hook installed by `runBootGuard`, when the code is `0` - the
|
|
187
|
+
* backstop for `aragon -p`, `aragon config set`, `aragon --version` and every
|
|
188
|
+
* other path that never mounts Ink.
|
|
189
|
+
*
|
|
190
|
+
* IDEMPOTENT through the first-line guard, which is also the fast path: on every
|
|
191
|
+
* ordinary launch this is one read and one string compare.
|
|
192
|
+
*
|
|
193
|
+
* `lastGoodVersion` is set here as well as at install time, deliberately: a
|
|
194
|
+
* machine whose state file was deleted between the install and the next launch
|
|
195
|
+
* converges to a correct rollback target after one healthy run.
|
|
196
|
+
*
|
|
197
|
+
* IT DOES NOT TOUCH `rolledBackFrom`, AND IT MUST NOT (P0-1e). An earlier draft
|
|
198
|
+
* had this function clear the rollback notice; it could never have done so,
|
|
199
|
+
* because in the session AFTER a rollback `autoInstalledVersion` is `''` and this
|
|
200
|
+
* function returns on its first line before touching anything. The notice is
|
|
201
|
+
* consumed by `UpdateService.start()` instead, which is also the only place that
|
|
202
|
+
* can decide it has actually been shown (D-40).
|
|
203
|
+
*/
|
|
204
|
+
export function markBootHealthy(version, deps = {}) {
|
|
205
|
+
try {
|
|
206
|
+
if (!version)
|
|
207
|
+
return;
|
|
208
|
+
const read = deps.readState ?? readUpdateState;
|
|
209
|
+
const write = deps.writeState ?? updateUpdateState;
|
|
210
|
+
const state = read();
|
|
211
|
+
if (state.autoInstalledVersion !== version)
|
|
212
|
+
return;
|
|
213
|
+
write({ autoInstalledVersion: '', bootFailures: 0, lastGoodVersion: version });
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Bookkeeping. Never a reason to fail a session that is otherwise healthy.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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 { type UpdateState } from '../update/state.js';
|
|
20
|
+
import type { InstallLockHandle } from '../update/install-lock.js';
|
|
21
|
+
import type { InstallResult, RunNpmInstallInput } from '../update/installer.js';
|
|
22
|
+
/** What stopped us, when something did. `undefined` on the happy path. */
|
|
23
|
+
export type RollbackFailure = 'no-package-root' | 'source-ineligible' | 'not-writable' | 'locked' | 'install-failed' | 'error';
|
|
24
|
+
export interface RollbackResult {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
failure?: RollbackFailure;
|
|
27
|
+
}
|
|
28
|
+
/** What this module resolved about the machine, injected wholesale by tests. */
|
|
29
|
+
export interface RollbackTarget {
|
|
30
|
+
root: string | null;
|
|
31
|
+
packageName: string;
|
|
32
|
+
autoInstallable: boolean;
|
|
33
|
+
writable: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface RollbackDeps {
|
|
36
|
+
readState?: () => UpdateState;
|
|
37
|
+
writeState?: (patch: Partial<Omit<UpdateState, 'schema'>>) => unknown;
|
|
38
|
+
/** Injected together; when all four are present nothing is imported at all. */
|
|
39
|
+
resolveTarget?: () => Promise<RollbackTarget>;
|
|
40
|
+
acquireLock?: () => InstallLockHandle | null;
|
|
41
|
+
install?: (input: RunNpmInstallInput) => Promise<InstallResult>;
|
|
42
|
+
log?: (level: 'info' | 'warn', msg: string, data: Record<string, unknown>) => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reinstall `good` and latch `bad` so nothing puts it back.
|
|
46
|
+
*
|
|
47
|
+
* THE ORDER OF OPERATIONS IS LOAD-BEARING.
|
|
48
|
+
*
|
|
49
|
+
* 1. LATCH FIRST, INSTALL SECOND. If the process dies between the latch and the
|
|
50
|
+
* spawn, the machine is left running a broken version that WILL NOT BE
|
|
51
|
+
* REINSTALLED - recoverable by hand. In the other order, a death between the
|
|
52
|
+
* spawn and the latch leaves a good version installed with no latch, and the
|
|
53
|
+
* next check reinstalls the bad one.
|
|
54
|
+
*
|
|
55
|
+
* `skippedVersion` is part of that write and is the reason it works across
|
|
56
|
+
* versions (C-17 / D-28): `readUpdateState` rebuilds from a fixed field list,
|
|
57
|
+
* so an OLDER `aragon` writing this file erases every field it does not know.
|
|
58
|
+
* `skippedVersion` is schema-1 and every shipped version honours it through
|
|
59
|
+
* `decideUpdate`. Per section 1.2 the ordinary rollback target is itself an H1
|
|
60
|
+
* build and preserves all four new fields; this covers the cases that
|
|
61
|
+
* argument does not reach - a hand-edited state file, a user who manually
|
|
62
|
+
* installs a pre-H1 version, and any future field that matters across the
|
|
63
|
+
* boundary.
|
|
64
|
+
*
|
|
65
|
+
* `rolledBackFrom` IS WRITTEN HERE AND NOWHERE ELSE (P0-1d). It is consumed
|
|
66
|
+
* exactly once, by `UpdateService.start()`, which is the only thing that can
|
|
67
|
+
* decide the notice has actually been shown.
|
|
68
|
+
*
|
|
69
|
+
* 2. RE-CLASSIFY BEFORE SPAWNING. The machine may have changed since the
|
|
70
|
+
* install; we do not run `npm i -g` on a tree we would not have installed
|
|
71
|
+
* into.
|
|
72
|
+
*
|
|
73
|
+
* 3. ONE NON-BLOCKING LOCK ATTEMPT (U-5b). If another process holds it, return:
|
|
74
|
+
* it is either installing or already rolling back, and two concurrent
|
|
75
|
+
* `npm i -g` on one prefix is the one thing the lock exists to stop.
|
|
76
|
+
*
|
|
77
|
+
* 4. `runNpmInstall` VERBATIM. It already validates the version against
|
|
78
|
+
* `STRICT_VERSION_RE`, spawns detached through `process.execPath`, writes to
|
|
79
|
+
* a real fd and verifies the classified root afterwards (U-6). A rollback is
|
|
80
|
+
* an install; it does not deserve a second code path.
|
|
81
|
+
*
|
|
82
|
+
* The stderr line belongs to `runBootGuard` and is written before this is called
|
|
83
|
+
* (P2-9). NEVER THROWS: the guard invokes it without `await` and cannot handle a
|
|
84
|
+
* rejection.
|
|
85
|
+
*/
|
|
86
|
+
export declare function performRollback(bad: string, good: string, deps?: RollbackDeps): Promise<RollbackResult>;
|