/** * Pure, incremental text-to-{@link LogRecord} parsing for the Homebridge UI log stream. * * The pure pieces that live here, shared by the socket and REST transports, are: * * - {@link LogLineSplitter} - a cursor-based incremental splitter that turns an unbounded stream of text chunks (from a WebSocket `stdout` event or a streamed REST * download) into discrete raw lines, transparently handling lines split across chunk boundaries and the four newline conventions the stream mixes. * - {@link parseLogLine} - a per-line parser that extracts the timestamp/plugin brackets and the ANSI-color-derived severity level, producing a {@link LogRecord} whose * `message` is ANSI-stripped and whose `raw` preserves the original escapes. * * A third pure function, {@link parseLogTimestamp} (with its shared {@link normalizeClock} clock-rule helper), interprets a {@link LogRecord.timestamp} string as an * epoch instant on demand. It is deliberately separate from {@link parseLogLine}, which leaves the timestamp as text: the epoch is a cold, cheaply-derivable value that * only the time-range query path needs, so it is computed by this one shared function when required rather than eagerly materialized on every parsed line. * * This module is the single owner of the ANSI escape regex and the SGR-code-to-{@link LogLevel} map - they live beside the parser that uses them, not in `settings.ts` * (which holds only scalars). The design mirrors the library's per-consumer incremental-assembly pattern (`process.ts`, `mp4-assembler.ts`): rather than sharing a * splitter core with `process.ts`, this splitter is purpose-built, because the two have opposite control-character requirements - `process.ts` strips non-printable * control characters to `os.EOL`, whereas this splitter must PRESERVE control characters because the ANSI color sequence IS the severity data. * * @module */ import type { LogRecord } from "./types.ts"; import type { Nullable } from "../util.ts"; /** * Cursor-based incremental line splitter for the log text stream. * * Feed each text chunk through {@link LogLineSplitter.consume} and iterate the raw lines it yields. The splitter carries the bytes of an incomplete trailing line across * calls, so a line split across two chunks is reassembled transparently, and it recognizes all four newline conventions the PTY-driven stream mixes (`\r\n`, `\n\r`, * `\r`, `\n`) as single line breaks. Crucially, a chunk that ends with a lone `\r` or `\n` holds that terminator in the carry rather than yielding immediately: the next * chunk may begin with the matching second half of a `\n\r`/`\r\n` pair, and emitting eagerly would split one line break into two and inject a phantom blank line. * * The scan is a single integer cursor over the carried buffer; the buffer is rebuilt once per `consume` (the residual tail becomes the next carry), not once per line, * so per-line work is a single `slice` of the line's content with no quadratic buffer churn. Because the splitter holds at most one partial line plus the current chunk, * its memory is bounded by chunk size regardless of total stream length. * * The class is intentionally signal-free and event-free. Resource lifecycle and async consumption belong to the composing transport, exactly as `Mp4BoxParser` leaves * those concerns to `Mp4SegmentAssembler`. * * @example * * ```ts * const splitter = new LogLineSplitter(); * * for(const line of splitter.consume(chunk)) { * * handle(parseLogLine(line)); * } * ``` * * @category Log Client */ export declare class LogLineSplitter { #private; /** * Feed the splitter a text chunk and yield every complete raw line now available. * * Each yielded line is the content between line breaks with the terminator removed and ANSI escapes preserved. A line that is still incomplete (no terminator yet, or * only a held lone terminator that may pair with the next chunk) is carried internally and surfaces on a later call once its break is unambiguous. * * @param chunk - A contiguous slice of log text from the transport. * * @returns An iterable of every complete raw line contained in (or completed by) this chunk, in stream order. */ consume(chunk: string): Iterable; /** * Flush any buffered final line that never received a terminator. * * A log stream may end without a trailing newline, leaving the last line held in the carry. Call this once after the source has ended to surface that final line. It * yields at most one line and clears the carry; calling it again yields nothing. A held lone terminator with no content (a trailing bare `\r`/`\n`) flushes as an * empty final line, matching the convention that a terminator delimits a line that exists. * * @returns An iterable yielding the final unterminated line, or nothing if the carry is empty. */ flush(): Iterable; } /** * Parse a single raw log line into a {@link LogRecord}. * * The `[timestamp] [Plugin Name] ` prefix is extracted from the ANSI-stripped text, and the remainder becomes the human-readable `message`. The severity level is read * from the SGR color sequence that the server wraps around the MESSAGE - which means it appears AFTER the timestamp/plugin brackets, not at the start of the line: the * timestamp (white) and plugin (cyan) carry their own colors, so reading the line's first color would always misclassify them as severity. We therefore scan for the * severity color starting just past the plugin bracket. The original line is preserved verbatim in `raw`. A colored line that carries no severity color resolves to * `level: "info"` (Homebridge's info convention), while a fully color-stripped line resolves to `level: null` because severity is genuinely unknown; a line with no * bracketed prefix resolves to `timestamp: null` / `plugin: null`, a `message` equal to the full stripped line, and a level read from any severity color present * anywhere in the (prefix-less) line. * * @param raw - A single raw log line (escapes intact, terminator already removed by {@link LogLineSplitter}). * * @returns The parsed {@link LogRecord}. * * @category Log Client */ export declare function parseLogLine(raw: string): LogRecord; /** * Parse a Homebridge log timestamp into epoch milliseconds, or `null` when the text is not a recognized timestamp. * * The Homebridge UI renders the first bracketed field as the host's locale/clock string. This recognizes the en-US default in both its renderings - the 12-hour * `M/D/YYYY, h:mm:ss AM` and the 24-hour `M/D/YYYY, HH:mm:ss` - via a single regex, constructs the instant in LOCAL time (matching how the server formats it in the * host's own timezone), and returns its epoch milliseconds. The shared {@link normalizeClock} helper owns the meridiem-to-24-hour conversion and the clock-component * range check, so that rule lives in exactly one place. * * Deliberate limitations, in the same register as the rest of the client: only the en-US 12h/24h default is recognized, so a server running another locale yields * `null` (the time-window stage carries forward the most recent parsed instant, so a null-epoch line is kept iff its parent record is in-window, never dropped merely for * lacking a parseable epoch); and the interpretation is LOCAL time, so a client whose timezone differs from the server's skews the absolute values. A well-formed but * impossible calendar date (for example `2/30`) returns `null`; a wall-clock time that falls in the client's DST spring-forward gap is best-effort accepted rather than * rejected, because only the calendar fields are round-trip-validated. * * @param text - The raw timestamp text from a {@link LogRecord.timestamp} (the first bracketed field), for example `"6/29/2026, 7:00:00 AM"`. * * @returns The instant as epoch milliseconds, or `null` when the text is not a recognized en-US timestamp or names an impossible calendar date. * * @category Log Client */ export declare function parseLogTimestamp(text: string): Nullable; /** * Normalize a raw clock reading - the captured hour/minute/second plus an optional meridiem token - into a 24-hour `{ hour, minute, second }`, or `null` when any * component is out of range. This is the SINGLE source of truth for the meridiem-to-24-hour conversion and the clock-component range check, shared by * {@link parseLogTimestamp} and the CLI-layer time-expression parser so neither re-implements the rule. It is a named export for those consumers and its own test, but it * is intentionally not part of the package barrel. * * Range rules: minute and second are 0-59 in every rendering; when a meridiem is present the hour is a 12-hour value (1-12), and without one it is a 24-hour value * (0-23). An out-of-range component yields `null`. The conversion maps `12am` to 0 and `12pm` to 12, adds 12 to any other PM hour, and leaves any other AM hour and every * 24-hour reading unchanged. * * @param parts - The raw clock components. * @param parts.hour - The hour as written (1-12 with a meridiem, 0-23 without). * @param parts.meridiem - The matched `AM`/`PM` token (any case) when the source carried one, or `undefined` for a 24-hour rendering. * @param parts.minute - The minute (0-59). * @param parts.second - The second (0-59). * * @returns The normalized 24-hour `{ hour, minute, second }`, or `null` when any component is out of range. * * @category Log Client */ export declare function normalizeClock(parts: { hour: number; meridiem?: string; minute: number; second: number; }): Nullable<{ hour: number; minute: number; second: number; }>; /** * Determine whether a raw log line begins a new Homebridge log entry - that is, whether it opens with a parseable bracketed timestamp. * * Every genuine entry the server emits starts with a `[timestamp]` field; a `[plugin]` bracket may or may not follow, because Homebridge's own core lines (for example * `[7/3/2026, 4:31:46 PM] Homebridge v1.11.3 ... is running`) carry a timestamp but no plugin prefix. Only the FIRST bracket is inspected: requiring a second `[plugin]` * bracket would wrongly reject those core lines. A line that is not an entry start - the server's `Loading logs...` / `File: ...` seed preamble, a byte-truncated seed * fragment, or a continuation line such as a stack frame or a wrapped object dump - has no leading parseable timestamp and returns `false`. ANSI is stripped first * because the server wraps the timestamp in an SGR color, so the raw line begins with an escape sequence rather than the `[`. * * @param raw - A single raw log line (escapes intact, terminator already removed by {@link LogLineSplitter}). * * @returns `true` when the line opens with a parseable en-US timestamp bracket, `false` otherwise. * * @category Log Client */ export declare function isLogLineStart(raw: string): boolean; /** * A one-way admission latch that suppresses the unusable leading lines of a live socket log seed until the first genuine log entry. * * The homebridge-config-ui-x native/file log method seeds a live tail by streaming the tail of the log file from a BYTE offset rather than a line boundary, so the first * line of file content is a fragment - the tail end of whatever line the offset fell inside - preceded by the server's own `Loading logs...` / `File: ...` preamble. None * of that is a showable entry. This gate drops every line until the first that begins a real entry - a parseable leading `[timestamp]`, per {@link isLogLineStart} - then * latches OPEN and admits everything thereafter, so the continuation lines and plugin-less core lines that legitimately lack a full prefix flow through untouched. * * It is composed per stream, exactly as {@link LogLineSplitter} is: the consumer owns WHEN to apply it (only on the byte-seeded socket seed - the REST whole-file * download starts at byte 0 and needs no gate), while this owns the latch policy. Once open it never closes, so steady-state admission is a single boolean read and the * recognition predicate runs only across the short leading prefix, never the live stream. * * A bounded safety valve guards the pathological case of a stream whose timestamps are not the recognized en-US rendering (a non-default server locale): if no entry * start is seen within `maxSkip` lines, the gate concludes the format is unrecognized, latches open, and admits the rest rather than suppressing the stream forever. * `maxSkip` is sized above any plausible leading-noise run yet well below the ~500-line seed, so it never fires for a normal log and bounds worst-case loss when it does. * * @example * * ```ts * const gate = new SeedGate(SEED_GATE_MAX_SKIP); * * for(const line of splitter.consume(chunk)) { * * if(gate.admit(line)) { * * emit(line); * } * } * ``` * * @category Log Client */ export declare class SeedGate { #private; /** * @param maxSkip - The safety-valve bound: the most leading non-entry lines to drop before opening the latch unconditionally. The consumer injects it (production * passes `SEED_GATE_MAX_SKIP`) so a test can drive the bounded-open path with a small value. */ constructor(maxSkip: number); /** * Decide whether a raw line is admitted, advancing the latch. Once the latch has opened every line is admitted; before then, only the first line that begins a genuine * entry (which opens the latch) or the line at which the skip bound is reached (the unrecognized-format safety valve) is admitted, and every leading non-entry line is * dropped. * * @param line - A single raw log line from the seed stream. * * @returns `true` if the line should be emitted, `false` if it is leading seed noise to drop. */ admit(line: string): boolean; } //# sourceMappingURL=parser.d.ts.map