@aragon-agent/cli 0.5.0 → 0.5.2
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 +158 -0
- package/README.md +254 -7
- package/dist/agent/agent-mode.d.ts +53 -0
- package/dist/agent/agent-mode.js +69 -0
- package/dist/agent/controller.d.ts +69 -0
- package/dist/agent/controller.js +154 -1
- package/dist/agent/plan-prompt.d.ts +18 -0
- package/dist/agent/plan-prompt.js +93 -0
- package/dist/agent/reducer.d.ts +14 -1
- package/dist/agent/reducer.js +9 -0
- package/dist/agent/system-prompt.d.ts +15 -0
- package/dist/agent/system-prompt.js +8 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +247 -23
- package/dist/commands/builtins.js +60 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +71 -4
- package/dist/config/app-paths.js +139 -8
- package/dist/config/cli-commands.d.ts +43 -0
- package/dist/config/cli-commands.js +196 -0
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +53 -1
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +82 -2
- package/dist/config/migrate-home.d.ts +63 -0
- package/dist/config/migrate-home.js +165 -0
- package/dist/config/migrate-legacy-state.d.ts +10 -0
- package/dist/config/migrate-legacy-state.js +15 -6
- package/dist/config/schema.d.ts +81 -0
- package/dist/config/schema.js +85 -0
- package/dist/config/store.d.ts +30 -12
- package/dist/config/store.js +47 -35
- package/dist/input/mouse-events.d.ts +61 -0
- package/dist/input/mouse-events.js +153 -0
- package/dist/input/stdin-mouse-filter.d.ts +42 -0
- package/dist/input/stdin-mouse-filter.js +153 -0
- package/dist/logging/cli-commands.d.ts +25 -0
- package/dist/logging/cli-commands.js +221 -0
- package/dist/logging/file-sink.d.ts +114 -0
- package/dist/logging/file-sink.js +326 -0
- package/dist/logging/install.d.ts +163 -0
- package/dist/logging/install.js +302 -0
- package/dist/logging/levels.d.ts +40 -0
- package/dist/logging/levels.js +59 -0
- package/dist/logging/logger.d.ts +110 -0
- package/dist/logging/logger.js +237 -0
- package/dist/logging/redact.d.ts +28 -0
- package/dist/logging/redact.js +90 -0
- package/dist/logging/secret-registry.d.ts +30 -0
- package/dist/logging/secret-registry.js +55 -0
- package/dist/skills/paths.d.ts +2 -4
- package/dist/skills/paths.js +7 -9
- package/dist/tools/human-input.d.ts +179 -0
- package/dist/tools/human-input.js +227 -0
- package/dist/tools/index.d.ts +35 -3
- package/dist/tools/index.js +96 -9
- package/dist/tools/plan-tools.d.ts +64 -0
- package/dist/tools/plan-tools.js +198 -0
- package/dist/ui/App.d.ts +12 -1
- package/dist/ui/App.js +223 -17
- package/dist/ui/Composer.d.ts +9 -1
- package/dist/ui/Composer.js +26 -7
- package/dist/ui/ModeChip.d.ts +29 -0
- package/dist/ui/ModeChip.js +15 -0
- package/dist/ui/PromptInput.d.ts +22 -1
- package/dist/ui/PromptInput.js +49 -2
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +10 -2
- package/dist/ui/entries/ToolPreview.d.ts +2 -0
- package/dist/ui/entries/ToolPreview.js +64 -0
- package/dist/ui/glyphs.d.ts +21 -0
- package/dist/ui/glyphs.js +12 -0
- package/dist/ui/layout/AppShell.d.ts +10 -1
- package/dist/ui/layout/AppShell.js +38 -3
- package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
- package/dist/ui/layout/ScrollIndicator.js +15 -0
- package/dist/ui/layout/ScrollViewport.d.ts +8 -2
- package/dist/ui/layout/ScrollViewport.js +24 -4
- package/dist/ui/layout/regions.d.ts +55 -0
- package/dist/ui/layout/regions.js +52 -0
- package/dist/ui/layout/scroll-indicator.d.ts +27 -0
- package/dist/ui/layout/scroll-indicator.js +35 -0
- package/dist/ui/layout/scroll.d.ts +9 -0
- package/dist/ui/layout/scroll.js +23 -0
- package/dist/ui/layout/wrap-rows.d.ts +24 -0
- package/dist/ui/layout/wrap-rows.js +74 -0
- package/dist/ui/overlays/HelpOverlay.js +15 -0
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +50 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +117 -0
- package/dist/ui/overlays/QuestionOverlay.d.ts +30 -0
- package/dist/ui/overlays/QuestionOverlay.js +193 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/screen.d.ts +14 -1
- package/dist/ui/screen.js +37 -3
- package/dist/ui/use-startup-notices.d.ts +25 -0
- package/dist/ui/use-startup-notices.js +64 -0
- package/dist/ui/use-wheel-routing.d.ts +68 -0
- package/dist/ui/use-wheel-routing.js +139 -0
- package/package.json +3 -2
- package/runtime/insecure-tls-warning.cjs +62 -0
- package/runtime/insecure-tls-warning.d.cts +5 -0
package/dist/config/store.js
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config store — read/atomic-write the user config JSON
|
|
3
|
-
*
|
|
4
|
-
* `0600` permissions on POSIX (spec §3.5 / §6.1).
|
|
2
|
+
* Config store — read/atomic-write the user config JSON. Paths come from
|
|
3
|
+
* `app-paths.ts`; nothing here does its own path arithmetic. The file may hold
|
|
4
|
+
* secrets, so it is written with `0600` permissions on POSIX (spec §3.5 / §6.1).
|
|
5
|
+
*
|
|
6
|
+
* There is deliberately no `getConfigDir()` / `getSkillsDir()` here any more.
|
|
7
|
+
* The first would now return the entire user root under a name that says
|
|
8
|
+
* otherwise (see the test-isolation contract in `app-paths.ts`); the second was
|
|
9
|
+
* a second name for `skills/paths.ts::getUserSkillsDir()`, which is precisely
|
|
10
|
+
* the drift the header of that module warns about.
|
|
5
11
|
*/
|
|
6
12
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from 'node:fs';
|
|
7
|
-
import { dirname
|
|
13
|
+
import { dirname } from 'node:path';
|
|
8
14
|
import process from 'node:process';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
export
|
|
15
|
-
return paths.config;
|
|
16
|
-
}
|
|
17
|
-
export function getConfigPath() {
|
|
18
|
-
return join(paths.config, 'config.json');
|
|
19
|
-
}
|
|
20
|
-
export function getSessionsDir() {
|
|
21
|
-
return join(paths.data, 'sessions');
|
|
22
|
-
}
|
|
23
|
-
/** User-scope skills root — `aragon skills install`'s default target. */
|
|
24
|
-
export function getSkillsDir() {
|
|
25
|
-
return join(paths.data, 'skills');
|
|
26
|
-
}
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Read
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
15
|
+
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
|
+
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
+
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampSkillsConfig, } from './schema.js';
|
|
18
|
+
// Re-exported so the many existing importers of `store.js` keep working; the
|
|
19
|
+
// definitions live in `app-paths.ts` because they are pure path arithmetic.
|
|
20
|
+
export { getConfigPath, getSessionsDir };
|
|
30
21
|
/**
|
|
31
|
-
* Read the persisted config file.
|
|
32
|
-
*
|
|
22
|
+
* Read the persisted config file.
|
|
23
|
+
*
|
|
24
|
+
* A corrupt config file still must never crash the CLI — the fallback to
|
|
25
|
+
* defaults is unchanged. What changed is that it is no longer SILENT: a single
|
|
26
|
+
* stray comma used to reset the model, theme, timeouts, skills and (as far as
|
|
27
|
+
* the user could tell) the API key, with nothing anywhere saying why. Now the
|
|
28
|
+
* failure travels out of band in `parseError`, and `loadConfig()` turns it into
|
|
29
|
+
* a log record plus one visible line. Hand-editing this file is a supported
|
|
30
|
+
* workflow, so the failure mode had to become visible.
|
|
33
31
|
*/
|
|
34
32
|
export function readConfigFile() {
|
|
35
33
|
const file = getConfigPath();
|
|
36
34
|
if (!existsSync(file))
|
|
37
|
-
return null;
|
|
35
|
+
return { config: null };
|
|
38
36
|
try {
|
|
39
37
|
const raw = readFileSync(file, 'utf-8');
|
|
40
38
|
const parsed = JSON.parse(raw);
|
|
41
39
|
if (parsed && typeof parsed === 'object') {
|
|
42
|
-
return parsed;
|
|
40
|
+
return { config: parsed };
|
|
43
41
|
}
|
|
44
|
-
return null;
|
|
42
|
+
return { config: null, parseError: `${file} does not contain a JSON object` };
|
|
45
43
|
}
|
|
46
|
-
catch {
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
config: null,
|
|
47
|
+
parseError: `${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
48
|
+
};
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
@@ -82,7 +82,7 @@ export function writeConfigFile(config) {
|
|
|
82
82
|
* Unlike `readConfigFile`, this always returns a complete `PersistedConfig`.
|
|
83
83
|
*/
|
|
84
84
|
export function loadPersistedConfig() {
|
|
85
|
-
const partial = readConfigFile() ?? {};
|
|
85
|
+
const partial = readConfigFile().config ?? {};
|
|
86
86
|
return {
|
|
87
87
|
...DEFAULT_CONFIG,
|
|
88
88
|
...partial,
|
|
@@ -93,6 +93,8 @@ export function loadPersistedConfig() {
|
|
|
93
93
|
// `skills` is the second nested object in this file and needs the same
|
|
94
94
|
// treatment `apiKeys` gets — see the note on updatePersistedConfig below.
|
|
95
95
|
skills: clampSkillsConfig({ ...DEFAULT_CONFIG.skills, ...(partial.skills ?? {}) }),
|
|
96
|
+
// `log` is the third. Same one-level rule, same merge.
|
|
97
|
+
log: clampLogConfig({ ...DEFAULT_CONFIG.log, ...(partial.log ?? {}) }),
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
@@ -110,9 +112,15 @@ export function loadPersistedConfig() {
|
|
|
110
112
|
*
|
|
111
113
|
* `SkillsConfig` is therefore capped at ONE level of nesting (scalars and
|
|
112
114
|
* string arrays only). A third level requires replacing this hand-written merge
|
|
113
|
-
* with a real deep-merge utility first, or the same bug returns.
|
|
115
|
+
* with a real deep-merge utility first, or the same bug returns. `LogConfig` is
|
|
116
|
+
* a third SECTION, but it is still one level deep (scalars only), so the same
|
|
117
|
+
* hand-written merge covers it — and it must stay that way for that reason.
|
|
114
118
|
*/
|
|
115
119
|
export function updatePersistedConfig(patch) {
|
|
120
|
+
// Registration site 5. Whatever route a key took to get here, this is the one
|
|
121
|
+
// function that writes it to disk, so this is the last place it can be added
|
|
122
|
+
// to the redactor's backstop before it also starts appearing in log records.
|
|
123
|
+
registerSecretsFrom(patch.apiKeys);
|
|
116
124
|
const current = loadPersistedConfig();
|
|
117
125
|
const merged = {
|
|
118
126
|
...current,
|
|
@@ -120,6 +128,10 @@ export function updatePersistedConfig(patch) {
|
|
|
120
128
|
version: CONFIG_VERSION,
|
|
121
129
|
apiKeys: { ...current.apiKeys, ...(patch.apiKeys ?? {}) },
|
|
122
130
|
skills: clampSkillsConfig({ ...current.skills, ...(patch.skills ?? {}) }),
|
|
131
|
+
// Without this line, changing the log level from the settings screen would
|
|
132
|
+
// replace the whole section and quietly revert `maxFiles`, `previewChars`
|
|
133
|
+
// and — worst of all — `redactSecrets`.
|
|
134
|
+
log: clampLogConfig({ ...current.log, ...(patch.log ?? {}) }),
|
|
123
135
|
};
|
|
124
136
|
writeConfigFile(merged);
|
|
125
137
|
return merged;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mouse-report parser (mouse-wheel-region-routing §5.1) — pure, stream-free.
|
|
3
|
+
*
|
|
4
|
+
* The alternate screen has no native scrollback, so every modern terminal turns
|
|
5
|
+
* on DEC private mode 1007 (*alternate scroll*) and translates each wheel notch
|
|
6
|
+
* into a burst of arrow keys. Arrow keys carry NO POINTER POSITION, which is
|
|
7
|
+
* why region routing is impossible from that channel and why the CLI asks for
|
|
8
|
+
* real SGR mouse reporting instead (`\x1b[?1000h\x1b[?1006h`).
|
|
9
|
+
*
|
|
10
|
+
* Asking for it is only half the job. Ink 5.2.1 has no mouse branch at all:
|
|
11
|
+
* `parse-keypress.js` fails to match `\x1b[<64;40;12M`, `use-input.js` strips
|
|
12
|
+
* the leading ESC, and `PromptInput`'s `isControlSeq` guard passes the
|
|
13
|
+
* remainder (`[` is 0x5B, >= 0x20) straight into `insert()`. So an unfiltered
|
|
14
|
+
* mouse report is not ignored — it is TYPED INTO THE USER'S MESSAGE. This
|
|
15
|
+
* module exists so `stdin-mouse-filter.ts` can remove every byte of every
|
|
16
|
+
* report before Ink ever sees the chunk (invariant I-1).
|
|
17
|
+
*
|
|
18
|
+
* Non-wheel reports (press / release / drag) are recognized and DISCARDED
|
|
19
|
+
* rather than passed through, for the same reason.
|
|
20
|
+
*/
|
|
21
|
+
export interface WheelEvent {
|
|
22
|
+
readonly kind: 'wheel';
|
|
23
|
+
readonly dir: 'up' | 'down';
|
|
24
|
+
/** 1-based terminal column, as reported. */
|
|
25
|
+
readonly x: number;
|
|
26
|
+
/** 1-based terminal row, as reported. */
|
|
27
|
+
readonly y: number;
|
|
28
|
+
readonly shift: boolean;
|
|
29
|
+
readonly alt: boolean;
|
|
30
|
+
readonly ctrl: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface MouseSplit {
|
|
33
|
+
/** Wheel events, in arrival order. Non-wheel reports are dropped, not returned. */
|
|
34
|
+
readonly events: WheelEvent[];
|
|
35
|
+
/** Everything that was not a mouse report — forward this to Ink verbatim. */
|
|
36
|
+
readonly text: string;
|
|
37
|
+
/** Trailing strict prefix of a possible report; feed it back in with the next chunk. */
|
|
38
|
+
readonly pending: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Ceiling on the held-back tail. A malformed sequence that looks like a prefix
|
|
42
|
+
* forever must never be able to wedge input: past this length the tail is
|
|
43
|
+
* flushed as ordinary text and parsing resumes from the next byte.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_PENDING_MOUSE_CHARS = 32;
|
|
46
|
+
/**
|
|
47
|
+
* True when `s` is a PROPER prefix of a possible mouse report (never a full
|
|
48
|
+
* one). Nothing a keyboard produces matches beyond `\x1b` and `\x1b[`, which is
|
|
49
|
+
* why the filter's flush timer is a rare path rather than a latency tax on
|
|
50
|
+
* typing: ordinary text and pasted blocks contain no `\x1b[<`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isMousePrefix(s: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Split a decoded stdin chunk into wheel events, passthrough text, and a
|
|
55
|
+
* trailing prefix to carry into the next chunk.
|
|
56
|
+
*
|
|
57
|
+
* The caller is expected to feed `pending + nextChunk` back in; ConPTY does
|
|
58
|
+
* split writes mid-sequence, and a report torn across two chunks would
|
|
59
|
+
* otherwise leak its tail into the composer (R-6).
|
|
60
|
+
*/
|
|
61
|
+
export declare function splitMouseEvents(buffer: string): MouseSplit;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mouse-report parser (mouse-wheel-region-routing §5.1) — pure, stream-free.
|
|
3
|
+
*
|
|
4
|
+
* The alternate screen has no native scrollback, so every modern terminal turns
|
|
5
|
+
* on DEC private mode 1007 (*alternate scroll*) and translates each wheel notch
|
|
6
|
+
* into a burst of arrow keys. Arrow keys carry NO POINTER POSITION, which is
|
|
7
|
+
* why region routing is impossible from that channel and why the CLI asks for
|
|
8
|
+
* real SGR mouse reporting instead (`\x1b[?1000h\x1b[?1006h`).
|
|
9
|
+
*
|
|
10
|
+
* Asking for it is only half the job. Ink 5.2.1 has no mouse branch at all:
|
|
11
|
+
* `parse-keypress.js` fails to match `\x1b[<64;40;12M`, `use-input.js` strips
|
|
12
|
+
* the leading ESC, and `PromptInput`'s `isControlSeq` guard passes the
|
|
13
|
+
* remainder (`[` is 0x5B, >= 0x20) straight into `insert()`. So an unfiltered
|
|
14
|
+
* mouse report is not ignored — it is TYPED INTO THE USER'S MESSAGE. This
|
|
15
|
+
* module exists so `stdin-mouse-filter.ts` can remove every byte of every
|
|
16
|
+
* report before Ink ever sees the chunk (invariant I-1).
|
|
17
|
+
*
|
|
18
|
+
* Non-wheel reports (press / release / drag) are recognized and DISCARDED
|
|
19
|
+
* rather than passed through, for the same reason.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Ceiling on the held-back tail. A malformed sequence that looks like a prefix
|
|
23
|
+
* forever must never be able to wedge input: past this length the tail is
|
|
24
|
+
* flushed as ordinary text and parsing resumes from the next byte.
|
|
25
|
+
*/
|
|
26
|
+
export const MAX_PENDING_MOUSE_CHARS = 32;
|
|
27
|
+
const ESC = '\x1b';
|
|
28
|
+
/** `\x1b[<b;x;yM` (press) or `…m` (release) — the only form we request. */
|
|
29
|
+
const SGR_REPORT = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
|
30
|
+
/** A strict prefix of an SGR report: `\x1b[<` plus digits and semicolons. */
|
|
31
|
+
const SGR_PREFIX = /^\x1b\[<[\d;]*$/;
|
|
32
|
+
/**
|
|
33
|
+
* True when `s` is a PROPER prefix of a possible mouse report (never a full
|
|
34
|
+
* one). Nothing a keyboard produces matches beyond `\x1b` and `\x1b[`, which is
|
|
35
|
+
* why the filter's flush timer is a rare path rather than a latency tax on
|
|
36
|
+
* typing: ordinary text and pasted blocks contain no `\x1b[<`.
|
|
37
|
+
*/
|
|
38
|
+
export function isMousePrefix(s) {
|
|
39
|
+
return /^\x1B(\[(<[\d;]*|M[\s\S]{0,2})?)?$/.test(s);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Decode an SGR button byte into a wheel event, or `null` when the report is
|
|
43
|
+
* not a wheel notch we act on.
|
|
44
|
+
*
|
|
45
|
+
* THE ORDER OF THESE TESTS IS THE SPECIFICATION, not a presentation choice.
|
|
46
|
+
* Button 66 (wheel-left) has `b & 1 === 0` and would decode as `'up'`, so the
|
|
47
|
+
* horizontal test has to precede the direction decode.
|
|
48
|
+
*/
|
|
49
|
+
function decodeWheel(b, x, y, final) {
|
|
50
|
+
// 1. Not a wheel report at all (press / release / drag) — consume, emit nothing.
|
|
51
|
+
if ((b & 64) === 0)
|
|
52
|
+
return null;
|
|
53
|
+
// 2. Horizontal wheel (66 = left, 67 = right). Consumed and dropped (non-goal).
|
|
54
|
+
if ((b & 3) >= 2)
|
|
55
|
+
return null;
|
|
56
|
+
// 3. Wheel buttons are press-only; a terminal that ever sent a release for one
|
|
57
|
+
// must not produce a second step for the same physical notch.
|
|
58
|
+
if (final !== 'M')
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
kind: 'wheel',
|
|
62
|
+
dir: (b & 1) === 1 ? 'down' : 'up',
|
|
63
|
+
x,
|
|
64
|
+
y,
|
|
65
|
+
shift: (b & 4) !== 0,
|
|
66
|
+
alt: (b & 8) !== 0,
|
|
67
|
+
ctrl: (b & 16) !== 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* X10 legacy reports (`\x1b[M` + three chars, each `code - 32`).
|
|
72
|
+
*
|
|
73
|
+
* We never request this encoding — `?1006h` is over a decade old and every
|
|
74
|
+
* terminal that implements `?1000` implements it — but a terminal that ignored
|
|
75
|
+
* the SGR request would fall back to it, and a leaked `\x1b[M` + garbage is
|
|
76
|
+
* exactly the draft corruption I-1 exists to prevent. So it is consumed
|
|
77
|
+
* unconditionally; the coordinates are only trusted when all three bytes are
|
|
78
|
+
* plausible, because `setEncoding('utf8')` mangles raw bytes above 0x7F.
|
|
79
|
+
*/
|
|
80
|
+
function matchX10(s) {
|
|
81
|
+
if (s.length < 6)
|
|
82
|
+
return { kind: 'partial' };
|
|
83
|
+
const codes = [s.charCodeAt(3), s.charCodeAt(4), s.charCodeAt(5)];
|
|
84
|
+
if (codes.some((c) => c < 32 || c > 255))
|
|
85
|
+
return { kind: 'complete', length: 6, event: null };
|
|
86
|
+
const [b, x, y] = codes;
|
|
87
|
+
return { kind: 'complete', length: 6, event: decodeWheel(b - 32, x - 32, y - 32, 'M') };
|
|
88
|
+
}
|
|
89
|
+
/** Classify the escape sequence starting at `s[0] === ESC`. */
|
|
90
|
+
function matchReport(s) {
|
|
91
|
+
if (s.length === 1)
|
|
92
|
+
return { kind: 'partial' }; // lone ESC so far
|
|
93
|
+
if (s[1] !== '[')
|
|
94
|
+
return { kind: 'none' };
|
|
95
|
+
if (s.length === 2)
|
|
96
|
+
return { kind: 'partial' }; // `\x1b[`
|
|
97
|
+
if (s[2] === '<') {
|
|
98
|
+
const m = SGR_REPORT.exec(s);
|
|
99
|
+
if (m) {
|
|
100
|
+
const b = Number(m[1]);
|
|
101
|
+
const x = Number(m[2]);
|
|
102
|
+
const y = Number(m[3]);
|
|
103
|
+
return { kind: 'complete', length: m[0].length, event: decodeWheel(b, x, y, m[4]) };
|
|
104
|
+
}
|
|
105
|
+
return SGR_PREFIX.test(s) ? { kind: 'partial' } : { kind: 'none' };
|
|
106
|
+
}
|
|
107
|
+
if (s[2] === 'M')
|
|
108
|
+
return matchX10(s);
|
|
109
|
+
return { kind: 'none' };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Split a decoded stdin chunk into wheel events, passthrough text, and a
|
|
113
|
+
* trailing prefix to carry into the next chunk.
|
|
114
|
+
*
|
|
115
|
+
* The caller is expected to feed `pending + nextChunk` back in; ConPTY does
|
|
116
|
+
* split writes mid-sequence, and a report torn across two chunks would
|
|
117
|
+
* otherwise leak its tail into the composer (R-6).
|
|
118
|
+
*/
|
|
119
|
+
export function splitMouseEvents(buffer) {
|
|
120
|
+
const events = [];
|
|
121
|
+
let text = '';
|
|
122
|
+
let i = 0;
|
|
123
|
+
while (i < buffer.length) {
|
|
124
|
+
const esc = buffer.indexOf(ESC, i);
|
|
125
|
+
if (esc === -1) {
|
|
126
|
+
text += buffer.slice(i);
|
|
127
|
+
return { events, text, pending: '' };
|
|
128
|
+
}
|
|
129
|
+
text += buffer.slice(i, esc);
|
|
130
|
+
const rest = buffer.slice(esc);
|
|
131
|
+
const m = matchReport(rest);
|
|
132
|
+
if (m.kind === 'complete') {
|
|
133
|
+
if (m.event)
|
|
134
|
+
events.push(m.event);
|
|
135
|
+
i = esc + m.length;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (m.kind === 'partial') {
|
|
139
|
+
// A tail that never resolves must not hold input hostage: past the
|
|
140
|
+
// ceiling it is ordinary text, and parsing continues after the ESC.
|
|
141
|
+
if (rest.length > MAX_PENDING_MOUSE_CHARS) {
|
|
142
|
+
text += rest;
|
|
143
|
+
return { events, text, pending: '' };
|
|
144
|
+
}
|
|
145
|
+
return { events, text, pending: rest };
|
|
146
|
+
}
|
|
147
|
+
// Not a mouse report — an arrow key, a paste, anything. Emit the ESC and
|
|
148
|
+
// resume scanning after it so the rest of the sequence reaches Ink intact.
|
|
149
|
+
text += ESC;
|
|
150
|
+
i = esc + 1;
|
|
151
|
+
}
|
|
152
|
+
return { events, text, pending: '' };
|
|
153
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdin mouse filter (mouse-wheel-region-routing §4.2 / §5.2).
|
|
3
|
+
*
|
|
4
|
+
* WHY A WRAPPER AND NOT A SECOND LISTENER. Ink 5.2.1 owns stdin in a way that
|
|
5
|
+
* forbids a second reader: `components/App.js` calls `setEncoding('utf8')`,
|
|
6
|
+
* `ref()`, `setRawMode(true)`, subscribes to `'readable'` and drains with
|
|
7
|
+
* `read()`. Attaching our own `'data'` listener to the same `process.stdin`
|
|
8
|
+
* would switch the stream into flowing mode and RACE Ink's `read()` for chunks
|
|
9
|
+
* — some keystrokes would reach Ink, some would not, non-deterministically
|
|
10
|
+
* (D-3). So the filter REPLACES the stream Ink is given: it reads the real
|
|
11
|
+
* stdin, strips every mouse report, and writes the remainder into a
|
|
12
|
+
* `PassThrough` that carries the surface Ink requires.
|
|
13
|
+
*
|
|
14
|
+
* ORDERING IS AN INVARIANT, NOT A CONVENTION (I-8). The caller must build this
|
|
15
|
+
* filter BEFORE anything writes `\x1b[?1000h`. Reporting enabled while Ink
|
|
16
|
+
* still holds the raw stream does not degrade the feature — it types
|
|
17
|
+
* `[<0;12;5M` into the user's message on every click, which is strictly worse
|
|
18
|
+
* than the bug this feature fixes. `cli.tsx::runInteractive` is the only caller
|
|
19
|
+
* and passes `filter !== null` to `enterAltScreen`.
|
|
20
|
+
*/
|
|
21
|
+
import { type WheelEvent } from './mouse-events.js';
|
|
22
|
+
export interface MouseSource {
|
|
23
|
+
/** Returns an unsubscribe function. Multiple subscribers are allowed. */
|
|
24
|
+
subscribe(listener: (event: WheelEvent) => void): () => void;
|
|
25
|
+
}
|
|
26
|
+
export interface MouseFilter {
|
|
27
|
+
/** Hand this to Ink: `render(<App/>, { stdin: filter.stdin })`. */
|
|
28
|
+
readonly stdin: NodeJS.ReadStream;
|
|
29
|
+
readonly source: MouseSource;
|
|
30
|
+
/** Idempotent. Detaches from the real stdin and clears the flush timer (I-7). */
|
|
31
|
+
dispose(): void;
|
|
32
|
+
}
|
|
33
|
+
export declare function createMouseFilter(real: NodeJS.ReadStream): MouseFilter;
|
|
34
|
+
/**
|
|
35
|
+
* Build a filter, or `null` if construction throws.
|
|
36
|
+
*
|
|
37
|
+
* The `null` return is the whole point (P0-1): `enterAltScreen` derives whether
|
|
38
|
+
* to enable mouse reporting from `filter !== null`, so a construction failure
|
|
39
|
+
* degrades to "the wheel is inert" — a supported, tested rung of the fail-safe
|
|
40
|
+
* ladder — rather than leaving reporting on with no filter in front of Ink.
|
|
41
|
+
*/
|
|
42
|
+
export declare function tryCreateMouseFilter(real: NodeJS.ReadStream, onError?: (reason: string) => void): MouseFilter | null;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdin mouse filter (mouse-wheel-region-routing §4.2 / §5.2).
|
|
3
|
+
*
|
|
4
|
+
* WHY A WRAPPER AND NOT A SECOND LISTENER. Ink 5.2.1 owns stdin in a way that
|
|
5
|
+
* forbids a second reader: `components/App.js` calls `setEncoding('utf8')`,
|
|
6
|
+
* `ref()`, `setRawMode(true)`, subscribes to `'readable'` and drains with
|
|
7
|
+
* `read()`. Attaching our own `'data'` listener to the same `process.stdin`
|
|
8
|
+
* would switch the stream into flowing mode and RACE Ink's `read()` for chunks
|
|
9
|
+
* — some keystrokes would reach Ink, some would not, non-deterministically
|
|
10
|
+
* (D-3). So the filter REPLACES the stream Ink is given: it reads the real
|
|
11
|
+
* stdin, strips every mouse report, and writes the remainder into a
|
|
12
|
+
* `PassThrough` that carries the surface Ink requires.
|
|
13
|
+
*
|
|
14
|
+
* ORDERING IS AN INVARIANT, NOT A CONVENTION (I-8). The caller must build this
|
|
15
|
+
* filter BEFORE anything writes `\x1b[?1000h`. Reporting enabled while Ink
|
|
16
|
+
* still holds the raw stream does not degrade the feature — it types
|
|
17
|
+
* `[<0;12;5M` into the user's message on every click, which is strictly worse
|
|
18
|
+
* than the bug this feature fixes. `cli.tsx::runInteractive` is the only caller
|
|
19
|
+
* and passes `filter !== null` to `enterAltScreen`.
|
|
20
|
+
*/
|
|
21
|
+
import { PassThrough } from 'node:stream';
|
|
22
|
+
import { splitMouseEvents } from './mouse-events.js';
|
|
23
|
+
/**
|
|
24
|
+
* How long a possible-prefix tail is held before being flushed as text.
|
|
25
|
+
*
|
|
26
|
+
* This is what stops a lone `\x1b` — the Esc key, which aborts a run — from
|
|
27
|
+
* being held hostage waiting for a `[` that will never arrive. Worst-case Esc
|
|
28
|
+
* latency becomes 12 ms, and only for the keystroke that is literally a bare
|
|
29
|
+
* ESC; every other key resolves `isMousePrefix` to false immediately.
|
|
30
|
+
*/
|
|
31
|
+
const PENDING_FLUSH_MS = 12;
|
|
32
|
+
/** Assign a method onto the PassThrough without widening its public type. */
|
|
33
|
+
function define(target, name, value) {
|
|
34
|
+
Object.defineProperty(target, name, { value, writable: true, configurable: true });
|
|
35
|
+
}
|
|
36
|
+
export function createMouseFilter(real) {
|
|
37
|
+
const wrapper = new PassThrough();
|
|
38
|
+
const listeners = new Set();
|
|
39
|
+
let pending = '';
|
|
40
|
+
let flushTimer = null;
|
|
41
|
+
let disposed = false;
|
|
42
|
+
// --- The surface Ink reads off the stream it is handed. ------------------
|
|
43
|
+
// `isRawModeSupported()` reads `props.stdin.isTTY` (App.js:34), so this has
|
|
44
|
+
// to be a DELEGATING GETTER rather than a snapshot: a stdin that is not a TTY
|
|
45
|
+
// must still report so through the wrapper, or Ink throws instead of falling
|
|
46
|
+
// back.
|
|
47
|
+
Object.defineProperty(wrapper, 'isTTY', {
|
|
48
|
+
get: () => real.isTTY,
|
|
49
|
+
configurable: true,
|
|
50
|
+
});
|
|
51
|
+
define(wrapper, 'setRawMode', (mode) => {
|
|
52
|
+
real.setRawMode?.(mode);
|
|
53
|
+
return wrapper;
|
|
54
|
+
});
|
|
55
|
+
define(wrapper, 'ref', () => {
|
|
56
|
+
real.ref?.();
|
|
57
|
+
return wrapper;
|
|
58
|
+
});
|
|
59
|
+
define(wrapper, 'unref', () => {
|
|
60
|
+
real.unref?.();
|
|
61
|
+
return wrapper;
|
|
62
|
+
});
|
|
63
|
+
const emit = (event) => {
|
|
64
|
+
for (const listener of [...listeners])
|
|
65
|
+
listener(event);
|
|
66
|
+
};
|
|
67
|
+
const clearFlushTimer = () => {
|
|
68
|
+
if (!flushTimer)
|
|
69
|
+
return;
|
|
70
|
+
clearTimeout(flushTimer);
|
|
71
|
+
flushTimer = null;
|
|
72
|
+
};
|
|
73
|
+
const flushPending = () => {
|
|
74
|
+
clearFlushTimer();
|
|
75
|
+
if (pending.length === 0 || disposed)
|
|
76
|
+
return;
|
|
77
|
+
const text = pending;
|
|
78
|
+
pending = '';
|
|
79
|
+
wrapper.write(text);
|
|
80
|
+
};
|
|
81
|
+
const onData = (chunk) => {
|
|
82
|
+
if (disposed)
|
|
83
|
+
return;
|
|
84
|
+
clearFlushTimer();
|
|
85
|
+
const decoded = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
86
|
+
const split = splitMouseEvents(pending + decoded);
|
|
87
|
+
pending = split.pending;
|
|
88
|
+
if (split.text.length > 0)
|
|
89
|
+
wrapper.write(split.text);
|
|
90
|
+
for (const event of split.events)
|
|
91
|
+
emit(event);
|
|
92
|
+
if (pending.length > 0) {
|
|
93
|
+
// `.unref()` so a held tail can never keep the process alive on its own.
|
|
94
|
+
flushTimer = setTimeout(flushPending, PENDING_FLUSH_MS);
|
|
95
|
+
flushTimer.unref?.();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
// P2-7: a closed stdin must still terminate Ink's reader, or it waits forever
|
|
99
|
+
// on a stream that will never produce another byte.
|
|
100
|
+
const onEnd = () => {
|
|
101
|
+
flushPending();
|
|
102
|
+
if (!wrapper.writableEnded)
|
|
103
|
+
wrapper.end();
|
|
104
|
+
};
|
|
105
|
+
// `setEncoding('utf8')` on the REAL stream so Node's StringDecoder handles
|
|
106
|
+
// multi-byte boundaries for us — the parser works on characters, and a UTF-8
|
|
107
|
+
// sequence split across two chunks would otherwise become two mojibake
|
|
108
|
+
// characters before it ever reached `splitMouseEvents`.
|
|
109
|
+
real.setEncoding?.('utf8');
|
|
110
|
+
real.on('data', onData);
|
|
111
|
+
real.once('end', onEnd);
|
|
112
|
+
real.once('close', onEnd);
|
|
113
|
+
return {
|
|
114
|
+
stdin: wrapper,
|
|
115
|
+
source: {
|
|
116
|
+
subscribe(listener) {
|
|
117
|
+
listeners.add(listener);
|
|
118
|
+
return () => listeners.delete(listener);
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
dispose() {
|
|
122
|
+
if (disposed)
|
|
123
|
+
return;
|
|
124
|
+
disposed = true;
|
|
125
|
+
clearFlushTimer();
|
|
126
|
+
real.removeListener('data', onData);
|
|
127
|
+
real.removeListener('end', onEnd);
|
|
128
|
+
real.removeListener('close', onEnd);
|
|
129
|
+
listeners.clear();
|
|
130
|
+
// I-7: a live `'data'` listener on stdin keeps the event loop alive, and
|
|
131
|
+
// `aragon` would hang on exit instead of returning to the shell.
|
|
132
|
+
real.pause?.();
|
|
133
|
+
real.unref?.();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build a filter, or `null` if construction throws.
|
|
139
|
+
*
|
|
140
|
+
* The `null` return is the whole point (P0-1): `enterAltScreen` derives whether
|
|
141
|
+
* to enable mouse reporting from `filter !== null`, so a construction failure
|
|
142
|
+
* degrades to "the wheel is inert" — a supported, tested rung of the fail-safe
|
|
143
|
+
* ladder — rather than leaving reporting on with no filter in front of Ink.
|
|
144
|
+
*/
|
|
145
|
+
export function tryCreateMouseFilter(real, onError) {
|
|
146
|
+
try {
|
|
147
|
+
return createMouseFilter(real);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
onError?.(err instanceof Error ? err.message : String(err));
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon logs path | list | tail | clear | open` — the non-interactive surface.
|
|
3
|
+
*
|
|
4
|
+
* Exit codes follow the existing convention: 0 success · 1 run-time failure ·
|
|
5
|
+
* 2 usage error.
|
|
6
|
+
*
|
|
7
|
+
* THE WHOLE GROUP IS EXEMPT from the "every subcommand records one info line"
|
|
8
|
+
* rule, and that is a correctness requirement rather than tidiness. Recording a
|
|
9
|
+
* line opens today's log file, and Node opens files without
|
|
10
|
+
* `FILE_SHARE_DELETE`; on Windows — the platform this feature was asked for —
|
|
11
|
+
* `aragon logs clear --yes` would then be unable to delete the very file its own
|
|
12
|
+
* announcement had just created. These commands are the log's operations
|
|
13
|
+
* surface; giving them observable side effects on the log is backwards.
|
|
14
|
+
*/
|
|
15
|
+
import { type CliFlags } from '../config/load.js';
|
|
16
|
+
export interface LogsCliOptions {
|
|
17
|
+
/** `-n <N>` — how many trailing lines `tail` prints (default 100). */
|
|
18
|
+
lines?: string;
|
|
19
|
+
follow?: boolean;
|
|
20
|
+
level?: string;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
/** Required by `clear`; without it the command refuses. */
|
|
23
|
+
yes?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function runLogsCommand(subcommand: string, opts: LogsCliOptions, flags: CliFlags): Promise<number>;
|