@aragon-agent/cli 0.5.9 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1157 -1026
- package/README.md +123 -0
- package/dist/boot/guard.d.ts +135 -0
- package/dist/boot/guard.js +218 -0
- package/dist/boot/rollback.d.ts +86 -0
- package/dist/boot/rollback.js +185 -0
- package/dist/cli.js +283 -2
- package/dist/commands/builtins.js +118 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +31 -0
- package/dist/config/app-paths.js +37 -0
- package/dist/config/cli-commands.d.ts +27 -1
- package/dist/config/cli-commands.js +51 -2
- package/dist/config/env.d.ts +10 -1
- package/dist/config/env.js +41 -1
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +25 -1
- package/dist/config/schema.d.ts +56 -0
- package/dist/config/schema.js +81 -0
- package/dist/config/store.js +13 -1
- package/dist/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +88 -2
- package/dist/ui/BottomStatusRow.d.ts +25 -3
- package/dist/ui/BottomStatusRow.js +10 -7
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/update/classify-failure.d.ts +32 -0
- package/dist/update/classify-failure.js +60 -0
- package/dist/update/install-lock.d.ts +55 -0
- package/dist/update/install-lock.js +184 -0
- package/dist/update/install-source.d.ts +97 -0
- package/dist/update/install-source.js +218 -0
- package/dist/update/installer.d.ts +108 -0
- package/dist/update/installer.js +242 -0
- package/dist/update/limits.d.ts +111 -0
- package/dist/update/limits.js +111 -0
- package/dist/update/npm-view.d.ts +62 -0
- package/dist/update/npm-view.js +133 -0
- package/dist/update/registry.d.ts +52 -0
- package/dist/update/registry.js +161 -0
- package/dist/update/semver.d.ts +89 -0
- package/dist/update/semver.js +295 -0
- package/dist/update/service.d.ts +193 -0
- package/dist/update/service.js +636 -0
- package/dist/update/state.d.ts +103 -0
- package/dist/update/state.js +142 -0
- package/dist/update/types.d.ts +169 -0
- package/dist/update/types.js +68 -0
- package/package.json +3 -3
package/dist/config/schema.js
CHANGED
|
@@ -570,6 +570,81 @@ export function clampFastConfig(raw) {
|
|
|
570
570
|
reviewMaxPerSession: clampInt(src.reviewMaxPerSession, DEFAULT_FAST_CONFIG.reviewMaxPerSession, REVIEW_SESSION_RANGE),
|
|
571
571
|
};
|
|
572
572
|
}
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Auto-update (cli-auto-update §4.1)
|
|
575
|
+
//
|
|
576
|
+
// The EIGHTH nested section in the persisted config, after `apiKeys`, `skills`,
|
|
577
|
+
// `log`, `team`, `todo`, `retry` and `fast` — and, like all of them, SCALARS
|
|
578
|
+
// ONLY, exactly one level deep. `store.ts` merges these sections BY HAND and
|
|
579
|
+
// that merge is only correct while nesting stays flat (C-5).
|
|
580
|
+
//
|
|
581
|
+
// BOTH OF `store.ts`'s MERGES MUST LEARN ABOUT THIS SECTION, AND THE OMISSION IS
|
|
582
|
+
// SILENT (C-4 / R-10). `config set update.mode notify` sends
|
|
583
|
+
// `{ update: { mode: 'notify' } }`; without the write merge the shallow
|
|
584
|
+
// top-level spread REPLACES the whole section and `checkIntervalMs`, `registry`
|
|
585
|
+
// and `distTag` silently revert on disk. This package has paid for that twice
|
|
586
|
+
// already — `todo` P0-1 and `fast` R-10 — which is why it is written down a
|
|
587
|
+
// third time rather than assumed to be common knowledge.
|
|
588
|
+
//
|
|
589
|
+
// FOUR KEYS AND NO MORE (D-16). `mode` covers on / notify / off; a fifth key
|
|
590
|
+
// (`autoInstall`) would be a second way to say the same thing, and two ways to
|
|
591
|
+
// express one setting is how they end up disagreeing.
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
/** `auto` installs; `notify` only reports; `off` disables the subsystem. */
|
|
594
|
+
export const UPDATE_MODES = ['auto', 'notify', 'off'];
|
|
595
|
+
export function isUpdateMode(v) {
|
|
596
|
+
return typeof v === 'string' && UPDATE_MODES.includes(v);
|
|
597
|
+
}
|
|
598
|
+
export const DEFAULT_UPDATE_CONFIG = {
|
|
599
|
+
mode: 'auto',
|
|
600
|
+
// FOUR HOURS, in ms. `checkIntervalMs` and not `checkIntervalHours` (D-17):
|
|
601
|
+
// every other duration in this config is in ms (`toolTimeoutMs`,
|
|
602
|
+
// `idleTimeoutMs`, `team.dispatchTimeoutMs`, `retry.initialDelayMs`), and one
|
|
603
|
+
// unit per file is worth more than one shorter number.
|
|
604
|
+
checkIntervalMs: 14_400_000,
|
|
605
|
+
registry: '',
|
|
606
|
+
distTag: 'latest',
|
|
607
|
+
};
|
|
608
|
+
/** 15 minutes to 7 days. The floor is a politeness bound on the registry. */
|
|
609
|
+
const UPDATE_INTERVAL_RANGE = { min: 900_000, max: 604_800_000 };
|
|
610
|
+
/** npm's own dist-tag grammar, loosely: no slashes, no spaces, no `@`. */
|
|
611
|
+
const DIST_TAG_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
612
|
+
/**
|
|
613
|
+
* THE single gate for every update-config read AND write, mirroring
|
|
614
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` / `clampTodoConfig`
|
|
615
|
+
* / `clampRetryConfig` / `clampFastConfig` for the reason all six record:
|
|
616
|
+
* hardening only the read path leaves a bad value on disk that reverts to the
|
|
617
|
+
* default on every launch, which presents to the user as "my setting won't
|
|
618
|
+
* stick".
|
|
619
|
+
*
|
|
620
|
+
* `registry` is CLAMPED TO `''` rather than rejected when it is not an
|
|
621
|
+
* `http(s):` URL. `''` means "derive it" (§3.3), which is the safe reading of a
|
|
622
|
+
* typo — the alternative is a background timer that throws on a value the user
|
|
623
|
+
* cannot see it reading.
|
|
624
|
+
*/
|
|
625
|
+
export function clampUpdateConfig(raw) {
|
|
626
|
+
const src = (raw && typeof raw === 'object' ? raw : {});
|
|
627
|
+
const registry = text(src.registry, DEFAULT_UPDATE_CONFIG.registry);
|
|
628
|
+
const distTag = text(src.distTag, DEFAULT_UPDATE_CONFIG.distTag);
|
|
629
|
+
return {
|
|
630
|
+
mode: isUpdateMode(src.mode) ? src.mode : DEFAULT_UPDATE_CONFIG.mode,
|
|
631
|
+
checkIntervalMs: clampInt(src.checkIntervalMs, DEFAULT_UPDATE_CONFIG.checkIntervalMs, UPDATE_INTERVAL_RANGE),
|
|
632
|
+
registry: isRegistryUrl(registry) ? registry : DEFAULT_UPDATE_CONFIG.registry,
|
|
633
|
+
distTag: DIST_TAG_RE.test(distTag) ? distTag : DEFAULT_UPDATE_CONFIG.distTag,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function isRegistryUrl(raw) {
|
|
637
|
+
if (raw.length === 0)
|
|
638
|
+
return false;
|
|
639
|
+
try {
|
|
640
|
+
const url = new URL(raw);
|
|
641
|
+
// `http:` is accepted for a LAN mirror; nothing else is a registry.
|
|
642
|
+
return url.protocol === 'https:' || url.protocol === 'http:';
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
573
648
|
export const DEFAULT_CONFIG = {
|
|
574
649
|
version: CONFIG_VERSION,
|
|
575
650
|
provider: 'anthropic',
|
|
@@ -621,6 +696,12 @@ export const DEFAULT_CONFIG = {
|
|
|
621
696
|
// falsy and so LOOKS correct — right up until `/fast on` writes a section that
|
|
622
697
|
// has no other keys in it (P0-1's shape, one feature later).
|
|
623
698
|
fast: DEFAULT_FAST_CONFIG,
|
|
699
|
+
// And one section later again, for the third time. Without this line a user
|
|
700
|
+
// whose `config.json` predates auto-update resolves `config.update.mode` to
|
|
701
|
+
// `undefined`, which is neither `'auto'` nor `'off'` — so the §3.8 gate
|
|
702
|
+
// (`cfg.update.mode !== 'off'`) would read TRUE and the feature would be on
|
|
703
|
+
// for a reason nobody could point at.
|
|
704
|
+
update: DEFAULT_UPDATE_CONFIG,
|
|
624
705
|
};
|
|
625
706
|
/**
|
|
626
707
|
* Runtime state that used to be stored in `config.json` by mistake
|
package/dist/config/store.js
CHANGED
|
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
|
|
|
14
14
|
import process from 'node:process';
|
|
15
15
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
16
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
-
import { CONFIG_VERSION, DEFAULT_CONFIG, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
17
|
+
import { CONFIG_VERSION, DEFAULT_CONFIG, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
18
18
|
/**
|
|
19
19
|
* Normalize `maxTokens` for both directions of the config file.
|
|
20
20
|
*
|
|
@@ -134,6 +134,11 @@ export function loadPersistedConfig() {
|
|
|
134
134
|
// READ half of the pair that keeps `/fast review 8` from silently switching
|
|
135
135
|
// the whole tier off on the next launch (C-3 / R-10).
|
|
136
136
|
fast: clampFastConfig({ ...DEFAULT_CONFIG.fast, ...(partial.fast ?? {}) }),
|
|
137
|
+
// `update` is the eighth. Same one-level rule, same merge — and the READ
|
|
138
|
+
// half is what supplies a whole section to every user whose `config.json`
|
|
139
|
+
// predates auto-update, which is all of them. Without it `config.update.mode`
|
|
140
|
+
// reads `undefined`, and the §3.8 gate tests `!== 'off'`.
|
|
141
|
+
update: clampUpdateConfig({ ...DEFAULT_CONFIG.update, ...(partial.update ?? {}) }),
|
|
137
142
|
};
|
|
138
143
|
}
|
|
139
144
|
/**
|
|
@@ -196,6 +201,13 @@ export function updatePersistedConfig(patch) {
|
|
|
196
201
|
// tomorrow with no error anywhere. This package has paid for that once
|
|
197
202
|
// already, in `todo` (P0-1).
|
|
198
203
|
fast: clampFastConfig({ ...current.fast, ...(patch.fast ?? {}) }),
|
|
204
|
+
// The eighth section, and the third time this file records the same trap.
|
|
205
|
+
// `config set update.mode notify` sends `{ update: { mode: 'notify' } }`;
|
|
206
|
+
// without this line the shallow top-level spread REPLACES the section,
|
|
207
|
+
// `checkIntervalMs` / `registry` / `distTag` are written absent, and an
|
|
208
|
+
// enterprise mirror the user configured last month is gone with no error
|
|
209
|
+
// anywhere. AC-20 is the assertion that proves both halves exist.
|
|
210
|
+
update: clampUpdateConfig({ ...current.update, ...(patch.update ?? {}) }),
|
|
199
211
|
};
|
|
200
212
|
writeConfigFile(merged);
|
|
201
213
|
return merged;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bin entry (cli-auto-update-hardening section 5.1.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this file is inside the glyph scanner's scope (C-19).
|
|
5
|
+
*
|
|
6
|
+
* KEEP IT THIS SMALL. Every import here is a module that can crash before the
|
|
7
|
+
* guard that exists to survive crashes. `node:process` is the only static one,
|
|
8
|
+
* and `__tests__/update-wiring.test.ts` asserts that on the source text.
|
|
9
|
+
*
|
|
10
|
+
* `dist/cli.js` IS UNCHANGED AND KEEPS ITS SHEBANG (R-17 / AC-50). Only the
|
|
11
|
+
* `bin` mapping moves; `npm start` (`node dist/cli.js`) and any user script that
|
|
12
|
+
* invokes `dist/cli.js` directly keep working byte for byte.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The bin entry (cli-auto-update-hardening section 5.1.1).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - this file is inside the glyph scanner's scope (C-19).
|
|
6
|
+
*
|
|
7
|
+
* KEEP IT THIS SMALL. Every import here is a module that can crash before the
|
|
8
|
+
* guard that exists to survive crashes. `node:process` is the only static one,
|
|
9
|
+
* and `__tests__/update-wiring.test.ts` asserts that on the source text.
|
|
10
|
+
*
|
|
11
|
+
* `dist/cli.js` IS UNCHANGED AND KEEPS ITS SHEBANG (R-17 / AC-50). Only the
|
|
12
|
+
* `bin` mapping moves; `npm start` (`node dist/cli.js`) and any user script that
|
|
13
|
+
* invokes `dist/cli.js` directly keep working byte for byte.
|
|
14
|
+
*/
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
try {
|
|
17
|
+
// DYNAMIC, AND THAT IS THE WHOLE POINT (P1-2 / R-22). A STATIC import of the
|
|
18
|
+
// guard would put `boot/guard -> update/state -> config/app-paths ->
|
|
19
|
+
// env-paths` in the bin entry's OWN module graph, and a resolution failure
|
|
20
|
+
// anywhere in that chain throws during THIS module's evaluation - outside the
|
|
21
|
+
// `try`, before any line of it runs. "An undeclared dependency" is the first
|
|
22
|
+
// way section 1.1 says a release bricks itself; a guard whose own import fails
|
|
23
|
+
// in that exact case has to degrade to "no guard", never to "no CLI".
|
|
24
|
+
const { runBootGuard } = await import('./boot/guard.js');
|
|
25
|
+
runBootGuard();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// A guard that throws must never be the reason the CLI will not start. This is
|
|
29
|
+
// the one catch in the package that is allowed to be empty: there is no logger
|
|
30
|
+
// yet, and there is nothing the user could do with the message.
|
|
31
|
+
}
|
|
32
|
+
// The `.catch` is for the IMPORT rejecting, which is precisely the import-time
|
|
33
|
+
// crash H1 exists for and which today produces an unhandled rejection with a
|
|
34
|
+
// stack the user cannot act on. `cli.js` runs `main()` at module scope and
|
|
35
|
+
// installs its own `.catch` for everything after that point.
|
|
36
|
+
//
|
|
37
|
+
// EXITING NON-ZERO HERE IS WHAT FEEDS THE GUARD'S COUNTER (D-30a): the exit hook
|
|
38
|
+
// the guard installed counts non-zero exits, and a module that will not import
|
|
39
|
+
// raises, so it arrives here.
|
|
40
|
+
await import('./cli.js').catch((err) => {
|
|
41
|
+
process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
});
|
package/dist/logging/logger.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import type { LogConfig } from '../config/schema.js';
|
|
24
24
|
import { type LogLevelName } from './levels.js';
|
|
25
25
|
/** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
|
|
26
|
-
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history';
|
|
26
|
+
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update';
|
|
27
27
|
export interface LogRecord {
|
|
28
28
|
ts: string;
|
|
29
29
|
lv: LogLevelName;
|
package/dist/ui/App.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import React from 'react';
|
|
|
8
8
|
import type { AgentController } from '../agent/controller.js';
|
|
9
9
|
import type { ConfirmRequest } from '../tools/index.js';
|
|
10
10
|
import { type Overlay, type ToastLevel } from '../agent/reducer.js';
|
|
11
|
+
import type { UpdateBridge } from '../update/types.js';
|
|
11
12
|
import { type RenderMode } from './layout/frame.js';
|
|
12
13
|
import type { MouseSource } from '../input/stdin-mouse-filter.js';
|
|
13
14
|
import type { HumanInputBridge } from '../tools/human-input.js';
|
|
@@ -49,5 +50,14 @@ export interface AppProps {
|
|
|
49
50
|
* is off: inline mode, `--no-mouse`, a non-TTY, or a filter that failed to
|
|
50
51
|
* build (mouse-wheel-region-routing §4.2). */
|
|
51
52
|
mouseSource?: MouseSource;
|
|
53
|
+
/**
|
|
54
|
+
* The auto-updater's late-arrival channel (cli-auto-update §3.8).
|
|
55
|
+
*
|
|
56
|
+
* A BRIDGE AND NOT THE SERVICE ITSELF, for the reason `confirmBridge` above is
|
|
57
|
+
* one: the service is built by a DYNAMIC `import()` fired after `render()`, so
|
|
58
|
+
* it does not exist when this component mounts. Absent under `update.mode:
|
|
59
|
+
* 'off'`, on a non-TTY, in CI, and in every test that does not exercise it.
|
|
60
|
+
*/
|
|
61
|
+
updateBridge?: UpdateBridge;
|
|
52
62
|
}
|
|
53
|
-
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, }: AppProps): React.ReactElement;
|
|
63
|
+
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }: AppProps): React.ReactElement;
|
package/dist/ui/App.js
CHANGED
|
@@ -34,6 +34,15 @@ import { TodoPanel } from './TodoPanel.js';
|
|
|
34
34
|
import { TodoStrip } from './TodoStrip.js';
|
|
35
35
|
import { ActivityLine } from './ActivityLine.js';
|
|
36
36
|
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
37
|
+
import { UpdateLine } from './UpdateLine.js';
|
|
38
|
+
// The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
|
|
39
|
+
// presence AT THE CALL SITE (C-15), and `UPDATE_LIMITS` owns the width
|
|
40
|
+
// threshold - both are values, and both are resolved here rather than inside
|
|
41
|
+
// `UpdateLine` so that component's only edge into `update/` stays erasable by
|
|
42
|
+
// tsc (cli-auto-update section 3.1 rule 1 / IF-1). Neither module opens a
|
|
43
|
+
// socket, spawns a process or creates a timer.
|
|
44
|
+
import { shouldRenderUpdateLine } from '../update/types.js';
|
|
45
|
+
import { UPDATE_LIMITS } from '../update/limits.js';
|
|
37
46
|
import { AppShell } from './layout/AppShell.js';
|
|
38
47
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
39
48
|
import { todoRailRows, todoRailWidth } from './layout/rail.js';
|
|
@@ -101,7 +110,7 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
101
110
|
function seedViewState(thinkingVisible) {
|
|
102
111
|
return initialViewState({ thinkingVisible });
|
|
103
112
|
}
|
|
104
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, }) {
|
|
113
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }) {
|
|
105
114
|
const { exit } = useApp();
|
|
106
115
|
const { stdout } = useStdout();
|
|
107
116
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -125,6 +134,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
125
134
|
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
126
135
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
127
136
|
const [confirmState, setConfirmState] = useState(null);
|
|
137
|
+
/**
|
|
138
|
+
* The updater's latest snapshot, or `null` while there is no service
|
|
139
|
+
* (cli-auto-update section 6.1).
|
|
140
|
+
*
|
|
141
|
+
* `UpdateSnapshot` is a VALUE OBJECT the service replaces wholesale on every
|
|
142
|
+
* transition, so this state can use referential equality and never diff fields.
|
|
143
|
+
*/
|
|
144
|
+
const [updateSnapshot, setUpdateSnapshot] = useState(null);
|
|
128
145
|
/** The request the question / plan overlay is currently rendering. */
|
|
129
146
|
const [humanRequest, setHumanRequest] = useState(null);
|
|
130
147
|
// Seeded from the history file, not from `CliConfig`: the list is no longer
|
|
@@ -200,6 +217,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
200
217
|
// `ViewState` is a field `restoreEntries`, `/clear`, `/reset` and the session
|
|
201
218
|
// round-trip each have to have an opinion about, and none of them has an
|
|
202
219
|
// opinion about a 3-second timer.
|
|
220
|
+
/**
|
|
221
|
+
* Whether the one `installing -> ready` toast has already been pushed
|
|
222
|
+
* (cli-auto-update section 6.3).
|
|
223
|
+
*
|
|
224
|
+
* A REF AND NOT STATE: this must survive every re-render without causing one,
|
|
225
|
+
* and it is the whole of AC-19 - ten re-renders while the phase sits at
|
|
226
|
+
* `ready` must still produce exactly one toast.
|
|
227
|
+
*/
|
|
228
|
+
const updateToastPushed = useRef(false);
|
|
203
229
|
const budgetRef = useRef(emptyBudget());
|
|
204
230
|
const followTimer = useRef(null);
|
|
205
231
|
/** True while the run in flight was started by the grace timer, not the user. */
|
|
@@ -801,6 +827,48 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
801
827
|
// --- Helpers -----------------------------------------------------------
|
|
802
828
|
const notify = (level, text) => dispatch({ type: 'notice', level, text });
|
|
803
829
|
const toast = (level, text) => dispatch({ type: 'pushToast', level, text });
|
|
830
|
+
// --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
|
|
831
|
+
//
|
|
832
|
+
// TWO ARRIVAL ORDERS, AND BOTH HAPPEN. The service is built by a dynamic
|
|
833
|
+
// `import()` fired after `render()`, so on a warm module cache it can be
|
|
834
|
+
// attached BEFORE this effect runs, and on a cold one long after. Reading
|
|
835
|
+
// `bridge.service` first and installing `onAttach` second covers both without
|
|
836
|
+
// a race - the same shape `confirmBridge` uses one screen up.
|
|
837
|
+
useEffect(() => {
|
|
838
|
+
if (!updateBridge)
|
|
839
|
+
return undefined;
|
|
840
|
+
const bridge = updateBridge;
|
|
841
|
+
let unsubscribe = null;
|
|
842
|
+
const attach = (service) => {
|
|
843
|
+
setUpdateSnapshot(service.snapshot());
|
|
844
|
+
unsubscribe = service.subscribe(setUpdateSnapshot);
|
|
845
|
+
};
|
|
846
|
+
if (bridge.service)
|
|
847
|
+
attach(bridge.service);
|
|
848
|
+
else
|
|
849
|
+
bridge.onAttach = attach;
|
|
850
|
+
return () => {
|
|
851
|
+
bridge.onAttach = null;
|
|
852
|
+
unsubscribe?.();
|
|
853
|
+
};
|
|
854
|
+
}, [updateBridge]);
|
|
855
|
+
// ONE TOAST, ON THE `installing -> ready` EDGE ONLY (section 6.3 / AC-19).
|
|
856
|
+
//
|
|
857
|
+
// It exists because the moment of completion may arrive while the user is
|
|
858
|
+
// reading the transcript with the row occupied by a run: the toast wins the row
|
|
859
|
+
// for its TTL and then hands it back to the persistent line. The ref is what
|
|
860
|
+
// makes ten re-renders at `ready` push exactly one.
|
|
861
|
+
useEffect(() => {
|
|
862
|
+
if (updateSnapshot?.phase !== 'ready')
|
|
863
|
+
return;
|
|
864
|
+
if (updateToastPushed.current)
|
|
865
|
+
return;
|
|
866
|
+
updateToastPushed.current = true;
|
|
867
|
+
toast('info', `${updateSnapshot.latestVersion ?? 'Update'} installed - restart aragon to apply.`);
|
|
868
|
+
// `toast` is a stable dispatch wrapper; depending on it would re-run this on
|
|
869
|
+
// every render and defeat the ref guard's purpose rather than its mechanism.
|
|
870
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
871
|
+
}, [updateSnapshot?.phase, updateSnapshot?.latestVersion]);
|
|
804
872
|
const persistConfig = (patch) => {
|
|
805
873
|
try {
|
|
806
874
|
updatePersistedConfig(patch);
|
|
@@ -858,6 +926,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
858
926
|
// Read at command time, so `/todo status` reports the live count rather than
|
|
859
927
|
// whatever it was when this context was built.
|
|
860
928
|
followBudget: budgetRef.current,
|
|
929
|
+
// Read at command time for the same reason, and additionally because the
|
|
930
|
+
// service ATTACHES LATE (section 3.8): a value captured when this closure was
|
|
931
|
+
// first created would be `undefined` for the whole session on a cold module
|
|
932
|
+
// cache, and `/update` would report "disabled" on a session where it is on.
|
|
933
|
+
// `?? undefined` because the bridge holds `null` and the port is optional.
|
|
934
|
+
update: updateBridge?.service ?? undefined,
|
|
861
935
|
});
|
|
862
936
|
/**
|
|
863
937
|
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
@@ -1379,7 +1453,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1379
1453
|
// surface the run is not visible on. (Inline overlays are part of the
|
|
1380
1454
|
// document flow, so there the suppression is conservative rather than
|
|
1381
1455
|
// necessary — P2-5.)
|
|
1382
|
-
activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
|
|
1456
|
+
activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
|
|
1457
|
+
// PRESENCE IS DECIDED HERE, NOT BY `UpdateLine` RETURNING `null`
|
|
1458
|
+
// (C-15 / cli-auto-update P0-1 / D-19). `BottomStatusRow` tests this
|
|
1459
|
+
// prop for truthiness, and a React element is truthy however it
|
|
1460
|
+
// renders - so a component that rendered nothing would give the row
|
|
1461
|
+
// ZERO rows and silently unbalance the frame budget. Same rule the
|
|
1462
|
+
// activity line above follows with `running && !overlayNode`.
|
|
1463
|
+
//
|
|
1464
|
+
// `!overlayNode` for the same reason it appears there: the full-screen
|
|
1465
|
+
// branch replaces the whole viewport with the overlay, and an update
|
|
1466
|
+
// notice under a settings screen is noise attached to a surface it is
|
|
1467
|
+
// not visible on.
|
|
1468
|
+
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
|
|
1383
1469
|
? {
|
|
1384
1470
|
teamActive: {
|
|
1385
1471
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -26,8 +26,13 @@
|
|
|
26
26
|
* Sharing the row means `budget.ts` and `AppShell.tsx` come out of this round
|
|
27
27
|
* UNMODIFIED — and `budget.test.ts` asserts that they did (AC-8a, DoD #8).
|
|
28
28
|
*
|
|
29
|
-
* PRECEDENCE:
|
|
30
|
-
* row nearest the input belongs
|
|
29
|
+
* PRECEDENCE: toast > activity > update > blank. A toast wins because a
|
|
30
|
+
* transient ack is a RESPONSE TO THE USER, and the row nearest the input belongs
|
|
31
|
+
* to it. The update line is LAST because it is the only one of the three that is
|
|
32
|
+
* PERSISTENT (cli-auto-update D-2): it loses nothing by waiting for the run to
|
|
33
|
+
* end, whereas an activity line deferred is an activity line that never renders.
|
|
34
|
+
* That also means a user who is actively working never sees the updater at all
|
|
35
|
+
* until they stop, which is the whole of the silence requirement.
|
|
31
36
|
*/
|
|
32
37
|
import React from 'react';
|
|
33
38
|
import type { Theme } from './theme.js';
|
|
@@ -38,6 +43,23 @@ export interface BottomStatusRowProps {
|
|
|
38
43
|
toasts: Toast[];
|
|
39
44
|
/** `<ActivityLine>` while a run owns the screen, else `null`. */
|
|
40
45
|
activity: React.ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* `<UpdateLine>` when the updater has something to say, else `null` — AND THE
|
|
48
|
+
* CALLER DECIDES WHICH (C-15 / cli-auto-update P0-1).
|
|
49
|
+
*
|
|
50
|
+
* `if (update)` below tests a REACT ELEMENT, and an element whose own render
|
|
51
|
+
* returns `null` is still truthy: that branch would emit a box of ZERO rows,
|
|
52
|
+
* the frame would keep its fixed height, Yoga would hand the row to the
|
|
53
|
+
* transcript, and `viewportRows()` would go on reporting the old number to
|
|
54
|
+
* five consumers. So `UpdateLine` is typed `React.ReactElement` and `App`
|
|
55
|
+
* gates it with `shouldRenderUpdateLine(snapshot)`.
|
|
56
|
+
*
|
|
57
|
+
* OPTIONAL, because `bottom-status-row.test.tsx` and the `App.tsx` fixtures
|
|
58
|
+
* construct this component without it and the test tree is inside
|
|
59
|
+
* `tsconfig.test.json` (`typecheck-scope.test.ts`); a required prop turns
|
|
60
|
+
* `npm run typecheck` red for a reason unrelated to this feature (P2-1).
|
|
61
|
+
*/
|
|
62
|
+
update?: React.ReactNode;
|
|
41
63
|
theme: Theme;
|
|
42
64
|
}
|
|
43
|
-
export declare function BottomStatusRow({ mode, toasts, activity, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
65
|
+
export declare function BottomStatusRow({ mode, toasts, activity, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { ToastStack } from './ToastStack.js';
|
|
4
|
-
export function BottomStatusRow({ mode, toasts, activity, theme, }) {
|
|
5
|
-
// A toast wins the row
|
|
6
|
-
|
|
7
|
-
if (toasts.length > 0 || !activity) {
|
|
4
|
+
export function BottomStatusRow({ mode, toasts, activity, update, theme, }) {
|
|
5
|
+
// A toast wins the row outright.
|
|
6
|
+
if (toasts.length > 0)
|
|
8
7
|
return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
|
|
9
|
-
}
|
|
10
8
|
// Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
|
|
11
|
-
// branch is already a conditional 0-3 row stack, and
|
|
9
|
+
// branch is already a conditional 0-3 row stack, and these are simply one more
|
|
12
10
|
// conditional row above it.
|
|
13
|
-
|
|
11
|
+
if (activity)
|
|
12
|
+
return _jsx(Box, { flexShrink: 0, children: activity });
|
|
13
|
+
if (update)
|
|
14
|
+
return _jsx(Box, { flexShrink: 0, children: update });
|
|
15
|
+
// The blank full-screen row that IS the budget must keep being emitted.
|
|
16
|
+
return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
|
|
14
17
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The update line (cli-auto-update section 6.2) - the THIRD occupant of the
|
|
3
|
+
* already-budgeted bottom row, after the toast stack and the activity line.
|
|
4
|
+
*
|
|
5
|
+
* ONE ROW, ALWAYS, AND NEVER `null` (C-15 / P0-1 / D-19). The return type is
|
|
6
|
+
* `React.ReactElement` rather than `React.ReactElement | null`, so the compiler
|
|
7
|
+
* refuses the shortcut: `BottomStatusRow` decides presence with `if (update)`,
|
|
8
|
+
* which tests a REACT ELEMENT, and an element whose render returns `null` is
|
|
9
|
+
* still truthy. Taking that branch would emit a box of ZERO rows - the frame
|
|
10
|
+
* keeps its fixed height, Yoga hands the row to the transcript, and
|
|
11
|
+
* `viewportRows()` goes on reporting the old number to `ScrollViewport`,
|
|
12
|
+
* `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`. The
|
|
13
|
+
* transcript would gain a row while the updater is idle and lose it the moment
|
|
14
|
+
* it has news. Presence is the CALLER's decision, through
|
|
15
|
+
* `shouldRenderUpdateLine`, exactly as `App.tsx` already decides the activity
|
|
16
|
+
* line's with `running && !overlayNode`.
|
|
17
|
+
*
|
|
18
|
+
* PURE: a function of `snapshot + compact + theme + caps`. No state, no timer,
|
|
19
|
+
* no clock. `<Text wrap="truncate">` so a narrow terminal degrades by truncation
|
|
20
|
+
* rather than by wrapping into a second row the fixed frame cannot afford.
|
|
21
|
+
*
|
|
22
|
+
* ASCII ONLY, and every user-visible character comes from `pickGlyphs(caps)`
|
|
23
|
+
* (C-3). No literal `...`, `+` or `~` may be spelled inline - `ActivityLine`'s
|
|
24
|
+
* routing of even the ellipsis through `pickGlyphs` is the precedent.
|
|
25
|
+
*
|
|
26
|
+
* `compact` RATHER THAN `cols` (IF-1). Section 6.2 describes the width tiers in
|
|
27
|
+
* terms of `cols`, but resolving them here would mean importing
|
|
28
|
+
* `UPDATE_LIMITS.statusCompactCols` - a VALUE - from `update/`, and section 3.1
|
|
29
|
+
* rule 1 requires this module's only edge into that tree to be erasable by tsc.
|
|
30
|
+
* The caller owns `UPDATE_LIMITS` already (it computes presence from it), so it
|
|
31
|
+
* resolves the threshold too. Same rule as presence: the call site decides.
|
|
32
|
+
*/
|
|
33
|
+
import React from 'react';
|
|
34
|
+
import type { Theme } from './theme.js';
|
|
35
|
+
import type { TermCapabilities } from './capabilities.js';
|
|
36
|
+
import type { UpdateSnapshot } from '../update/types.js';
|
|
37
|
+
export interface UpdateLineProps {
|
|
38
|
+
snapshot: UpdateSnapshot;
|
|
39
|
+
/** `cols < UPDATE_LIMITS.statusCompactCols`, resolved by the caller. */
|
|
40
|
+
compact: boolean;
|
|
41
|
+
theme: Theme;
|
|
42
|
+
caps: TermCapabilities;
|
|
43
|
+
}
|
|
44
|
+
export declare function UpdateLine({ snapshot, compact, theme, caps }: UpdateLineProps): React.ReactElement;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { pickGlyphs } from './glyphs.js';
|
|
4
|
+
export function UpdateLine({ snapshot, compact, theme, caps }) {
|
|
5
|
+
const glyphs = pickGlyphs(caps);
|
|
6
|
+
const version = snapshot.latestVersion ?? snapshot.currentVersion;
|
|
7
|
+
const advice = snapshot.advice ?? '';
|
|
8
|
+
const dot = glyphs.midDot;
|
|
9
|
+
const line = (() => {
|
|
10
|
+
if (snapshot.phase === 'installing') {
|
|
11
|
+
return {
|
|
12
|
+
glyph: glyphs.arrowUp,
|
|
13
|
+
text: compact
|
|
14
|
+
? `${version}${glyphs.ellipsis}`
|
|
15
|
+
: `updating to ${version}${glyphs.ellipsis}`,
|
|
16
|
+
color: theme.muted,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (snapshot.phase === 'ready') {
|
|
20
|
+
return {
|
|
21
|
+
glyph: glyphs.check,
|
|
22
|
+
text: compact ? `${version} ready` : `${version} installed ${dot} restart aragon to apply`,
|
|
23
|
+
color: theme.toast.success,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (snapshot.phase === 'failed') {
|
|
27
|
+
// H2's three classified failures (cli-auto-update-hardening section 8).
|
|
28
|
+
// Each names the REAL obstacle rather than the generic one, because each
|
|
29
|
+
// is allowed to break the three-strike silence and a notice that breaks
|
|
30
|
+
// silence must be worth the interruption.
|
|
31
|
+
if (snapshot.reason === 'blocked-by-os') {
|
|
32
|
+
// `npm i -g` FROM THE SAME SHELL HITS THE SAME HANDLE. On Windows the
|
|
33
|
+
// shim that launched us is being read by the `cmd.exe` running it for
|
|
34
|
+
// as long as our process lives, and npm's install rewrites that shim -
|
|
35
|
+
// so the generic advice is not merely unhelpful here, it is a wrong
|
|
36
|
+
// instruction (R-3 / section 5.3.1).
|
|
37
|
+
return {
|
|
38
|
+
glyph: glyphs.warn,
|
|
39
|
+
text: compact || !advice
|
|
40
|
+
? 'update blocked'
|
|
41
|
+
: `update blocked ${dot} close other aragon windows, then: ${advice}`,
|
|
42
|
+
color: theme.noticeWarn,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (snapshot.reason === 'no-space') {
|
|
46
|
+
return {
|
|
47
|
+
glyph: glyphs.warn,
|
|
48
|
+
text: compact ? 'no disk space' : `update failed ${dot} no disk space`,
|
|
49
|
+
color: theme.noticeWarn,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (snapshot.reason === 'not-writable') {
|
|
53
|
+
return {
|
|
54
|
+
glyph: glyphs.warn,
|
|
55
|
+
text: compact || !advice
|
|
56
|
+
? 'update blocked'
|
|
57
|
+
: `update needs write access ${dot} ${advice}`,
|
|
58
|
+
color: theme.noticeWarn,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
glyph: glyphs.warn,
|
|
63
|
+
text: compact || !advice ? 'update failed' : `update failed ${dot} ${advice}`,
|
|
64
|
+
color: theme.noticeWarn,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (snapshot.phase === 'available') {
|
|
68
|
+
if (snapshot.reason === 'node-too-old') {
|
|
69
|
+
// Both numbers or neither: "needs a newer Node" without saying which is
|
|
70
|
+
// a notice the user cannot act on.
|
|
71
|
+
const detail = snapshot.requiredNode && snapshot.runningNode
|
|
72
|
+
? `${version} needs Node ${snapshot.requiredNode} (running ${snapshot.runningNode})`
|
|
73
|
+
: `${version} needs a newer Node`;
|
|
74
|
+
return {
|
|
75
|
+
glyph: glyphs.warn,
|
|
76
|
+
text: compact ? 'needs newer Node' : detail,
|
|
77
|
+
color: theme.noticeWarn,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (snapshot.reason === 'install-ineffective') {
|
|
81
|
+
return {
|
|
82
|
+
glyph: glyphs.warn,
|
|
83
|
+
text: compact
|
|
84
|
+
? 'check npm prefix'
|
|
85
|
+
: `${version} installed elsewhere${advice ? ` ${dot} ${advice}` : ''}`,
|
|
86
|
+
color: theme.noticeWarn,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
glyph: glyphs.arrowUp,
|
|
91
|
+
text: compact
|
|
92
|
+
? version
|
|
93
|
+
: `${version} available${advice ? ` ${dot} ${advice}` : ''}`,
|
|
94
|
+
color: theme.muted,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// H1's rollback notice, and it goes HERE rather than in a phase branch
|
|
98
|
+
// (cli-auto-update-hardening section 8 / P0-1). It is true for a whole
|
|
99
|
+
// SESSION rather than for a phase, so `shouldRenderUpdateLine` decides its
|
|
100
|
+
// presence from `snapshot.rolledBackFrom` outside `phase` entirely - and the
|
|
101
|
+
// phases that leaves are exactly `idle` / `checking`, which is this
|
|
102
|
+
// fallthrough. An earlier draft put it in the `failed` / `available`
|
|
103
|
+
// branches and keyed it on phase `idle`, a phase neither branch handles;
|
|
104
|
+
// three independent suppressions, so the one line telling a user their CLI
|
|
105
|
+
// had been silently downgraded could never have appeared.
|
|
106
|
+
//
|
|
107
|
+
// NOT `version` ABOVE: that is `latestVersion ?? currentVersion`, and after
|
|
108
|
+
// a rollback `latestVersion` may well be the BAD release we just escaped.
|
|
109
|
+
if (snapshot.rolledBackFrom) {
|
|
110
|
+
const to = snapshot.rolledBackTo ?? snapshot.currentVersion;
|
|
111
|
+
return {
|
|
112
|
+
glyph: glyphs.warn,
|
|
113
|
+
text: compact
|
|
114
|
+
? 'rolled back'
|
|
115
|
+
: `rolled back to ${to} after ${snapshot.rolledBackFrom} failed to start`,
|
|
116
|
+
color: theme.noticeWarn,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// `idle` / `checking`, and a `failed` below the notice threshold. The CALLER
|
|
120
|
+
// never mounts us here (`shouldRenderUpdateLine` is false), but the row must
|
|
121
|
+
// still be exactly one row if it ever is - the same blank-row discipline
|
|
122
|
+
// `ToastStack` applies to its own budgeted row.
|
|
123
|
+
return null;
|
|
124
|
+
})();
|
|
125
|
+
if (!line) {
|
|
126
|
+
return (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: " " }) }));
|
|
127
|
+
}
|
|
128
|
+
return (_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: line.color, children: [' ', line.glyph, " ", line.text] }) }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a tail of npm output into a reason (cli-auto-update-hardening H2 /
|
|
3
|
+
* section 5.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
|
|
6
|
+
*
|
|
7
|
+
* PURE, AND THE ONLY IMPORT IS A TYPE. The interesting half of H2 is not this
|
|
8
|
+
* table, it is the REPORTING rule that decides which of the resulting reasons
|
|
9
|
+
* may break the three-strike silence - and that lives in `types.ts` beside
|
|
10
|
+
* `shouldRenderUpdateLine`, as `IMMEDIATE_NOTICE_REASONS`.
|
|
11
|
+
*
|
|
12
|
+
* WHY THIS EXISTS AT ALL. The one line a user is ever allowed to see on the
|
|
13
|
+
* failure path reads `update failed <dot> npm i -g @aragon-agent/cli` for EVERY
|
|
14
|
+
* cause. For `EPERM` / `EBUSY` - the Windows self-replacement case, R-3 - that
|
|
15
|
+
* advice hits the same held handle and fails identically, so the single visible
|
|
16
|
+
* failure is also a wrong instruction.
|
|
17
|
+
*/
|
|
18
|
+
import type { UpdateReason } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Classify an install failure from the bounded stderr tail `runNpmInstall`
|
|
21
|
+
* already returns.
|
|
22
|
+
*
|
|
23
|
+
* ONE PARAMETER, NOT TWO. An earlier draft took `exitCode` and never read it:
|
|
24
|
+
* npm exits `1` for every one of these, so the code carries no information the
|
|
25
|
+
* tail does not, and a parameter that no branch and no test consumes is a
|
|
26
|
+
* maintenance liability that reads like a promise (P2-2).
|
|
27
|
+
*
|
|
28
|
+
* An absent or unrecognised tail is `install-failed` - the pre-H2 behaviour, so
|
|
29
|
+
* a machine whose npm says something we have never seen degrades to exactly what
|
|
30
|
+
* it did before rather than to a confident wrong answer.
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyInstallFailure(tail: string | undefined): UpdateReason;
|