// The CLI's log level, and the one function the commands write stderr lines // through. // // Every line quak prints on stderr is a log line at one of six levels, from // the quietest: fatal, error, warning, info, debug, trace. A run prints the // lines at its own level and at every quieter one. Its level is info unless // `-v` or `-q` asks for another (see "CLI surface" in the README). Every // command that exits non-zero logs a fatal line saying why, so even `-q`, which // prints only fatal lines, never leaves a non-zero exit unexplained. An error // line reports something that failed, such as a file or an album, and a // warning something that did not fail but needs the user's attention. stdout // carries only a command's output and is not a log. // // The library itself writes no lines: its layers report through callbacks, and // the CLI logs what they report. `logRequests` gives the API client options // that log each request and each retry, and `logRefresh` the library's // `onProgress` that logs what a refresh and an ML data fetch report. import type { ApiClientOptions } from "./api/client.js"; import { describeError } from "./errors.js"; import type { RefreshEvent } from "./library/index.js"; import type { RetryOptions } from "./retry.js"; const LEVELS = ["fatal", "error", "warning", "info", "debug", "trace"] as const; export type LogLevel = (typeof LEVELS)[number]; // Writes `line` on stderr when the run's level prints lines at `level`. export type Log = (level: LogLevel, line: string) => void; // The level asked for by `-v` given `verbose` times, or by `-q`: info without // either, debug for `-v`, trace for `-vv` and more, fatal for `-q`. // `bin/quak.ts` refuses `-q` and `-v` together. export const logLevel = (verbose: number, quiet: boolean): LogLevel => { if (quiet) return "fatal"; if (verbose === 0) return "info"; return verbose === 1 ? "debug" : "trace"; }; // The log that writes each line at `level` or quieter to `stderr`, ending it // with a newline. export const stderrLog = (stderr: { write(text: string): unknown }, level: LogLevel): Log => (lineLevel, line) => { if (LEVELS.indexOf(lineLevel) <= LEVELS.indexOf(level)) { stderr.write(line + "\n"); } }; // API client options that log each request at trace, with the status of its // response or the error it failed with, and each retry at debug, with why and // how long it waits. `retry` is the retry policy, the default when not given. export const logRequests = ( log: Log, retry?: RetryOptions, ): ApiClientOptions => ({ retry: { ...retry, onRetry: (err, attempt, delayMs) => log( "debug", `Attempt ${attempt} failed, retrying in ${Math.round(delayMs)} ms: ${describeError(err)}`, ), }, onRequest: ({ method, url, status, error }) => log( "trace", `${method} ${url}: ${error === undefined ? `HTTP ${status}` : describeError(error)}`, ), }); // How a log line names each operation the library reports the start and end // of. const OPERATION_NAMES: Record = { refresh: "Refresh", fetchMLData: "ML data fetch", precacheThumbnails: "Thumbnail precache", precacheOriginals: "Original precache", }; // A library `onProgress` that logs, at info, each collection whose files a // refresh has fetched, and at debug the start and end of each refresh and ML // data fetch, with the error of one that failed. export const logRefresh = (log: Log) => (event: RefreshEvent): void => { if (event.status === "progress") { log( "info", `Fetched the files of ${event.fetched} of ${event.total} collections`, ); return; } const error = event.error === undefined ? "" : `: ${event.error}`; log( "debug", `${OPERATION_NAMES[event.operation]} ${event.status}${error}`, ); };