use super::city::report;
use super::grammar::Arguments;
use super::router::{client_summary, log_floor, log_levels};
use super::version::{check, headline};
use super::{DEPENDENCIES, install, wire_client};
use kernel::consts_policy::DEFAULT_AT;
use sprawling::{assembly, serving};
use std::process::ExitCode;
pub(super) fn enrol(read: &Arguments) -> ExitCode {
let Some(reference) = read.positional(1) else {
return ExitCode::from(2);
};
let Some((realm, name)) = wire_client::split_reference(reference) else {
eprintln!("not a credential reference: {reference}");
eprintln!("recovery: give it as <realm>/<name>, for example modelscope/api");
return ExitCode::from(2);
};
let value = match std::io::read_to_string(std::io::stdin()) {
Ok(text) => text.trim().to_owned(),
Err(err) => {
eprintln!("could not read the credential from stdin: {err}");
return ExitCode::FAILURE;
}
};
if value.is_empty() {
eprintln!("nothing arrived on stdin");
eprintln!(
"recovery: pipe the value in, for example: cat key.txt | sprawling enrol {reference}"
);
return ExitCode::from(2);
}
let at = read.value("--at").unwrap_or(DEFAULT_AT);
match wire_client::enrol(at, realm, name, &value) {
Ok(reference) => {
println!("{reference}");
eprintln!("accepted; the city stores it as soon as its worker is free");
ExitCode::SUCCESS
}
Err(err) => report(err),
}
}
pub(super) fn install(args: &[String]) -> ExitCode {
let direction = if args.iter().any(|a| a == "--uninstall") {
install::Direction::Uninstall
} else {
install::Direction::Install
};
let done = match install::install(direction) {
Ok(done) => done,
Err(err) => return report(err),
};
println!();
match direction {
install::Direction::Uninstall => println!(" removed {}", done.binary.display()),
install::Direction::Install => println!(" installed {}", done.binary.display()),
}
match done.path {
install::PathOutcome::Unchanged => println!(" PATH already said what it needed to say"),
install::PathOutcome::Rewritten => {
println!(" PATH rewritten for this user account");
println!();
println!(" Open a NEW shell window - a running one keeps the PATH it started with.");
}
install::PathOutcome::SelfService(line) => {
println!(" that directory is not on PATH; add this line where you keep such lines:");
println!();
println!(" {line}");
}
}
if let Some(notice) = done.notice {
println!(" note: {notice}");
}
println!();
ExitCode::SUCCESS
}
pub(super) fn export(city: Option<&String>, dest: Option<&String>) -> ExitCode {
let (Some(city), Some(dest)) = (city, dest) else {
eprintln!("usage: sprawling export <city-dir> <bundle-dir>");
return ExitCode::from(2);
};
match storage::Bundle::export(std::path::Path::new(city), std::path::Path::new(dest)) {
Ok(manifest) => {
println!(
"exported {} record(s), {} object(s), {} file(s) to {dest}",
manifest.records(),
manifest.cas_objects(),
manifest.files()
);
println!("chain head: {}", manifest.head());
ExitCode::SUCCESS
}
Err(err) => report(err.into_ax()),
}
}
pub(super) fn restore(bundle: Option<&String>, city: Option<&String>) -> ExitCode {
let (Some(bundle), Some(city)) = (bundle, city) else {
eprintln!("usage: sprawling restore <bundle-dir> <city-dir>");
return ExitCode::from(2);
};
match storage::Bundle::restore(std::path::Path::new(bundle), std::path::Path::new(city)) {
Ok(manifest) => {
println!(
"restored {} record(s) into {city}; chain head {}",
manifest.records(),
manifest.head()
);
ExitCode::SUCCESS
}
Err(err) => report(err.into_ax()),
}
}
pub(super) fn fork(args: &[String]) -> ExitCode {
let (Some(dir), Some(run_raw), Some(seq_raw)) = (args.get(1), args.get(2), args.get(3)) else {
eprintln!("usage: sprawling fork <city-dir> <run-id> <at-seq> [addr]");
return ExitCode::from(2);
};
let run = match kernel::RunId::parse(run_raw) {
Ok(run) => run,
Err(err) => return report(err),
};
let Ok(seq) = seq_raw.parse::<u64>() else {
eprintln!("not a sequence number: {seq_raw}");
return ExitCode::from(2);
};
let addr = match args.get(4).map(|raw| kernel::Address::parse(raw)) {
None => None,
Some(Ok(addr)) => Some(addr),
Some(Err(err)) => return report(err),
};
let (vault, _notice) = serving::open_vault();
let Some(room) = addr else {
eprintln!("usage: sprawling fork <city-dir> <run> <seq> <addr>");
eprintln!("a branch opens a session in a room; name the room it opens in");
return ExitCode::from(2);
};
let origin = kernel::Origin {
run,
at_seq: kernel::Seq::new(seq),
};
let outcome = accounting::worker::RunWorker::new(
std::path::Path::new(dir),
runtime::diagnostics::Diagnostics::off(),
assembly::hands(vault),
)
.and_then(|mut worker| {
worker.handle(wire::Command::OpenSession {
addr: room.clone(),
carry: wire::Carry::Nothing,
from: Some(origin),
idem: kernel::IdemKey::derive(&kernel::RunId::CITY, kernel::Seq::FIRST, b"fork"),
})
});
match outcome {
Ok(()) => {
println!("{} now branches from {run} at seq {seq}", room.as_str());
println!("the next dispatch into it inherits that conversation");
ExitCode::SUCCESS
}
Err(err) => report(err),
}
}
pub(super) fn adopt(dir: Option<&String>, addr: Option<&String>) -> ExitCode {
let (Some(dir), Some(addr_raw)) = (dir, addr) else {
eprintln!("usage: sprawling adopt <city-dir> <addr>");
eprintln!("move or clone the directory under the city first, then adopt it");
return ExitCode::from(2);
};
let addr = match kernel::Address::parse(addr_raw) {
Ok(addr) => addr,
Err(err) => return report(err),
};
let (vault, _notice) = serving::open_vault();
let outcome = accounting::worker::RunWorker::new(
std::path::Path::new(dir),
runtime::diagnostics::Diagnostics::off(),
assembly::hands(vault),
)
.and_then(|mut worker| worker.adopt_building(addr.clone()));
match outcome {
Ok(()) => {
println!(
"adopted {} - its files are untouched, its rules are new",
addr.as_str()
);
println!("edit {}/RULES.toml to shape them", addr.as_str());
ExitCode::SUCCESS
}
Err(err) => report(err),
}
}
pub(super) fn replay(dir: Option<&String>) -> ExitCode {
let Some(dir) = dir else {
eprintln!("usage: sprawling replay <ledger-dir>");
return ExitCode::from(2);
};
match verified_chain(std::path::Path::new(dir)) {
Ok(line) => {
println!("{line}");
ExitCode::SUCCESS
}
Err(err) => {
eprintln!("{err}");
eprintln!("recovery: {}", err.recovery());
ExitCode::FAILURE
}
}
}
pub(super) fn verified_chain(dir: &std::path::Path) -> Result<String, kernel::AxError> {
let segments = storage::ledger_segments_at(dir).map_err(storage::StorageError::into_ax)?;
if segments.is_empty() {
return Err(kernel::AxError::failure(
kernel::AxCode::PathNotFound,
"verify ledger",
dir.display().to_string(),
)
.with_recovery(
"no ledger segment here; point at the ledger directory itself \
rather than at the city that contains it",
));
}
match storage::audit_chain(dir).map_err(storage::StorageError::into_ax)? {
storage::ChainAudit::Whole { lines } => Ok(format!(
"chain verified: {lines} line(s), tail seq {}",
lines
.checked_sub(1)
.map_or_else(|| "none".to_owned(), |tail| tail.to_string())
)),
storage::ChainAudit::Broken(reason) => Err(reason),
}
}
pub(super) fn status(args: &[String]) -> ExitCode {
if args.iter().any(|a| a == "--deps") {
print!("{DEPENDENCIES}");
return ExitCode::SUCCESS;
}
println!("{}", headline(kernel::release::MATURITY));
println!("client: {}", client_summary());
println!(
"built from {} crate(s); list them with status --deps",
DEPENDENCIES.lines().count()
);
match log_floor(args) {
Ok(Some(level)) => println!("log: {level} and everything a wider audience reads"),
Ok(None) => println!("log: off"),
Err(unknown) => {
eprintln!("not a log level: {unknown}");
eprintln!("recovery: {}", log_levels());
return ExitCode::from(2);
}
}
if args.iter().any(|a| a == "--check") {
return check();
}
ExitCode::SUCCESS
}