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