dax
Version:
Cross platform shell tools inspired by zx.
396 lines • 19 kB
TypeScript
import * as dntShim from "../../../../../../_dnt.shims.js";
import type { FsFileWrapper, Path } from "../../../path/0.3.2/mod.js";
import { type ConsoleSize, type RenderInterval, type StaticTextContainer, type TextItem } from "../../../console-static-text/0.3.4/mod.js";
import { Buffer } from "../../../../@std/io/0.225.3/buffer.js";
import type { CommandBuilder, KillSignal } from "./command.js";
import type { ByteRingBuffer } from "./byteRingBuffer.js";
import { LineRingBuffer } from "./lineRingBuffer.js";
import type { Closer, Reader, ReaderSync, Writer, WriterSync } from "../../../../@std/io/0.225.3/types.js";
import type { CommandPipeWriter } from "./commandHandler.js";
export type { Closer, Reader, ReaderSync, Writer, WriterSync };
/**
* Owns the static text scope and the list of active tail segments. A single
* shared instance handles all `InheritTailWriter`s in the process so parallel
* commands interleave into the same pinned region. Tests can construct
* their own with a stand-in `StaticTextContainer` to assert on the emitted
* ANSI bytes; pass `interval: null` to skip the periodic refresh and drive
* `container.refresh()` manually.
*/
export declare class TailRenderer implements Disposable {
#private;
readonly container: StaticTextContainer;
constructor(options?: {
container?: StaticTextContainer;
interval?: RenderInterval | null;
});
[Symbol.dispose](): void;
/** @internal */
register(seg: InheritTailState): void;
/** @internal */
unregister(seg: InheritTailState): void;
/** @internal */
logAbove(items: TextItem[]): void;
}
export type PipeReader = Reader | ReaderSync;
export type PipeWriter = Writer | WriterSync;
/** An awaitable that resolves to an object exposing a `readable` stream —
* e.g. a `RequestBuilder` that resolves to a response with `.readable`. */
export interface AwaitableReadable {
/** Resolves to an object that exposes a `ReadableStream<Uint8Array>` via
* its `readable` property. */
then<TResult1 = {
readable: dntShim.ReadableStream<Uint8Array>;
}, TResult2 = never>(onfulfilled?: ((value: {
readable: dntShim.ReadableStream<Uint8Array>;
}) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
}
/** Behaviour to use for stdin.
* @value "inherit" - Sends the stdin of the process to the shell (default).
* @value "null" - Does not pipe or redirect the pipe.
*/
export type ShellPipeReaderKind = "inherit" | "null" | Reader | dntShim.ReadableStream<Uint8Array> | Uint8Array | CommandBuilder | FsFileWrapper | Path | AwaitableReadable;
/**
* The behaviour to use for a shell pipe.
* @value "inherit" - Sends the output directly to the current process' corresponding pipe (default).
* @value "null" - Does not pipe or redirect the pipe.
* @value "piped" - Captures the pipe without outputting.
* @value "inheritPiped" - Captures the pipe with outputting.
*/
export type ShellPipeWriterKind = "inherit" | "null" | "piped" | "inheritPiped" | WriterSync | dntShim.WritableStream<Uint8Array> | FsFileWrapper | Path;
export declare class NullPipeReader implements Reader {
read(_p: Uint8Array): Promise<number | null>;
}
export declare class NullPipeWriter implements WriterSync {
writeSync(p: Uint8Array): number;
}
export declare class ShellPipeWriter {
#private;
constructor(kind: ShellPipeWriterKind, inner: PipeWriter);
get kind(): ShellPipeWriterKind;
get inner(): PipeWriter;
write(p: Uint8Array): number | Promise<number>;
writeAll(data: Uint8Array): void | Promise<void>;
writeText(text: string): void | Promise<void>;
writeLine(text: string): void | Promise<void>;
}
export declare class CapturingBufferWriter implements Writer {
#private;
constructor(innerWriter: Writer, buffer: Buffer);
getBuffer(): Buffer;
write(p: Uint8Array): Promise<number>;
}
export declare class CapturingBufferWriterSync implements WriterSync {
#private;
constructor(innerWriter: WriterSync, buffer: Buffer);
getBuffer(): Buffer;
writeSync(p: Uint8Array): number;
}
/** Async tap that mirrors writes into a {@link ByteRingBuffer} for use as
* "errorTail" — the ring keeps the trailing N bytes of whatever the
* command sent through this stream, so they can be surfaced in the thrown
* `Error` even when the underlying sink (a piped command, a `WritableStream`,
* a file on disk) doesn't expose what was written. Pairs with
* {@link ErrorTailCaptureWriterSync}. */
export declare class ErrorTailCaptureWriter implements Writer {
#private;
readonly ring: ByteRingBuffer;
constructor(innerWriter: Writer, ring: ByteRingBuffer);
write(p: Uint8Array): Promise<number>;
}
/** Sync variant of {@link ErrorTailCaptureWriter}. */
export declare class ErrorTailCaptureWriterSync implements WriterSync {
#private;
readonly ring: ByteRingBuffer;
constructor(innerWriter: WriterSync, ring: ByteRingBuffer);
writeSync(p: Uint8Array): number;
}
export declare class InheritStaticTextBypassWriter implements WriterSync {
#private;
constructor(innerWriter: WriterSync);
writeSync(p: Uint8Array): number;
flush(): void;
}
/**
* Default number of lines to show in `.tailDisplay()` mode. Docker Compose
* shows a comparable-sized window; small enough not to dominate the terminal,
* large enough to convey progress.
*/
export declare const DEFAULT_INHERIT_TAIL_LINES = 5;
/**
* Default number of lines to retain for error-time flushback. On error the
* live tail (5 lines) rarely shows what actually broke, so we keep a larger
* buffer and promote it to scrollback when the command fails.
*/
export declare const DEFAULT_INHERIT_TAIL_ERROR_LINES = 80;
/** Context passed to a `TailMaxLines` callback at draw time. */
export interface TailMaxLinesContext {
/** Current terminal size, or `undefined` if the host isn't a TTY. */
size: ConsoleSize | undefined;
}
/** Total number of rows the tail occupies — header included — so two
* commands tailing at `"50%"` each compose into a full screen instead of
* spilling. When a header is shown the visible output count is `maxLines - 1`,
* clamped to at least 1 row of output (so a headered tail is always at
* least 2 rows total). Accepts:
* - A literal `number`, taken as-is.
* - A string like `"50%"`, resolved against the terminal's row count at draw
* time so the tail re-fits if the user resizes mid-run.
* - A function called per draw — for cases neither form covers, e.g.
* `(ctx) => Math.min(10, (ctx.size?.rows ?? 24) - 5)` to leave headroom. */
export type TailMaxLines = number | `${number}%` | ((ctx: TailMaxLinesContext) => number);
/** Context passed to a `TailHeader` callback at draw time. */
export interface TailHeaderContext {
/** The raw command text being run. */
command: string;
/** Current terminal size, or `undefined` if the host isn't a TTY. */
size: ConsoleSize | undefined;
}
/** Header rendered above the live tail.
* - `undefined` (default): `Running <command>` while running, `Ran <command>` in scrollback (when `printCommand()` is set).
* - `false`: no header.
* - `string`: rendered verbatim — you supply any styling.
* - function: called per draw with `{ command, size }`; the result is rendered verbatim.
*
* Regardless of this setting, the error path still emits `> <command>` to
* scrollback so failed commands stay unambiguous in logs. */
export type TailHeader = string | false | ((ctx: TailHeaderContext) => string);
/**
* Construction-time options for `InheritTailWriter`. The user-facing
* `.tailDisplay()` API maps onto this plus a few post-construction setters
* for header/promote behavior.
*/
export interface InheritTailWriterOptions {
/** Number of visible tail lines. See {@link TailMaxLines}.
* @default 5
*/
maxLines?: TailMaxLines;
/** Treat the inner writer as TTY-attached. Defaults to `process.stderr.isTTY`.
* When false, writes pass through to the inner writer untouched (no pinned
* region) — used when the host process isn't a terminal. */
isTty?: boolean;
/** Renderer hosting the pinned scrolling region. Defaults to the global
* one tied to `staticText` + `renderInterval`. Tests pass a custom
* renderer to capture the emitted ANSI bytes in isolation. */
renderer?: TailRenderer;
/** Header text or per-draw callback (already pre-bound — accepts only
* `{ size }`). Set in the constructor so the segment is fully labeled
* before it gets registered with the renderer; otherwise the live area
* could paint a header-less segment for one tick. */
header?: string | ((ctx: {
size: ConsoleSize | undefined;
}) => string);
/** When true, render `header` as-is (no `Running` / `Ran` framing). */
headerVerbatim?: boolean;
/** Always-shown command label on the error scrollback path, even when
* `header` is hidden or customized. */
errorHeader?: string;
/** When true, promote the header to scrollback as `Ran <cmd>` (or the
* verbatim header) on success — typically wired to `.printCommand()`. */
promoteHeaderOnSuccess?: boolean;
}
/**
* Default size of the per-stream error-context ring buffer. 8 KiB is enough
* to capture the last few screens worth of output — large enough to convey
* what went wrong without ballooning memory for long-running commands.
*/
export declare const DEFAULT_ERROR_TAIL_BYTES: number;
/**
* Configuration for `.errorTail()`. When enabled, dax silently retains
* the trailing N bytes of stdout and/or stderr and appends them to the
* thrown `Error.message` if the command exits with a non-zero code.
* Targets streams the user can't see — piped to another command,
* redirected to a file, sent to a `WritableStream`, or discarded with
* `"null"`. Streams routed to the terminal (`"inherit"` /
* `"inheritPiped"`) are skipped since the bytes already reached the
* user's scrollback.
*/
export interface ErrorTailOptions {
/** Per-stream cap on retained bytes. Once exceeded, the oldest bytes
* are dropped first.
* @default 8192 (8 KiB) */
maxBytes?: number;
/** Capture the trailing bytes of stdout.
* @default true */
stdout?: boolean;
/** Capture the trailing bytes of stderr.
* @default true */
stderr?: boolean;
/** Capture stdout and stderr into a single interleaved buffer instead
* of separate per-stream buffers. When true, the error message shows
* output in the order it was written rather than grouped by stream.
* @default false */
combined?: boolean;
}
/**
* Configuration for `.tailDisplay()`. Pass `true` to enable with defaults,
* or an options object to customize.
*/
export interface TailDisplayOptions {
/** Number of visible tail lines. See {@link TailMaxLines}.
* @default 5
*/
maxLines?: TailMaxLines;
/** Header rendered above the live tail. See {@link TailHeader}. */
header?: TailHeader;
}
/** Internal: a maxLines value normalized into a uniform per-draw callback,
* so the renderer doesn't dispatch on type each tick. */
type ResolvedMaxLinesFn = (ctx: TailMaxLinesContext) => number;
/** Internal: a header value normalized into a uniform per-draw callback.
* The user-facing `(ctx: TailHeaderContext) => string` is pre-bound with
* the command text by command.ts before reaching state, so the renderer
* (which only knows about `size`) can call it uniformly. */
type ResolvedHeaderFn = (ctx: {
size: ConsoleSize | undefined;
}) => string;
declare class InheritTailState {
#private;
readonly resolveMaxLines: ResolvedMaxLinesFn;
readonly lines: LineRingBuffer;
readonly enabled: boolean;
/** Per-draw callback that produces the header text. `undefined` means
* "no header" (live tail renders no label). Percentages, raw strings and
* user callbacks are all normalized into this single shape upstream. */
headerFn: ResolvedHeaderFn | undefined;
/** When true, the header is rendered verbatim instead of being framed
* with the built-in `Running <text>` / `Ran <text>` styling. Set when
* the user supplies a custom header string or function. */
headerVerbatim: boolean;
/** Always-present fallback shown on the error path, even if the live
* header is hidden or customized — so error scrollback is unambiguous
* about which command failed. */
errorHeader: string | undefined;
promoteHeaderOnSuccess: boolean;
readonly renderer: TailRenderer;
constructor(renderer: TailRenderer, maxLines: TailMaxLines, isTty: boolean, maxErrorLines?: number);
get omittedLineCount(): number;
/** Visible output rows after subtracting the header (if any) from the
* total `maxLines` budget. Floors to 1 so a headered segment always shows
* at least one line. */
visibleLineCount(ctx: TailMaxLinesContext): number;
addRef(): void;
setHeader(header: string | ResolvedHeaderFn | undefined, options?: {
verbatim?: boolean;
}): void;
appendLines(newLines: string[]): void;
release(errored: boolean, trailing: string[]): void;
}
/**
* Docker-style partial scrolling writer.
*
* Instead of streaming command output straight to the terminal, this buffers
* output line-by-line and shows only the most recent `maxLines` lines inside
* a static text scope pinned to the bottom of the terminal. When the command
* finishes successfully, the scope is cleared (the header is promoted to
* scrollback); on error the retained tail is flushed above so the user can
* see what happened.
*
* Falls back to writing directly to `innerWriter` when the host process is
* not attached to a TTY, since there's nowhere to anchor a scrolling region.
*
* Pass an existing writer as the second argument to share its scrolling
* region — used internally when both stdout and stderr are tailed, so the
* two streams interleave into one scroll area with a single header.
*/
export declare class InheritTailWriter implements WriterSync, Disposable {
#private;
constructor(innerWriter: WriterSync, options?: InheritTailWriterOptions);
constructor(innerWriter: WriterSync, sibling: InheritTailWriter);
/** Snapshot of the live tail (last visible-window retained lines, oldest
* first). The visible-window size is `maxLines - 1` when a header is shown
* (recomputed from the current console size for percentage / callback
* `maxLines`). */
get tailLines(): readonly string[];
/** Number of completed lines that were dropped from the retained ring
* buffer because its capacity is bounded. Rendered as
* `...N lines omitted...` above the retained tail when the command fails. */
get omittedLineCount(): number;
/**
* Sets a label rendered above the tail lines that identifies what this
* scrolling region is showing. Accepts a literal string (collapsed to
* a single line) or a per-draw callback that receives the current
* `{ size }`. `undefined` removes the header. Long text is truncated to
* the terminal width.
*
* When `options.verbatim` is true, the text is rendered as-is (no
* built-in `Running` / `Ran` framing). Used by `.tailDisplay({ header })`
* so the caller has full control over styling.
*/
setHeader(header: string | ((ctx: {
size: ConsoleSize | undefined;
}) => string) | undefined, options?: {
verbatim?: boolean;
}): void;
/**
* Sets the command label preserved on the error scrollback path even
* when the live header is hidden or customized — so `> <command>` is
* always shown when a tailed command fails, regardless of `.tailDisplay`
* header config.
*/
setErrorHeader(text: string | undefined): void;
/**
* Controls whether the `Ran <command>` header is promoted to scrollback
* on successful finalize. Defaults to `false` — on success the live tail
* clears silently unless the caller opts in (command.ts enables it when
* `.printCommand()` was set, so the command stays visible in scrollback).
* The error-path header (`> <command>` + retained tail) is always emitted
* regardless of this flag since it's diagnostic.
*/
setPromoteHeaderOnSuccess(value: boolean): void;
writeSync(p: Uint8Array): number;
/**
* Clears the scrolling region. Called on successful command completion.
*
* If a header was set it's promoted to scrollback via `logAbove` before
* the scope is disposed, so "which commands ran" remains visible after
* the transient tail clears. When multiple writers share a scope the
* scope is only disposed once all of them have finalized.
*
* Any partial pending line is still passed to `release` so that — if a
* sibling writer subsequently errors — the success side's last partial
* line is preserved in the error scrollback. The success path itself
* never renders trailing lines, so there's no visual cost when no
* sibling errors.
*/
finalize(): void;
[Symbol.dispose](): void;
/**
* Promotes the header + retained tail above the static region before
* clearing it. Called when the command errored so the user has visible
* context. When multiple writers share a scope, any writer finalizing
* for error causes the shared region to use the error path once all
* writers have finalized.
*/
finalizeForError(): void;
}
/**
* Header rendered above the live tail while the command is in flight:
* `Running <command>` in bold cyan.
*/
export declare function formatTailHeader(text: string, size: ConsoleSize | undefined): string;
/**
* Past-tense header promoted to scrollback when a tailed command completes
* successfully: `Ran <command>` in bold green.
*/
export declare function formatRanHeader(text: string, size: ConsoleSize | undefined): string;
export interface PipedBufferListener extends WriterSync, Closer {
setError(err: Error): void;
}
export declare class PipedBuffer implements WriterSync {
#private;
constructor();
getBuffer(): Buffer | undefined;
setError(err: Error): void;
close(): void;
writeSync(p: Uint8Array): number;
setListener(listener: PipedBufferListener): void;
}
export declare class PipeSequencePipe implements Reader, WriterSync {
#private;
close(): void;
writeSync(p: Uint8Array): number;
read(p: Uint8Array): Promise<number | null>;
}
export declare function pipeReaderToWritable(reader: Reader, writable: dntShim.WritableStream<Uint8Array>, signal: AbortSignal): Promise<void>;
export declare function pipeReadableToWriterSync(readable: dntShim.ReadableStream<Uint8Array>, writer: ShellPipeWriter | CommandPipeWriter, signal: AbortSignal | KillSignal): Promise<void>;
//# sourceMappingURL=pipes.d.ts.map