wsb 0.251.6311

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! Everything this tool says, and the one place it says it.
//!
//! # Why this exists
//!
//! A command that streams prose is usable by a person at a terminal and by
//! nothing else. The workbench shows the same operations as buttons, and what
//! it had to show was a wall of text with no way to know what was running,
//! what finished, or whether anything was happening at all.
//!
//! So every command takes `--jsonl`, and under it every byte the command emits
//! — its own writing, and the output of every process it starts — leaves as
//! JSONL: one [`Record`] per line, from a closed set of document types. Nothing
//! about what a command DOES changes; only how it says it.
//!
//! # One envelope, every tool
//!
//! Every wsb tool takes `--jsonl`, and all answer in this
//! envelope: `{"seq":N,"ts":"…Z","type":"…", …}` — one object per line, the
//! record's own fields flattened beside the envelope's. Logs and typed answers
//! share the one stream: a `machine` record and a `stderr` record are both
//! lines, and a consumer selects on `type` rather than on which pipe it read
//! from. That is what makes a reader of one tool a reader of all three.
//!
//! # The discipline that makes "every byte" true
//!
//! Text can only escape this process through:
//!
//! * [`outln!`] / [`out!`] / [`errln!`] / [`err!`] — this module's macros,
//!   which write to the terminal in human mode and package the text in
//!   JSONL mode; and
//! * [`Command`] — the one process type this crate starts, which gives a child
//!   the terminal in human mode and captures the streams the caller left to
//!   inherit when output is being packaged.
//!
//! `println!`, `eprintln!` and `std::process::Command` do not appear anywhere
//! else in this crate, and tests assert it (TEST9660, TEST9661). That is what
//! makes the promise a property of the source rather than a habit.

pub mod activity;
mod child;
pub mod record;
mod sink;

pub use child::Command;
pub use record::{Level, Line, Outcome, PlannedSuite, Record, Stream};
pub use sink::{
    attach_transcript, report_to_daemon, NO_TRANSCRIPT_ENV,
    emit, finish, is_jsonl, jsonl_mode, started,
    pass_live_display, record_err, take_jsonl_flag, write_err, write_out, SettledLines,
};

/// Report why the run is ending badly.
///
/// In human mode this writes the message the way the command always has. When
/// output is being packaged it emits a [`Record::Failure`] — the one record a
/// consumer has to look at to know what went wrong, rather than reading the
/// last few `stderr` lines and guessing which of them was the cause.
pub fn failed(message: &str, code: &str) {
    if is_jsonl() {
        emit(Record::Failure {
            message: message.to_string(),
            code: Some(code.to_string()),
        });
        return;
    }
    write_err(&format!("{message}\n"));
}

/// Say where this run is being written down.
///
/// The first line of a run, before anything it was asked to do. A packaged run
/// gets it as a `Message` with the `log-file` code, so a consumer can pick the
/// path out without matching on wording; a terminal gets one dim line, because
/// what somebody wants from it is to be able to find the file later — not to
/// read it now.
///
/// It goes to STDERR on a terminal: a command whose stdout is being piped into
/// something else must not have a path prepended to its answer.
pub fn log_file(path: &str) {
    if is_jsonl() {
        emit(Record::Message {
            level: Level::Info,
            text: format!("log: {path}"),
            code: Some("log-file".to_string()),
        });
        return;
    }
    write_err(&format!("\x1b[2mlog: {path}\x1b[0m\n"));
}

/// Say something as data as well as on screen.
///
/// In human mode this writes the text the way the command always has. When
/// output is being packaged it emits a [`Record::Message`] instead, so a
/// consumer gets the level and the code rather than having to recognise the
/// wording.
pub fn message(level: Level, code: &str, text: impl Into<String>) {
    let text = text.into();
    if is_jsonl() {
        emit(Record::Message {
            level,
            text,
            code: Some(code.to_string()),
        });
        return;
    }
    match level {
        Level::Error | Level::Warn => write_err(&format!("{text}\n")),
        Level::Info => write_out(&format!("{text}\n")),
    }
}