devflow-kit 2.0.1 → 2.1.0
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 +22 -0
- package/dist/cli/agents-view/render.js +9 -42
- package/dist/cli/agents-view/terminal.js +29 -153
- package/dist/cli/commands/agents.js +11 -1
- package/dist/cli/commands/flags.js +501 -82
- package/dist/cli/commands/init-seed.js +60 -42
- package/dist/cli/commands/init.js +36 -79
- package/dist/cli/commands/proxy.js +42 -13
- package/dist/cli/commands/uninstall.js +2 -3
- package/dist/cli/flags-view/index.js +9 -0
- package/dist/cli/flags-view/render.js +271 -0
- package/dist/cli/flags-view/state.js +478 -0
- package/dist/cli/flags-view/terminal.js +63 -0
- package/dist/cli/tui/cells.js +47 -0
- package/dist/cli/tui/terminal.js +329 -0
- package/dist/cli.js +3 -2
- package/dist/core/ansi.js +84 -0
- package/dist/core/flags.js +938 -123
- package/dist/core/manifest.js +97 -13
- package/dist/core/teammate-mode-cleanup.js +2 -2
- package/dist/hud/colors.js +6 -70
- package/package.json +2 -2
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic TUI shell — shared by agents-view and flags-view.
|
|
3
|
+
*
|
|
4
|
+
* applies ADR-013: impure I/O shell in CLI layer; pure logic lives in state + render.
|
|
5
|
+
* avoids PF-014: cleanup wired via Promise resolve — never process.exit() inside
|
|
6
|
+
* a finally-guarded scope.
|
|
7
|
+
* avoids PF-017: one generic shell, thin adapters per TUI — not copy-adapted per consumer.
|
|
8
|
+
*
|
|
9
|
+
* Bounded: MAX_KEYPRESSES = 50_000 hard limit (reliability rule — every loop bounded).
|
|
10
|
+
*
|
|
11
|
+
* Frame output contract — alt mode (avoids stale-frame ghosting on terminal shrink):
|
|
12
|
+
* - Each frame line ends with ERASE_EOL (clears to end of line).
|
|
13
|
+
* - Lines are joined with '\n' EXCEPT the last, which has no trailing '\n'.
|
|
14
|
+
* - ERASE_BELOW (ESC[0J) is appended after the last line to erase content below
|
|
15
|
+
* the frame on every redraw.
|
|
16
|
+
*
|
|
17
|
+
* Frame output contract — inline mode (D-INLINE):
|
|
18
|
+
* - No ENTER_ALT/LEAVE_ALT; renders in place in the normal scroll buffer.
|
|
19
|
+
* - First frame: write lines directly, track prevLineCount.
|
|
20
|
+
* - Subsequent frames: cursor-up (prevLineCount-1) + \r, rewrite lines, ERASE_BELOW.
|
|
21
|
+
* - Exit: cursor-up to frame top, ERASE_BELOW, SHOW_CURSOR — erases widget completely.
|
|
22
|
+
* - Height is clamped to stdout.rows - INLINE_MARGIN to prevent terminal scroll.
|
|
23
|
+
*/
|
|
24
|
+
import * as readline from 'readline';
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Constants
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
/** Hard upper bound on keypress events — resolves with signalAction on exhaustion. */
|
|
29
|
+
export const MAX_KEYPRESSES = 50_000;
|
|
30
|
+
/**
|
|
31
|
+
* Lines reserved below the inline widget so the shell prompt is never clobbered.
|
|
32
|
+
* D-INLINE: height clamped to stdout.rows - INLINE_MARGIN in inline mode.
|
|
33
|
+
*/
|
|
34
|
+
export const INLINE_MARGIN = 2;
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Terminal escape sequences
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
const ESC = '\x1b';
|
|
39
|
+
const ENTER_ALT = `${ESC}[?1049h`;
|
|
40
|
+
const LEAVE_ALT = `${ESC}[?1049l`;
|
|
41
|
+
const HIDE_CURSOR = `${ESC}[?25l`;
|
|
42
|
+
const SHOW_CURSOR = `${ESC}[?25h`;
|
|
43
|
+
/** Move cursor to top-left without clearing (less flicker than full clear). */
|
|
44
|
+
const HOME = `${ESC}[H`;
|
|
45
|
+
/** Erase from cursor to end of line. */
|
|
46
|
+
const ERASE_EOL = `${ESC}[K`;
|
|
47
|
+
/** Erase from cursor to end of screen. */
|
|
48
|
+
const ERASE_BELOW = `${ESC}[0J`;
|
|
49
|
+
/**
|
|
50
|
+
* Move cursor up N lines (D-INLINE: used by inline-mode repaints).
|
|
51
|
+
* Returns an empty string for n ≤ 0 so callers need no guard.
|
|
52
|
+
*/
|
|
53
|
+
const cursorUp = (n) => (n > 0 ? `${ESC}[${n}A` : '');
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Keypress normalization
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
/**
|
|
58
|
+
* Normalize a readline keypress event to a canonical key string.
|
|
59
|
+
* Maps backspace/delete/home/end to named tokens (raw bytes otherwise).
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeKey(str, key) {
|
|
62
|
+
if (key?.ctrl && key.name === 'c')
|
|
63
|
+
return 'ctrl-c';
|
|
64
|
+
const name = key?.name ?? '';
|
|
65
|
+
switch (name) {
|
|
66
|
+
case 'up': return 'up';
|
|
67
|
+
case 'down': return 'down';
|
|
68
|
+
case 'left': return 'left';
|
|
69
|
+
case 'right': return 'right';
|
|
70
|
+
case 'tab': return 'tab';
|
|
71
|
+
case 'return': return 'enter';
|
|
72
|
+
case 'escape': return 'escape';
|
|
73
|
+
case 'space': return 'space';
|
|
74
|
+
case 'backspace': return 'backspace';
|
|
75
|
+
case 'delete': return 'delete';
|
|
76
|
+
case 'home': return 'home';
|
|
77
|
+
case 'end': return 'end';
|
|
78
|
+
default:
|
|
79
|
+
return str ?? name;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Internal helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
function getDims(stdout) {
|
|
86
|
+
return {
|
|
87
|
+
rows: stdout.rows ?? 24,
|
|
88
|
+
cols: stdout.columns ?? 80,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write a complete frame to stdout.
|
|
93
|
+
*
|
|
94
|
+
* Frame output contract:
|
|
95
|
+
* HOME + each_line + ERASE_EOL + '\n' (except no '\n' after last line) + ERASE_BELOW
|
|
96
|
+
*
|
|
97
|
+
* ERASE_BELOW clears stale content below the frame when the terminal shrinks.
|
|
98
|
+
* No trailing '\n' on the last line keeps the cursor on that line so ERASE_BELOW
|
|
99
|
+
* erases exactly from the last data row to the bottom.
|
|
100
|
+
*/
|
|
101
|
+
function renderToStdout(state, stdout, renderFrame) {
|
|
102
|
+
const dims = getDims(stdout);
|
|
103
|
+
// D-REL-M1: clamp to terminal height so HOME-anchored redraws never desync
|
|
104
|
+
// on small panes. max(1, …) ensures at least one line is always written.
|
|
105
|
+
const lines = renderFrame(state, dims).slice(0, Math.max(1, dims.rows));
|
|
106
|
+
let out = HOME;
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
out += lines[i] + ERASE_EOL;
|
|
109
|
+
if (i < lines.length - 1)
|
|
110
|
+
out += '\n';
|
|
111
|
+
}
|
|
112
|
+
out += ERASE_BELOW;
|
|
113
|
+
stdout.write(out);
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// runTui — generic driver
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
/**
|
|
119
|
+
* Launch a generic interactive TUI.
|
|
120
|
+
*
|
|
121
|
+
* In 'alt' mode (default): enters the alternate screen buffer, hides the cursor,
|
|
122
|
+
* enables raw mode, and redraws by moving to HOME on each keypress.
|
|
123
|
+
*
|
|
124
|
+
* In 'inline' mode (D-INLINE): renders in-place in the normal scroll buffer.
|
|
125
|
+
* Repaints use cursor-up instead of ENTER_ALT/HOME. Height is clamped to
|
|
126
|
+
* stdout.rows - INLINE_MARGIN. Widget is erased completely on exit.
|
|
127
|
+
*
|
|
128
|
+
* Resolves when `reduce` returns an intent !== `spec.continueIntent`, when a
|
|
129
|
+
* signal fires, or when MAX_KEYPRESSES is exhausted.
|
|
130
|
+
*
|
|
131
|
+
* @returns Promise resolving to `{ intent, state }` at exit.
|
|
132
|
+
*/
|
|
133
|
+
export async function runTui(spec) {
|
|
134
|
+
// D-SEAM: default to process streams; callers (tests) may inject fakes.
|
|
135
|
+
const stdin = (spec.io?.stdin ?? process.stdin);
|
|
136
|
+
const stdout = (spec.io?.stdout ?? process.stdout);
|
|
137
|
+
const isInline = spec.screen === 'inline';
|
|
138
|
+
// REL-H1 driver bail: reject BEFORE any terminal mutation when stdin is not a
|
|
139
|
+
// TTY and no spec.io.stdin was injected.
|
|
140
|
+
//
|
|
141
|
+
// Without this guard: alt-screen is entered, raw mode is skipped (no setRawMode
|
|
142
|
+
// on non-TTY stdin), stdin ends immediately (no keypresses), the promise never
|
|
143
|
+
// settles, the process exits, and cleanup() never runs — leaving the terminal
|
|
144
|
+
// in alt-screen with hidden cursor.
|
|
145
|
+
//
|
|
146
|
+
// spec.io?.stdin injected = test / pipe path that owns its own stream lifecycle.
|
|
147
|
+
// That path may deliberately pass a non-isTTY stream (e.g. PassThrough in tests)
|
|
148
|
+
// and is exempted from this guard.
|
|
149
|
+
if (!spec.io?.stdin && !stdin.isTTY) {
|
|
150
|
+
throw new Error('runTui: stdin is not a TTY — use process.stdin on a real TTY or inject spec.io.stdin');
|
|
151
|
+
}
|
|
152
|
+
// ── Enable readline keypress events ─────────────────────────────────────
|
|
153
|
+
readline.emitKeypressEvents(stdin);
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
let state = spec.initialState;
|
|
156
|
+
let cleaned = false;
|
|
157
|
+
let keypressCount = 0;
|
|
158
|
+
// D-INLINE: tracks how many lines the last inline frame occupied.
|
|
159
|
+
// Used for cursor-up repaint and widget-erase on exit. Zero = no frame written yet.
|
|
160
|
+
let prevLineCount = 0;
|
|
161
|
+
// ── Inline-mode helpers ──────────────────────────────────────────────
|
|
162
|
+
function getInlineDims() {
|
|
163
|
+
const d = getDims(stdout);
|
|
164
|
+
return { rows: Math.max(1, d.rows - INLINE_MARGIN), cols: d.cols };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Render one inline frame in-place.
|
|
168
|
+
* First call: writes lines directly, sets prevLineCount.
|
|
169
|
+
* Subsequent calls: cursor-up (prevLineCount-1) + \r, rewrites, ERASE_BELOW.
|
|
170
|
+
* D-INLINE: ERASE_BELOW handles shrinking frames without a high-watermark.
|
|
171
|
+
*/
|
|
172
|
+
function renderInline(s) {
|
|
173
|
+
const dims = getInlineDims();
|
|
174
|
+
const lines = spec.renderFrame(s, dims).slice(0, dims.rows);
|
|
175
|
+
const lineCount = lines.length;
|
|
176
|
+
let out = '';
|
|
177
|
+
if (prevLineCount > 0) {
|
|
178
|
+
// Move back to start of previous frame
|
|
179
|
+
out += cursorUp(prevLineCount - 1) + '\r';
|
|
180
|
+
}
|
|
181
|
+
for (let i = 0; i < lineCount; i++) {
|
|
182
|
+
out += lines[i] + ERASE_EOL;
|
|
183
|
+
if (i < lineCount - 1)
|
|
184
|
+
out += '\n';
|
|
185
|
+
}
|
|
186
|
+
// Erase stale lines below current frame (handles shrinking frames)
|
|
187
|
+
out += ERASE_BELOW;
|
|
188
|
+
stdout.write(out);
|
|
189
|
+
prevLineCount = lineCount;
|
|
190
|
+
}
|
|
191
|
+
/** Dispatch render to the appropriate mode. */
|
|
192
|
+
function doRender(s) {
|
|
193
|
+
if (isInline) {
|
|
194
|
+
renderInline(s);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
renderToStdout(s, stdout, spec.renderFrame);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ── Guarded startup — terminal setup, initial resize, and first render ─
|
|
201
|
+
//
|
|
202
|
+
// All operations that modify terminal state run inside this try block so
|
|
203
|
+
// that any throw (including setRawMode EIO on a detached TTY) routes
|
|
204
|
+
// through cleanup(). cleanup() is a function declaration and is therefore
|
|
205
|
+
// hoisted, so it is callable here even though its textual definition
|
|
206
|
+
// appears later. removeListener on a not-yet-registered listener is a
|
|
207
|
+
// no-op, making partial setup safe to tear down.
|
|
208
|
+
try {
|
|
209
|
+
// D-SEC-S3: enter screen and enable raw mode inside the guarded block
|
|
210
|
+
// so a setRawMode throw cannot leave the terminal stranded.
|
|
211
|
+
// D-INLINE: inline mode skips ENTER_ALT — renders in the scroll buffer.
|
|
212
|
+
stdout.write(isInline ? HIDE_CURSOR : ENTER_ALT + HIDE_CURSOR);
|
|
213
|
+
if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
214
|
+
stdin.setRawMode(true);
|
|
215
|
+
}
|
|
216
|
+
stdin.resume();
|
|
217
|
+
// Apply initial resize (sets viewportHeight from actual terminal dims).
|
|
218
|
+
const initialDims = isInline ? getInlineDims() : getDims(stdout);
|
|
219
|
+
if (spec.onResize) {
|
|
220
|
+
state = spec.onResize(state, initialDims);
|
|
221
|
+
}
|
|
222
|
+
doRender(state);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
cleanup();
|
|
226
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// ── Cleanup (idempotent) ────────────────────────────────────────────────
|
|
230
|
+
function cleanup() {
|
|
231
|
+
if (cleaned)
|
|
232
|
+
return;
|
|
233
|
+
cleaned = true;
|
|
234
|
+
stdin.removeListener('keypress', onKeypress);
|
|
235
|
+
process.removeListener('SIGINT', onSigint);
|
|
236
|
+
process.removeListener('SIGTERM', onSigterm);
|
|
237
|
+
stdout.removeListener('resize', onResize);
|
|
238
|
+
if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
239
|
+
try {
|
|
240
|
+
stdin.setRawMode(false);
|
|
241
|
+
}
|
|
242
|
+
catch { /* ignore */ }
|
|
243
|
+
}
|
|
244
|
+
// Pause stdin to release the ref'd TTY handle — mirrors the stdin.resume()
|
|
245
|
+
// at startup. Without this the resumed stdin keeps the event loop alive
|
|
246
|
+
// and the CLI hangs after the TUI resolves.
|
|
247
|
+
stdin.pause();
|
|
248
|
+
if (isInline) {
|
|
249
|
+
// D-INLINE: erase widget and restore cursor.
|
|
250
|
+
// Move to start of frame, erase to bottom, show cursor.
|
|
251
|
+
let out = prevLineCount > 1 ? cursorUp(prevLineCount - 1) + '\r' : '\r';
|
|
252
|
+
if (prevLineCount > 0)
|
|
253
|
+
out += ERASE_BELOW;
|
|
254
|
+
out += SHOW_CURSOR;
|
|
255
|
+
stdout.write(out);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
stdout.write(LEAVE_ALT + SHOW_CURSOR);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function settle(intent, finalState) {
|
|
262
|
+
cleanup();
|
|
263
|
+
resolve({ intent, state: finalState });
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Tear down and reject. Used when a handler throws.
|
|
267
|
+
*
|
|
268
|
+
* A throw inside an EventEmitter listener does NOT reject the enclosing
|
|
269
|
+
* promise — it escapes as an uncaughtException and kills the process with
|
|
270
|
+
* cleanup() never having run, leaving raw mode and alt-screen set. Routing
|
|
271
|
+
* every handler failure through here keeps the PF-014 invariant (cleanup
|
|
272
|
+
* always runs) while still surfacing the error rather than swallowing it.
|
|
273
|
+
*/
|
|
274
|
+
function fail(err) {
|
|
275
|
+
cleanup();
|
|
276
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
277
|
+
}
|
|
278
|
+
// ── Resize handler ─────────────────────────────────────────────────────
|
|
279
|
+
function onResize() {
|
|
280
|
+
try {
|
|
281
|
+
const d = isInline ? getInlineDims() : getDims(stdout);
|
|
282
|
+
if (spec.onResize) {
|
|
283
|
+
state = spec.onResize(state, d);
|
|
284
|
+
}
|
|
285
|
+
doRender(state);
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
fail(err);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ── Keypress handler ───────────────────────────────────────────────────
|
|
292
|
+
function onKeypress(str, key) {
|
|
293
|
+
try {
|
|
294
|
+
keypressCount++;
|
|
295
|
+
if (keypressCount > MAX_KEYPRESSES) {
|
|
296
|
+
// Hard safety bound — exit on exhaustion (avoids unbounded event loop).
|
|
297
|
+
settle(spec.signalAction, state);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const normalized = normalizeKey(str, key);
|
|
301
|
+
const { state: next, intent } = spec.reduce(state, normalized);
|
|
302
|
+
state = next;
|
|
303
|
+
if (intent !== spec.continueIntent) {
|
|
304
|
+
// D-TS: TS cannot narrow A to Exclude<A,C> from a !== check on a generic C.
|
|
305
|
+
// The invariant holds at runtime: any intent that is not continueIntent is Exclude<A,C>.
|
|
306
|
+
settle(intent, state);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
doRender(state);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
fail(err);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ── Signal handlers ────────────────────────────────────────────────────
|
|
316
|
+
function onSigint() {
|
|
317
|
+
settle(spec.signalAction, state);
|
|
318
|
+
}
|
|
319
|
+
function onSigterm() {
|
|
320
|
+
settle(spec.signalAction, state);
|
|
321
|
+
}
|
|
322
|
+
// Register all listeners
|
|
323
|
+
stdin.on('keypress', onKeypress);
|
|
324
|
+
process.on('SIGINT', onSigint);
|
|
325
|
+
process.on('SIGTERM', onSigterm);
|
|
326
|
+
stdout.on('resize', onResize);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=terminal.js.map
|
package/dist/cli.js
CHANGED
|
@@ -58,8 +58,9 @@ program.action(() => {
|
|
|
58
58
|
}
|
|
59
59
|
program.help();
|
|
60
60
|
});
|
|
61
|
-
// Parse arguments
|
|
62
|
-
|
|
61
|
+
// Parse arguments — parseAsync so async command handlers propagate rejection
|
|
62
|
+
// rather than surfacing as an unhandled rejection with a bare stack trace.
|
|
63
|
+
await program.parseAsync();
|
|
63
64
|
// Show help if no arguments
|
|
64
65
|
if (!process.argv.slice(2).length) {
|
|
65
66
|
program.outputHelp();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI color helpers — no dependencies, precompiled escape sequences.
|
|
3
|
+
*
|
|
4
|
+
* Neutral home for terminal-primitive helpers shared across CLI layers
|
|
5
|
+
* (src/cli/tui/, src/cli/flags-view/, src/cli/agents-view/) and the HUD
|
|
6
|
+
* feature module (src/hud/). Re-exported verbatim from src/hud/colors.ts
|
|
7
|
+
* so all existing HUD call sites remain untouched.
|
|
8
|
+
*
|
|
9
|
+
* applies ADR-013: src/core/ = agent-neutral logic; ANSI primitives have no
|
|
10
|
+
* feature coupling and belong here, not in a feature module.
|
|
11
|
+
* applies PF-017 corollary: one shared definition over per-consumer copies.
|
|
12
|
+
*/
|
|
13
|
+
const ESC = '\x1b[';
|
|
14
|
+
const RESET = `${ESC}0m`;
|
|
15
|
+
export function bold(s) {
|
|
16
|
+
return `${ESC}1m${s}${RESET}`;
|
|
17
|
+
}
|
|
18
|
+
export function dim(s) {
|
|
19
|
+
return `${ESC}2m${s}${RESET}`;
|
|
20
|
+
}
|
|
21
|
+
export function red(s) {
|
|
22
|
+
return `${ESC}31m${s}${RESET}`;
|
|
23
|
+
}
|
|
24
|
+
export function green(s) {
|
|
25
|
+
return `${ESC}32m${s}${RESET}`;
|
|
26
|
+
}
|
|
27
|
+
export function yellow(s) {
|
|
28
|
+
return `${ESC}33m${s}${RESET}`;
|
|
29
|
+
}
|
|
30
|
+
export function blue(s) {
|
|
31
|
+
return `${ESC}34m${s}${RESET}`;
|
|
32
|
+
}
|
|
33
|
+
export function magenta(s) {
|
|
34
|
+
return `${ESC}35m${s}${RESET}`;
|
|
35
|
+
}
|
|
36
|
+
export function cyan(s) {
|
|
37
|
+
return `${ESC}36m${s}${RESET}`;
|
|
38
|
+
}
|
|
39
|
+
export function gray(s) {
|
|
40
|
+
return `${ESC}90m${s}${RESET}`;
|
|
41
|
+
}
|
|
42
|
+
export function white(s) {
|
|
43
|
+
return `${ESC}37m${s}${RESET}`;
|
|
44
|
+
}
|
|
45
|
+
export function orange(s) {
|
|
46
|
+
return `${ESC}38;5;208m${s}${RESET}`;
|
|
47
|
+
}
|
|
48
|
+
export function brightRed(s) {
|
|
49
|
+
return `${ESC}91m${s}${RESET}`;
|
|
50
|
+
}
|
|
51
|
+
export function boldRed(s) {
|
|
52
|
+
return `${ESC}1;31m${s}${RESET}`;
|
|
53
|
+
}
|
|
54
|
+
export function bgGreen(s) {
|
|
55
|
+
return `${ESC}42m${s}${RESET}`;
|
|
56
|
+
}
|
|
57
|
+
export function bgYellow(s) {
|
|
58
|
+
return `${ESC}43m${s}${RESET}`;
|
|
59
|
+
}
|
|
60
|
+
export function bgRed(s) {
|
|
61
|
+
return `${ESC}41m${s}${RESET}`;
|
|
62
|
+
}
|
|
63
|
+
export function inverse(s) {
|
|
64
|
+
return `${ESC}7m${s}${RESET}`;
|
|
65
|
+
}
|
|
66
|
+
export function truncate(s, max) {
|
|
67
|
+
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
68
|
+
}
|
|
69
|
+
// S2 — Terminal-escape and control-character sanitization (HIGH, pre-existing defect).
|
|
70
|
+
//
|
|
71
|
+
// The prior pattern (/\x1b\[[0-9;]*m/g) matched only SGR sequences (colour).
|
|
72
|
+
// The broadened ANSI_PATTERN also covers:
|
|
73
|
+
// CSI sequences — \x1b[ ... with intermediate bytes, any final byte
|
|
74
|
+
// OSC sequences — \x1b] ... terminated by BEL (\x07) or ST (\x1b\\)
|
|
75
|
+
// Two-byte C1 — \x1b followed by any single character in the C1 range
|
|
76
|
+
// CTRL_PATTERN removes non-printable C0 control chars that are not TAB (\x09)
|
|
77
|
+
// or standard newlines (\x0a, \x0d). Together they prevent agent names
|
|
78
|
+
// embedded in model IDs from injecting escape sequences into --list output.
|
|
79
|
+
const ANSI_PATTERN = /\x1b(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
|
|
80
|
+
const CTRL_PATTERN = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
81
|
+
export function stripAnsi(s) {
|
|
82
|
+
return s.replace(ANSI_PATTERN, '').replace(CTRL_PATTERN, '');
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=ansi.js.map
|