@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
309 lines (308 loc) • 12.8 kB
TypeScript
export type TtyInput = NodeJS.ReadableStream & {
isTTY?: boolean;
setRawMode?: (mode: boolean) => unknown;
};
export type TtyOutput = NodeJS.WritableStream & {
isTTY?: boolean;
columns?: number;
rows?: number;
on?(event: 'resize', listener: () => void): unknown;
removeListener?(event: 'resize', listener: () => void): unknown;
};
export type TerminalFeatures = {
/** ANSI colours and text styles; NO_COLOR and FORCE_COLOR=0 turn them all off. */
color: boolean;
/** ◆ ◇ │ …, or the ASCII fallback for legacy Windows consoles and the Linux console. */
unicode: boolean;
/** OSC 8 hyperlinks. */
hyperlinks: boolean;
};
/**
* The rich UI's features, or null for the plain line output: `--json`, stdin
* or stderr not a terminal (pipes, files), TERM=dumb, or CI set.
*/
export declare function terminalFeatures(options: {
mode: 'text' | 'json';
stdin: TtyInput | undefined;
stderr: TtyOutput | undefined;
env: NodeJS.ProcessEnv;
platform?: NodeJS.Platform;
}): TerminalFeatures | null;
/** Legacy Windows consoles (no Windows Terminal, no terminal program) and the Linux console get ASCII. */
export declare function supportsUnicode(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
/** Terminals known to render OSC 8 links; FORCE_HYPERLINK overrides. Multiplexers are left out. */
export declare function supportsHyperlinks(env: NodeJS.ProcessEnv): boolean;
export type Symbols = {
active: string;
done: string;
rail: string;
end: string;
warning: string;
error: string;
check: string;
radioOn: string;
radioOff: string;
pointer: string;
/** Text cut short, an activity in progress (`Checking…`) and a masked ID (`0123…cdef`). */
ellipsis: string;
/** Between the items of a summary line (`84 reads · 72 ok`); see `Tui.join`. */
separator: string;
mask: string;
placeholder: string;
barFull: string;
barEmpty: string;
up: string;
down: string;
spinner: readonly string[];
};
export declare const UNICODE_SYMBOLS: Symbols;
export declare const ASCII_SYMBOLS: Symbols;
export type Style = (text: string) => string;
export type Styles = Record<'bold' | 'dim' | 'underline' | 'inverse' | 'green' | 'yellow' | 'red' | 'cyan' | 'brand', Style>;
/** SGR styles; with colour off, every style is the identity (no escape sequences at all). */
export declare function createStyles(color: boolean): Styles;
export declare function stripAnsi(text: string): string;
/** Terminal columns of one character: 0 for marks and controls, 2 for CJK and emoji, else 1. */
export declare function charWidth(char: string): number;
/** Terminal columns of a line, ignoring escape sequences. */
export declare function displayWidth(text: string): number;
/**
* At most `width` columns, ending in `…` when cut. Escape sequences are kept;
* styles and an open OSC 8 link are closed after a cut.
*/
export declare function truncate(text: string, width: number, ellipsis?: string): string;
/**
* Plain `text` (no escape sequences) in at most `width` columns, keeping its
* start and its end around the ellipsis, so names that share a long prefix
* (`Acme … Production`, `Acme … Staging`) still differ once cut.
*/
export declare function truncateMiddle(text: string, width: number, ellipsis?: string): string;
/** `text` padded with spaces to `width` columns. */
export declare function padEnd(text: string, width: number): string;
export type Key = {
name: 'char';
char: string;
} | {
name: 'enter' | 'backspace' | 'delete' | 'up' | 'down' | 'left' | 'right' | 'home' | 'end' | 'tab' | 'escape' | 'ctrl-c' | 'ctrl-d' | 'ctrl-u' | 'ctrl-w' | 'other';
};
/** Keys in a chunk of raw-mode input; a paste arrives as many `char` keys. */
export declare function parseKeys(input: string): Key[];
/**
* The lines at the bottom of the output that are redrawn in place: each
* update moves the cursor back to the region's first line, clears to the end
* of the screen and writes the new lines in a single write. Lines are cut to
* the terminal width, so each takes one row; after a resize, the previous
* lines are counted at the new width (terminals rewrap them).
*/
export declare class LiveRegion {
private readonly out;
private readonly ellipsis;
private lines;
constructor(out: TtyOutput, ellipsis?: string);
/** The terminal width; 80 when it reports none (0 or undefined, as some pseudo-terminals do). */
get columns(): number;
get rows(): number;
private erase;
/** Lines that fit: one column short of the width (no pending wrap) and one row short of the height. */
private fit;
draw(lines: readonly string[]): void;
/** Permanent lines above the region (never cut; long ones wrap), then the region again. */
commit(lines: readonly string[]): void;
clear(): void;
get height(): number;
}
/** Where the session listens for SIGINT, SIGTERM and exit (the process; a plain emitter in tests). */
export type SignalSource = {
on(event: string, listener: (...args: unknown[]) => void): unknown;
removeListener(event: string, listener: (...args: unknown[]) => void): unknown;
listenerCount(event: string): number;
emit(event: string, ...args: unknown[]): boolean;
};
export type TuiOptions = {
stdin: TtyInput;
stderr: TtyOutput;
features: TerminalFeatures;
signals?: SignalSource;
exit?: (code: number) => void;
/** Spinner frame interval. */
frameMs?: number;
};
/** A prompt: its frame, and what each key does (a value ends it). */
export type PromptView<T> = {
render(): string[];
key(key: Key): {
value: T;
} | undefined;
};
export type Spinner = {
update(label: string): void;
stop(): void;
};
export type SelectItem<T> = {
label: string;
detail?: string;
value: T;
};
export declare class Tui {
readonly symbols: Symbols;
readonly style: Styles;
readonly features: TerminalFeatures;
/** What Ctrl-C (or SIGTERM) prints before the process exits. */
cancelMessage: string;
private readonly stdin;
private readonly stderr;
private readonly region;
private readonly signals;
private readonly exitProcess;
private readonly frameMs;
private readonly decoder;
private frame;
private animated;
private timer;
private tick;
private keyHandler;
private reading;
private raw;
private suspended;
private closed;
private interrupted;
constructor(options: TuiOptions);
/** True once the user cancelled (Ctrl-C or SIGTERM); nothing is drawn after that. */
get isInterrupted(): boolean;
get columns(): number;
/** Permanent lines above the live frame. */
print(...lines: string[]): void;
/** `│` */
rail(): void;
/** `◇ title detail` */
step(title: string, detail?: string): void;
/** `◇ title › value` (an answered question). */
answered(title: string, value: string): void;
/** `│ text`, wrapped at word boundaries (`text` is plain). */
info(text: string): void;
/** `▲ text` */
warn(text: string): void;
/** `■ text`, then dim detail lines under it. */
error(text: string, ...details: string[]): void;
/** `│ text` (dim): a note inside the current step, wrapped at word boundaries. */
note(text: string): void;
/**
* A command the user may copy, indented and on a line of its own: never
* wrapped, cut or prefixed (a long one is soft-wrapped by the terminal and
* still copies as one line).
*/
command(text: string): void;
/** Dim lines under the end of the timeline (after `└`), indented and wrapped. */
footnote(text: string): void;
/** Plain text in lines that fit after an `indent`-column prefix (a longer word, such as a path, stays whole). */
private wrap;
/** The current spinner frame. */
get spin(): string;
/** `███░░░` for a ratio between 0 and 1. */
bar(ratio: number, width: number): string;
/** The items of a summary line: `a · b · c`, or `a - b - c` in ASCII. */
join(parts: readonly string[]): string;
/**
* Text written elsewhere with the Unicode symbols (an error message with a
* masked ID, a warning) in this session's: on an ASCII-only console, `…`
* becomes `...` and ` · ` becomes ` - `.
*/
symbolText(text: string): string;
/** An OSC 8 link where the terminal supports it, else the text. The URL must be printable ASCII. */
link(url: string, text?: string): string;
/** The live frame, replacing the previous one; animated frames are redrawn every frame (spinners). */
show(render: () => string[], options?: {
animated?: boolean;
}): void;
clearFrame(): void;
/** Redraw the live frame now (after its state changed). */
draw(): void;
/** `◒ label` until stopped. */
spinner(label: string): Spinner;
/** `work` with a spinner. */
busy<T>(label: string, work: () => Promise<T>): Promise<T>;
private syncTimer;
/** Run a prompt until a key ends it; its frame is cleared afterwards. Ctrl-C never ends it (it cancels the run). */
prompt<T>(view: PromptView<T>): Promise<T>;
/** The fake cursor of inputs (the real one stays hidden). */
private get cursor();
private head;
/** Columns an input's first line has for its title and value together (the frame's width less `◆ ` and ` › `). */
private get headRoom();
/** A plain title cut (start and end kept) to at most `width` columns, so the value after it stays on screen. */
private fitTitle;
private noticeLines;
/**
* A one-line text input: printable characters, Backspace, Ctrl-U (clear) and
* Ctrl-W (last word). Null on Ctrl-D with no text (the input ended). On a
* narrow terminal the (plain) title is cut before the value, down to a third
* of the line; a value wider than the rest shows its end.
*/
text(options: {
title: string;
notice?: string | null;
maxLength?: number;
}): Promise<string | null>;
/**
* A numeric code input: digits are shown as typed (spaces and dashes, typed
* or pasted, are skipped) and the input ends at the last digit. Enter with
* fewer digits keeps it open. Null on Ctrl-D (the input ended). On a narrow
* terminal the (plain) title is cut, never the digits.
*/
code(options: {
title: string;
length: number;
hint?: string;
notice?: string | null;
}): Promise<string | null>;
/**
* An arrow-key list (↑/↓ or j/k, Home/End, Enter). Null on Ctrl-D. Labels
* are plain text, padded to one column; on a narrow terminal each is cut in
* the middle so its detail (a masked ID) always stays on screen.
*/
select<T>(options: {
title: string;
hint?: string;
items: ReadonlyArray<SelectItem<T>>;
initial?: number;
}): Promise<T | null>;
/** Yes / No: ←/→ (or Tab, h/l) move, y or n answers at once, Enter answers the highlighted choice. Null on Ctrl-D. */
confirm(options: {
title: string;
initial: boolean;
detail?: string;
}): Promise<boolean | null>;
private readonly onData;
/** Ctrl-C in raw mode sends no signal: raise SIGINT for every listener, this session's included. */
interrupt(): void;
private setRaw;
private attachInput;
private detachInput;
/**
* Hand the terminal to another program (cf's interactive sign-in): the
* frame is cleared, raw mode is off, input is released and the cursor shown.
*/
suspend(): void;
resume(): void;
/** `work` with the terminal handed over (see `suspend`). */
handOver<T>(work: () => Promise<T>): Promise<T>;
private readonly onResize;
/** Raw mode off, the frame cleared, the cursor shown; nothing more is drawn. */
private restore;
private detachSignals;
/** Print the cancel message; exit with `code` unless another listener for `signal` will (it exits 130 after cleaning up). */
private cancel;
private readonly onSigint;
private readonly onSigterm;
private readonly onExit;
/** End the session: the terminal is restored and the listeners removed. Safe to call twice. */
close(): void;
}
/** The terminal UI has shown this error (■ and its hint); `runCli` does not print it again. */
export declare function markRendered(error: object): void;
export declare function wasRendered(error: unknown): boolean;
/**
* Words in lines of at most `width` columns; a longer word gets a line of its
* own. Leading spaces (an indented line) are kept on every line.
*/
export declare function wrapWords(text: string, width: number): string[];