@aragon-agent/cli 0.5.9 → 0.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1157 -1026
- package/README.md +123 -0
- package/dist/boot/guard.d.ts +135 -0
- package/dist/boot/guard.js +218 -0
- package/dist/boot/rollback.d.ts +86 -0
- package/dist/boot/rollback.js +185 -0
- package/dist/cli.js +376 -2
- package/dist/commands/builtins.js +118 -0
- package/dist/commands/perf.d.ts +5 -1
- package/dist/commands/perf.js +43 -1
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +31 -0
- package/dist/config/app-paths.js +37 -0
- package/dist/config/cli-commands.d.ts +27 -1
- package/dist/config/cli-commands.js +51 -2
- package/dist/config/env.d.ts +11 -1
- package/dist/config/env.js +58 -1
- package/dist/config/load.d.ts +30 -0
- package/dist/config/load.js +56 -1
- package/dist/config/schema.d.ts +91 -0
- package/dist/config/schema.js +107 -0
- package/dist/config/store.js +13 -1
- package/dist/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +88 -2
- package/dist/ui/BottomStatusRow.d.ts +25 -3
- package/dist/ui/BottomStatusRow.js +10 -7
- package/dist/ui/PromptInput.d.ts +10 -0
- package/dist/ui/PromptInput.js +41 -48
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/ui/editor-reducer.d.ts +62 -0
- package/dist/ui/editor-reducer.js +90 -0
- package/dist/ui/frame-differ.d.ts +95 -0
- package/dist/ui/frame-differ.js +230 -0
- package/dist/ui/stdout-frame-writer.d.ts +41 -0
- package/dist/ui/stdout-frame-writer.js +102 -0
- package/dist/update/classify-failure.d.ts +32 -0
- package/dist/update/classify-failure.js +60 -0
- package/dist/update/install-lock.d.ts +55 -0
- package/dist/update/install-lock.js +184 -0
- package/dist/update/install-source.d.ts +97 -0
- package/dist/update/install-source.js +218 -0
- package/dist/update/installer.d.ts +108 -0
- package/dist/update/installer.js +242 -0
- package/dist/update/limits.d.ts +111 -0
- package/dist/update/limits.js +111 -0
- package/dist/update/npm-view.d.ts +62 -0
- package/dist/update/npm-view.js +133 -0
- package/dist/update/registry.d.ts +52 -0
- package/dist/update/registry.js +161 -0
- package/dist/update/semver.d.ts +89 -0
- package/dist/update/semver.js +295 -0
- package/dist/update/service.d.ts +193 -0
- package/dist/update/service.js +636 -0
- package/dist/update/state.d.ts +103 -0
- package/dist/update/state.js +142 -0
- package/dist/update/types.d.ts +169 -0
- package/dist/update/types.js +68 -0
- package/package.json +3 -3
package/dist/ui/PromptInput.js
CHANGED
|
@@ -8,13 +8,24 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
* The buffer/cursor transforms (`applyEdit`, `moveVertical`) and the suggestion
|
|
9
9
|
* computations (`slashSuggestions`, `fileTokenAt`) are pure module functions,
|
|
10
10
|
* exported for `input.test.ts`, so the component itself stays thin.
|
|
11
|
+
*
|
|
12
|
+
* THE EDITOR'S FIVE PIECES OF STATE LIVE IN ONE REDUCER, and every branch of the
|
|
13
|
+
* input handler performs AT MOST ONE `dispatch` (tui-input-flicker-fix §3.4).
|
|
14
|
+
* Ink mounts a LEGACY React root, where `setState` outside a React event handler
|
|
15
|
+
* is not batched — and `useInput` runs from a stdin `'data'` listener, which is
|
|
16
|
+
* outside React entirely. The five `setState` calls one keystroke used to make
|
|
17
|
+
* were five commits, five whole-tree Yoga layouts and, through Ink's 32 ms
|
|
18
|
+
* leading+trailing throttle, two full-frame repaints per character. `fileMatches`
|
|
19
|
+
* stays a plain `useState` because it is written from the debounced glob effect
|
|
20
|
+
* rather than from a key.
|
|
11
21
|
*/
|
|
12
|
-
import { useEffect, useRef, useState } from 'react';
|
|
22
|
+
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
13
23
|
import { Box, Text, useInput } from 'ink';
|
|
14
24
|
import { glob as tinyGlob } from 'tinyglobby';
|
|
15
25
|
import { pickGlyphs } from './glyphs.js';
|
|
16
26
|
import { recognize } from '../input/keymap.js';
|
|
17
27
|
import { AutocompletePopup } from './AutocompletePopup.js';
|
|
28
|
+
import { editorReducer, INITIAL_EDITOR_STATE } from './editor-reducer.js';
|
|
18
29
|
// ---------------------------------------------------------------------------
|
|
19
30
|
// Pure editing helpers (unit-tested in input.test.ts)
|
|
20
31
|
// ---------------------------------------------------------------------------
|
|
@@ -128,11 +139,8 @@ function isControlSeq(input) {
|
|
|
128
139
|
// Component
|
|
129
140
|
// ---------------------------------------------------------------------------
|
|
130
141
|
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }) {
|
|
131
|
-
const [
|
|
132
|
-
const
|
|
133
|
-
const [historyIndex, setHistoryIndex] = useState(null);
|
|
134
|
-
const [dismissed, setDismissed] = useState(false);
|
|
135
|
-
const [sel, setSel] = useState(0);
|
|
142
|
+
const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
|
|
143
|
+
const { buffer, cursor, historyIndex, dismissed, sel } = editor;
|
|
136
144
|
const [fileMatches, setFileMatches] = useState([]);
|
|
137
145
|
// --- Derived suggestion state (shared by render + input handler). --------
|
|
138
146
|
const slashSug = dismissed ? null : slashSuggestions(buffer, commands);
|
|
@@ -189,46 +197,36 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
189
197
|
onDraftChange(hasDraft);
|
|
190
198
|
}, [hasDraft, onDraftChange]);
|
|
191
199
|
// --- Mutators. -----------------------------------------------------------
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
setSel(0);
|
|
196
|
-
};
|
|
200
|
+
// ONE DISPATCH EACH, and that is the rule §3.4 asks a reviewer to check line
|
|
201
|
+
// by line. Two dispatches from one key would put the legacy root back to two
|
|
202
|
+
// commits and undo the whole of F3 for that branch.
|
|
197
203
|
const insert = (text) => {
|
|
198
|
-
|
|
199
|
-
setCursor(cursor + text.length);
|
|
200
|
-
resetDraftFlags();
|
|
204
|
+
dispatch({ type: 'insert', text });
|
|
201
205
|
};
|
|
202
206
|
const submit = (text) => {
|
|
203
207
|
onSubmit(text);
|
|
204
|
-
|
|
205
|
-
setCursor(0);
|
|
206
|
-
resetDraftFlags();
|
|
208
|
+
dispatch({ type: 'clear' });
|
|
207
209
|
};
|
|
208
210
|
const recallUp = () => {
|
|
209
211
|
if (history.length === 0)
|
|
210
212
|
return;
|
|
211
213
|
const idx = historyIndex === null ? history.length - 1 : Math.max(0, historyIndex - 1);
|
|
212
214
|
const value = history[idx] ?? '';
|
|
213
|
-
|
|
214
|
-
setBuffer(value);
|
|
215
|
-
setCursor(value.length);
|
|
215
|
+
dispatch({ type: 'recall', buffer: value, cursor: value.length, historyIndex: idx });
|
|
216
216
|
};
|
|
217
217
|
const recallDown = () => {
|
|
218
218
|
if (historyIndex === null)
|
|
219
219
|
return;
|
|
220
220
|
const idx = historyIndex + 1;
|
|
221
|
+
// Past the newest entry the walk ends and the draft goes back to empty —
|
|
222
|
+
// `historyIndex: null` is what tells `verticalOrHistory` the user is editing
|
|
223
|
+
// again rather than still stepping.
|
|
221
224
|
if (idx >= history.length) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
setCursor(0);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
const value = history[idx] ?? '';
|
|
228
|
-
setHistoryIndex(idx);
|
|
229
|
-
setBuffer(value);
|
|
230
|
-
setCursor(value.length);
|
|
225
|
+
dispatch({ type: 'recall', buffer: '', cursor: 0, historyIndex: null });
|
|
226
|
+
return;
|
|
231
227
|
}
|
|
228
|
+
const value = history[idx] ?? '';
|
|
229
|
+
dispatch({ type: 'recall', buffer: value, cursor: value.length, historyIndex: idx });
|
|
232
230
|
};
|
|
233
231
|
const completeSelection = () => {
|
|
234
232
|
const chosen = popupItems[clampedSel];
|
|
@@ -236,20 +234,17 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
236
234
|
return;
|
|
237
235
|
if (popupKind === 'slash') {
|
|
238
236
|
const next = `${chosen.label} `;
|
|
239
|
-
|
|
240
|
-
setCursor(next.length);
|
|
237
|
+
dispatch({ type: 'replace', buffer: next, cursor: next.length });
|
|
241
238
|
}
|
|
242
239
|
else if (popupKind === 'file' && fileTok) {
|
|
243
240
|
const next = buffer.slice(0, fileTok.start) + chosen.label + buffer.slice(fileTok.end);
|
|
244
|
-
|
|
245
|
-
setCursor(fileTok.start + chosen.label.length);
|
|
241
|
+
dispatch({ type: 'replace', buffer: next, cursor: fileTok.start + chosen.label.length });
|
|
246
242
|
}
|
|
247
|
-
resetDraftFlags();
|
|
248
243
|
};
|
|
249
244
|
const verticalOrHistory = (dir) => {
|
|
250
245
|
const moved = moveVertical(buffer, cursor, dir);
|
|
251
246
|
if (moved) {
|
|
252
|
-
|
|
247
|
+
dispatch({ type: 'moveCursor', cursor: moved.cursor });
|
|
253
248
|
return;
|
|
254
249
|
}
|
|
255
250
|
// At an edge: recall history only from an empty buffer or while already
|
|
@@ -274,11 +269,11 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
274
269
|
// Popup navigation owns Up/Down/Tab/→/Enter/Esc while it is open.
|
|
275
270
|
if (popupItems.length > 0) {
|
|
276
271
|
if (key.upArrow) {
|
|
277
|
-
|
|
272
|
+
dispatch({ type: 'select', sel: Math.max(0, clampedSel - 1) });
|
|
278
273
|
return;
|
|
279
274
|
}
|
|
280
275
|
if (key.downArrow) {
|
|
281
|
-
|
|
276
|
+
dispatch({ type: 'select', sel: Math.min(popupItems.length - 1, clampedSel + 1) });
|
|
282
277
|
return;
|
|
283
278
|
}
|
|
284
279
|
if (key.tab || (key.rightArrow && cursor >= buffer.length)) {
|
|
@@ -292,7 +287,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
292
287
|
return;
|
|
293
288
|
}
|
|
294
289
|
if (key.escape) {
|
|
295
|
-
|
|
290
|
+
dispatch({ type: 'dismiss' });
|
|
296
291
|
return;
|
|
297
292
|
}
|
|
298
293
|
// Any other key falls through so typing keeps filtering the popup.
|
|
@@ -303,9 +298,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
303
298
|
const intent = recognize(input, key);
|
|
304
299
|
if (intent) {
|
|
305
300
|
const next = applyEdit(buffer, cursor, intent);
|
|
306
|
-
|
|
307
|
-
setCursor(next.cursor);
|
|
308
|
-
resetDraftFlags();
|
|
301
|
+
dispatch({ type: 'replace', buffer: next.buffer, cursor: next.cursor });
|
|
309
302
|
return;
|
|
310
303
|
}
|
|
311
304
|
if (key.ctrl)
|
|
@@ -321,19 +314,19 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
321
314
|
return;
|
|
322
315
|
}
|
|
323
316
|
if (key.backspace || key.delete) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
317
|
+
// The `cursor > 0` guard lives in the reducer now, so this branch is one
|
|
318
|
+
// unconditional dispatch and a no-op at the left edge returns the same
|
|
319
|
+
// state object — which React bails out on, exactly as the old
|
|
320
|
+
// `if (cursor > 0)` skipped the `setState`s.
|
|
321
|
+
dispatch({ type: 'backspace' });
|
|
329
322
|
return;
|
|
330
323
|
}
|
|
331
324
|
if (key.leftArrow) {
|
|
332
|
-
|
|
325
|
+
dispatch({ type: 'moveCursor', cursor: Math.max(0, cursor - 1) });
|
|
333
326
|
return;
|
|
334
327
|
}
|
|
335
328
|
if (key.rightArrow) {
|
|
336
|
-
|
|
329
|
+
dispatch({ type: 'moveCursor', cursor: Math.min(buffer.length, cursor + 1) });
|
|
337
330
|
return;
|
|
338
331
|
}
|
|
339
332
|
// `!key.shift` is load-bearing: Shift+↑/↓ is the viewport's line-scroll
|
|
@@ -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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer editor state — F3 of tui-input-flicker-fix (§3.4).
|
|
3
|
+
*
|
|
4
|
+
* Ink creates its container with tag `0` — a LEGACY React root
|
|
5
|
+
* (`ink/build/ink.js:59-61`). In a legacy root, `setState` called outside a
|
|
6
|
+
* React event handler is NOT batched: each call schedules and immediately
|
|
7
|
+
* flushes a synchronous render. `useInput`'s callback runs from a stdin
|
|
8
|
+
* `'data'` listener, which is outside React entirely, so `PromptInput.insert`'s
|
|
9
|
+
* five `setState` calls became five commits, five whole-tree Yoga layouts, and
|
|
10
|
+
* — through Ink's 32 ms leading+trailing throttle — TWO full-frame repaints per
|
|
11
|
+
* printable character (§2 R3). The flicker got worse the faster the user typed,
|
|
12
|
+
* which is exactly what the report described.
|
|
13
|
+
*
|
|
14
|
+
* Collapsing the five always-together pieces of state into one reducer makes a
|
|
15
|
+
* keystroke cost one commit. `fileMatches` deliberately stays a separate
|
|
16
|
+
* `useState` in the component: it is written from the debounced async glob
|
|
17
|
+
* effect, not from the key handler, so folding it in here would couple two
|
|
18
|
+
* unrelated update sources.
|
|
19
|
+
*
|
|
20
|
+
* THE RULE A REVIEWER SHOULD CHECK LINE BY LINE: every branch of the `useInput`
|
|
21
|
+
* callback performs at most ONE `dispatch` and then returns.
|
|
22
|
+
*
|
|
23
|
+
* Extracted from the component so it is unit-testable without mounting Ink.
|
|
24
|
+
*/
|
|
25
|
+
export interface EditorState {
|
|
26
|
+
buffer: string;
|
|
27
|
+
cursor: number;
|
|
28
|
+
historyIndex: number | null;
|
|
29
|
+
dismissed: boolean;
|
|
30
|
+
sel: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A `type` and not an `interface` because it is a discriminated union — the one
|
|
34
|
+
* documented exception to this package's interface-first convention.
|
|
35
|
+
*/
|
|
36
|
+
export type EditorAction = {
|
|
37
|
+
type: 'insert';
|
|
38
|
+
text: string;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'replace';
|
|
41
|
+
buffer: string;
|
|
42
|
+
cursor: number;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'backspace';
|
|
45
|
+
} | {
|
|
46
|
+
type: 'moveCursor';
|
|
47
|
+
cursor: number;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'recall';
|
|
50
|
+
buffer: string;
|
|
51
|
+
cursor: number;
|
|
52
|
+
historyIndex: number | null;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'select';
|
|
55
|
+
sel: number;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'dismiss';
|
|
58
|
+
} | {
|
|
59
|
+
type: 'clear';
|
|
60
|
+
};
|
|
61
|
+
export declare const INITIAL_EDITOR_STATE: EditorState;
|
|
62
|
+
export declare function editorReducer(state: EditorState, action: EditorAction): EditorState;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer editor state — F3 of tui-input-flicker-fix (§3.4).
|
|
3
|
+
*
|
|
4
|
+
* Ink creates its container with tag `0` — a LEGACY React root
|
|
5
|
+
* (`ink/build/ink.js:59-61`). In a legacy root, `setState` called outside a
|
|
6
|
+
* React event handler is NOT batched: each call schedules and immediately
|
|
7
|
+
* flushes a synchronous render. `useInput`'s callback runs from a stdin
|
|
8
|
+
* `'data'` listener, which is outside React entirely, so `PromptInput.insert`'s
|
|
9
|
+
* five `setState` calls became five commits, five whole-tree Yoga layouts, and
|
|
10
|
+
* — through Ink's 32 ms leading+trailing throttle — TWO full-frame repaints per
|
|
11
|
+
* printable character (§2 R3). The flicker got worse the faster the user typed,
|
|
12
|
+
* which is exactly what the report described.
|
|
13
|
+
*
|
|
14
|
+
* Collapsing the five always-together pieces of state into one reducer makes a
|
|
15
|
+
* keystroke cost one commit. `fileMatches` deliberately stays a separate
|
|
16
|
+
* `useState` in the component: it is written from the debounced async glob
|
|
17
|
+
* effect, not from the key handler, so folding it in here would couple two
|
|
18
|
+
* unrelated update sources.
|
|
19
|
+
*
|
|
20
|
+
* THE RULE A REVIEWER SHOULD CHECK LINE BY LINE: every branch of the `useInput`
|
|
21
|
+
* callback performs at most ONE `dispatch` and then returns.
|
|
22
|
+
*
|
|
23
|
+
* Extracted from the component so it is unit-testable without mounting Ink.
|
|
24
|
+
*/
|
|
25
|
+
export const INITIAL_EDITOR_STATE = {
|
|
26
|
+
buffer: '',
|
|
27
|
+
cursor: 0,
|
|
28
|
+
historyIndex: null,
|
|
29
|
+
dismissed: false,
|
|
30
|
+
sel: 0,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The three flags `resetDraftFlags()` used to clear with three separate
|
|
34
|
+
* `setState` calls.
|
|
35
|
+
*
|
|
36
|
+
* Every action that CHANGES THE BUFFER resets them, and that is the pre-existing
|
|
37
|
+
* semantics rather than a new rule: an edit invalidates the history cursor (the
|
|
38
|
+
* draft is no longer the recalled entry), un-dismisses the popup (the user is
|
|
39
|
+
* typing again, so suggestions should come back), and re-homes the selection
|
|
40
|
+
* (the candidate list is about to be recomputed).
|
|
41
|
+
*/
|
|
42
|
+
const DRAFT_FLAGS = { historyIndex: null, dismissed: false, sel: 0 };
|
|
43
|
+
export function editorReducer(state, action) {
|
|
44
|
+
switch (action.type) {
|
|
45
|
+
case 'insert':
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
...DRAFT_FLAGS,
|
|
49
|
+
buffer: state.buffer.slice(0, state.cursor) + action.text + state.buffer.slice(state.cursor),
|
|
50
|
+
cursor: state.cursor + action.text.length,
|
|
51
|
+
};
|
|
52
|
+
case 'replace':
|
|
53
|
+
return { ...state, ...DRAFT_FLAGS, buffer: action.buffer, cursor: action.cursor };
|
|
54
|
+
case 'backspace': {
|
|
55
|
+
if (state.cursor <= 0)
|
|
56
|
+
return state;
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
...DRAFT_FLAGS,
|
|
60
|
+
buffer: state.buffer.slice(0, state.cursor - 1) + state.buffer.slice(state.cursor),
|
|
61
|
+
cursor: state.cursor - 1,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
case 'moveCursor':
|
|
65
|
+
// NO DRAFT-FLAG RESET. Moving the caret is not an edit: clearing
|
|
66
|
+
// `historyIndex` here would end a history walk the moment the user pressed
|
|
67
|
+
// ← to look at what they recalled.
|
|
68
|
+
return state.cursor === action.cursor ? state : { ...state, cursor: action.cursor };
|
|
69
|
+
case 'recall':
|
|
70
|
+
// `historyIndex` is SET, not reset, which is why recall cannot reuse
|
|
71
|
+
// `replace` — the two differ in exactly the field that keeps the walk
|
|
72
|
+
// alive. `dismissed` and `sel` are LEFT ALONE, which is not an oversight:
|
|
73
|
+
// the three `setState` calls this replaces did not touch them either, and
|
|
74
|
+
// an Esc-dismissed popup that reopened when the user stepped through their
|
|
75
|
+
// history would be a behaviour change smuggled in by a refactor whose whole
|
|
76
|
+
// claim is that input semantics are identical (K-7).
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
buffer: action.buffer,
|
|
80
|
+
cursor: action.cursor,
|
|
81
|
+
historyIndex: action.historyIndex,
|
|
82
|
+
};
|
|
83
|
+
case 'select':
|
|
84
|
+
return state.sel === action.sel ? state : { ...state, sel: action.sel };
|
|
85
|
+
case 'dismiss':
|
|
86
|
+
return state.dismissed ? state : { ...state, dismissed: true };
|
|
87
|
+
case 'clear':
|
|
88
|
+
return INITIAL_EDITOR_STATE;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame differ — F1 of tui-input-flicker-fix (§3.2).
|
|
3
|
+
*
|
|
4
|
+
* Ink 5.2.1 has exactly one repaint strategy: on every commit it serialises the
|
|
5
|
+
* WHOLE frame and hands it to `log-update`, which writes
|
|
6
|
+
* `eraseLines(previousLineCount) + frame` in a single `stdout.write`
|
|
7
|
+
* (`ink/build/log-update.js:17`). `ansiEscapes.eraseLines` walks BOTTOM-UP, so
|
|
8
|
+
* the composer — the last row of the frame — is erased near the start of every
|
|
9
|
+
* repaint and rewritten at the very end of it. During a run that happens 15–30
|
|
10
|
+
* times a second with the composer's bytes unchanged, and the user sees the
|
|
11
|
+
* bottom of their screen flicker while they type (§2 R1/R2).
|
|
12
|
+
*
|
|
13
|
+
* This module recognises that write shape, diffs the new frame against the
|
|
14
|
+
* previous one line by line, and emits only the rows that actually changed, each
|
|
15
|
+
* addressed absolutely. It is PURE: no I/O, no Node globals, no event emitter —
|
|
16
|
+
* `stdout-frame-writer.ts` is what binds it to a real stream. That is what keeps
|
|
17
|
+
* it unit-testable with plain string literals and no TTY.
|
|
18
|
+
*
|
|
19
|
+
* THREE INVARIANTS LIVE HERE AND EACH ONE FAILS SILENTLY IF DROPPED:
|
|
20
|
+
*
|
|
21
|
+
* - I-4 · An emitted line is `CUP · SGR0 · text · SGR0 · CSI K`, IN THAT ORDER,
|
|
22
|
+
* and it never contains `CSI 2K`. Erasing first would blank the row and refill
|
|
23
|
+
* it, which is R1's defect at 1/H the scale but on the one row the user is
|
|
24
|
+
* watching. Overwriting in place means no cell is ever presentably empty; the
|
|
25
|
+
* trailing `CSI K` removes only the residue a longer previous line left, and
|
|
26
|
+
* the trailing SGR reset before it stops a BCE terminal painting that residue
|
|
27
|
+
* in whatever background colour the line ended inside.
|
|
28
|
+
* - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
|
|
29
|
+
* `previousLineCount` is computed from the string IT produced and is
|
|
30
|
+
* unaffected by what we actually wrote, so a later pass-through's
|
|
31
|
+
* `eraseLines(H+1)` must find the cursor exactly where log-update left it last
|
|
32
|
+
* time. Drop this and one fallback shifts the frame up a row for the rest of
|
|
33
|
+
* the session.
|
|
34
|
+
* - I-6 · Any unrecognised write invalidates the cache. Absolute addressing is
|
|
35
|
+
* valid only while nothing else writes to stdout. Full-screen mode already
|
|
36
|
+
* forbids that, but "forbidden" is not "impossible", and the failure mode of
|
|
37
|
+
* the guard being absent is a permanently misaligned screen while the failure
|
|
38
|
+
* mode of it firing spuriously is one extra full repaint.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Rebuild `ansiEscapes.eraseLines(count)` byte for byte.
|
|
42
|
+
*
|
|
43
|
+
* Exported for the upgrade tripwire (`frame-differ-ink-shape.test.ts`), which
|
|
44
|
+
* drives the real `ink/build/log-update.js` and asserts this reconstruction is
|
|
45
|
+
* byte-equal to what it produced. If a future Ink changes the shape, that test
|
|
46
|
+
* fails loudly instead of this module silently degrading to pass-through for the
|
|
47
|
+
* rest of the package's life (K-2).
|
|
48
|
+
*/
|
|
49
|
+
export declare function eraseLinesPrefix(count: number): string;
|
|
50
|
+
export interface FrameDifferOptions {
|
|
51
|
+
/** Emit the DEC 2026 envelope around each batch. */
|
|
52
|
+
sync: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Live terminal height. Read on every `transform`, NEVER cached: it is the
|
|
55
|
+
* only input to the I-9 geometry guard and it changes under the differ's feet
|
|
56
|
+
* on resize. `undefined` ⇒ stand down and pass through.
|
|
57
|
+
*/
|
|
58
|
+
rows: () => number | undefined;
|
|
59
|
+
/** Raised once, on the 0→1 edge of `fallbacks` (§5.6). */
|
|
60
|
+
onFirstFallback?: () => void;
|
|
61
|
+
}
|
|
62
|
+
export interface FrameWriterStats {
|
|
63
|
+
/** Frames the differ EMITTED — `framesDiffed + framesFull`, never pass-throughs. */
|
|
64
|
+
framesTotal: number;
|
|
65
|
+
framesDiffed: number;
|
|
66
|
+
framesFull: number;
|
|
67
|
+
linesWritten: number;
|
|
68
|
+
bytesWritten: number;
|
|
69
|
+
/**
|
|
70
|
+
* Pass-throughs caused by a write the differ did not understand.
|
|
71
|
+
*
|
|
72
|
+
* NOT INVALIDATIONS (AC-11 / K-14). A resize legitimately invalidates and shows
|
|
73
|
+
* up as `framesFull`; counting invalidations here would put AC-7 and AC-11 in
|
|
74
|
+
* direct contradiction and one of them would end up quietly relaxed. It also
|
|
75
|
+
* excludes the two pass-throughs this module inflicts on ITSELF and already
|
|
76
|
+
* knows are benign — the session's seed write and the degenerate-geometry
|
|
77
|
+
* stand-down — because `fallbacks > 0` is documented to mean "something wrote
|
|
78
|
+
* to stdout behind Ink's back" and is what raises the §5.6 notice.
|
|
79
|
+
*/
|
|
80
|
+
fallbacks: number;
|
|
81
|
+
}
|
|
82
|
+
export interface FrameDiffer {
|
|
83
|
+
/**
|
|
84
|
+
* Bytes to write instead of `chunk`; `''` = write nothing; `null` = write
|
|
85
|
+
* `chunk`. The parameter is `string` and only `string` (P1-4): decoding, and
|
|
86
|
+
* the decision about what is even diffable, belong to the writer — the only
|
|
87
|
+
* place untyped input arrives.
|
|
88
|
+
*/
|
|
89
|
+
transform(chunk: string): string | null;
|
|
90
|
+
/** Force the next frame to be a full repaint. */
|
|
91
|
+
invalidate(): void;
|
|
92
|
+
/** Single owner of the counters (P2-2); the writer re-exports this. */
|
|
93
|
+
stats(): FrameWriterStats;
|
|
94
|
+
}
|
|
95
|
+
export declare function createFrameDiffer(options: FrameDifferOptions): FrameDiffer;
|