sprawling 0.0.12

A local multi-agent harness for persistent agent teams: long-running project work, agent communication, and experiments in coordination and social simulation.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (c) 2026 2youg1 and the sprawling contributors

//! What `status` says about which release this is, and the one command
//! in this binary that asks the registry whether a newer one exists.
//!
//! Its own file rather than another pair of functions under `status`:
//! everything else `status` prints is read out of this binary, and this
//! is the part that leaves the machine. The reading itself belongs to
//! `bin::release`; this renders it for a terminal, as the machine page
//! renders the same value for a browser.

use kernel::Maturity;
use sprawling::release;
use sprawling::release::Built;
use std::process::ExitCode;
use wire::{Registry, RegistryNewest, RegistryReading, ReleaseAnswer};

/// The first line `status` prints, as in
/// `sprawling 0.0.9 (alpha), built from source`.
///
/// The production path passes `kernel::release::MATURITY`, the one place
/// the maturity is written (kernel D18); taking it as a parameter is what
/// lets a test see this line follow it (`crates/sprawling/spec/Doctor.lean` ยง8-162).
pub(super) fn headline(maturity: Maturity) -> String {
    format!(
        "sprawling {} ({}){}",
        env!("CARGO_PKG_VERSION"),
        maturity.word(),
        cut()
    )
}

/// The day this release was cut, appended to the version line.
///
/// An early version number says almost nothing about how old a tree is,
/// and how old it is, is what its reader most needs to know
/// (CHANGELOG.md, opening note) - so the date sits beside the number in
/// the line a person reads first, and reading it costs no network.
fn cut() -> String {
    match release::built() {
        Ok(Built::Released(mine)) => format!(", released {}", mine.released()),
        Ok(Built::FromSource) => String::from(", built from source"),
        // A build that carries a tag it cannot decode is mislabelled.
        // Saying so here is cheaper than letting every later line
        // describe a release this binary is not.
        Err(err) => format!(", carrying a tag that does not decode: {}", err.recovery()),
    }
}

/// The one command in this binary that reaches the network, and only
/// when `--check` asked it to.
///
/// The exit code reports whether the question was answered, never what
/// the answer was: a release being out of date is news, not a failure,
/// while a registry that could not be read leaves a person believing
/// they checked when they did not.
pub(super) fn check() -> ExitCode {
    println!();
    match release::answer() {
        ReleaseAnswer::Refused { refusal } => {
            eprintln!("could not check: {refusal}");
            eprintln!("recovery: {}", refusal.recovery());
            ExitCode::FAILURE
        }
        ReleaseAnswer::Unconfirmed {
            mine,
            registries,
            update,
        } => {
            println!(
                "{}: confirm the original installer before updating.",
                mine.version
            );
            for line in &registries {
                println!("{}", registry_said(line));
            }
            print_update(&update);
            ExitCode::SUCCESS
        }
        ReleaseAnswer::Unreleased { registries, update } => {
            println!("this binary was built from source, so it is none of the published releases.");
            for line in &registries {
                println!("{}", registry_said(line));
            }
            print_update(&update);
            ExitCode::SUCCESS
        }
        ReleaseAnswer::Stands {
            mine,
            newest,
            registries: _,
            verdict,
            update,
        } => {
            match verdict {
                kernel::ReleaseVerdict::Current => println!(
                    "{} is the newest release published. Nothing to do.",
                    mine.version
                ),
                kernel::ReleaseVerdict::Behind => {
                    println!(
                        "a newer release is published: {}{} (this binary is {}).",
                        newest.version,
                        date_suffix(&newest),
                        mine.version
                    );
                    println!();
                    print_update(&update);
                    println!();
                    println!(
                        "Nothing was downloaded and nothing was changed. \
                         Updating is yours to run."
                    );
                }
                kernel::ReleaseVerdict::Ahead => println!(
                    "this binary ({}) is newer than the newest available through its install channel ({}).",
                    mine.version, newest.version
                ),
            }
            ExitCode::SUCCESS
        }
    }
}

fn print_update(update: &wire::UpdateHint) {
    if let Some(command) = &update.command {
        println!("  update  {command}");
    }
    for command in &update.alternatives {
        println!("  choose  {command}");
    }
}

fn date_suffix(line: &wire::ReleaseLine) -> String {
    if line.released.is_empty() {
        String::new()
    } else {
        format!(", cut {}", line.released)
    }
}

/// One registry's line, as the terminal prints it.
fn registry_said(line: &RegistryNewest) -> String {
    let registry = match line.registry {
        Registry::Npm => "npm",
        Registry::CratesIo => "crates.io",
        Registry::Github => "GitHub releases",
    };
    match &line.reading {
        RegistryReading::Read { newest } => format!(
            "newest on {registry}: {}{}",
            newest.version,
            date_suffix(newest)
        ),
        RegistryReading::Refused { refusal } => format!("{registry} could not be read: {refusal}"),
    }
}

#[cfg(test)]
mod tests {
    use super::{cut, headline};
    use kernel::Maturity;

    /// Entering alpha moves `kernel::release::MATURITY` and nothing else,
    /// so the line a person reads first has to say what it is given.
    #[test]
    fn the_status_line_says_the_maturity_it_is_given() {
        assert_eq!(
            headline(Maturity::Alpha),
            format!("sprawling {} (alpha){}", env!("CARGO_PKG_VERSION"), cut())
        );
    }
}