mod collectives;
mod config;
mod prompt;
mod steps;
use std::io::IsTerminal;
use std::path::Path;
use crate::harness::{self, Harness};
use crate::install::{self, Action, Change, Targets};
use crate::{collective, learn, runner};
use collectives::Mesh;
use prompt::Prompt;
const USAGE: &str = "usage: borg setup [--yes] [--no-vectors] [--harness HARNESS,...|all] [--private DIR...] \
[--team NAME --paths DIR... [--relay URL|disabled]] [--join TICKET [--paths DIR...]] [--home DIR]";
pub const FLAGS: &[&str] = &["--yes", "--no-vectors", "--harness", "--private", "--team", "--paths", "--relay", "--join", "--home"];
#[derive(Debug, Default)]
pub struct Opts {
yes: bool,
no_vectors: bool,
harness: Option<String>,
join: Option<String>,
team: Option<String>,
relay: Option<String>,
home: Option<String>,
private: Option<Vec<String>>,
paths: Vec<String>,
}
impl Opts {
fn set(&mut self, flag: &str, rest: &[&str]) -> Result<usize, String> {
let values: Vec<String> = rest.iter().take_while(|a| !a.starts_with("--")).map(|a| (*a).to_string()).collect();
match flag {
"--yes" | "-y" => self.yes = true,
"--no-vectors" => self.no_vectors = true,
"--private" => self.private = Some(values.clone()),
"--paths" => self.paths.clone_from(&values),
_ => {
let slot = self.value(flag).ok_or(format!("unknown flag {flag}"))?;
*slot = Some(values.first().ok_or(format!("{flag} needs a value"))?.clone());
return Ok(1);
}
}
Ok(if matches!(flag, "--private" | "--paths") { values.len() } else { 0 })
}
fn value(&mut self, flag: &str) -> Option<&mut Option<String>> {
match flag {
"--harness" => Some(&mut self.harness),
"--join" => Some(&mut self.join),
"--team" => Some(&mut self.team),
"--relay" => Some(&mut self.relay),
"--home" => Some(&mut self.home),
_ => None,
}
}
fn check(&self) -> Result<(), String> {
if self.team.is_some() && self.join.is_some() {
return Err("--team founds a collective, --join joins one: not both".into());
}
if !self.paths.is_empty() && self.team.is_none() && self.join.is_none() {
return Err("--paths names a team collective's repos: with --team or --join".into());
}
if self.yes && self.team.is_some() && self.paths.is_empty() {
return Err("--yes --team NAME needs --paths DIR...: a team collective without repo directories routes no repo".into());
}
self.values()
}
fn values(&self) -> Result<(), String> {
if self.relay.is_some() && self.team.is_none() {
return Err("--relay is the founded collective's (--team); a joiner gets it from the ticket".into());
}
self.join.as_deref().map_or(Ok(()), collectives::ticket)?;
self.relay.as_deref().map_or(Ok(None), collectives::relay_value).map(drop)
}
}
fn parse(args: &[&str]) -> Result<Opts, String> {
let mut o = Opts::default();
let mut i = 0;
while let Some(flag) = args.get(i) {
i += 1 + o.set(flag, &args[i + 1..])?;
}
o.check()?;
Ok(o)
}
#[must_use]
pub fn main(args: &[&str]) -> i32 {
if args.iter().any(|a| matches!(*a, "-h" | "--help")) {
println!("{USAGE}");
return 0;
}
let o = match parse(args) {
Ok(o) => o,
Err(e) => {
eprintln!("borg setup: {e}\n{USAGE}");
return 2;
}
};
if !o.yes && !std::io::stdin().is_terminal() {
eprintln!("borg setup asks questions and needs a terminal; --yes takes every default without asking");
return 2;
}
if let Some(h) = &o.home {
install::isolate("setup", Path::new(h));
}
let mut stdin = std::io::stdin().lock();
let mut ask = Prompt::new(o.yes, &mut stdin);
run(&o, &mut ask).unwrap_or_else(|e| {
eprintln!("borg setup: {e}");
1
})
}
type Wire = (&'static dyn Harness, Vec<Change>);
struct Plan {
config: Option<Change>,
mesh: Option<Mesh>,
vectors: Option<String>,
wiring: Vec<Wire>,
}
fn run(o: &Opts, ask: &mut Prompt) -> Result<i32, String> {
let t = o.home.as_deref().map_or_else(Targets::from_env, |h| Targets::under(Path::new(h)));
ask.heading("1. What is here");
let found = detect(&t);
ask.heading("2. Collectives");
let (want, mesh) = collectives::choose(o, ask, collective::config_path().exists())?;
let config = config::change(&collective::config_path(), &want)?;
ask.heading("3. Wiring");
let wiring = wiring(&harnesses(o, ask, &found)?, &t)?;
let mesh = mesh.filter(|m| !steps::done(m));
let plan = Plan { config, mesh, vectors: steps::vectors_plan(o.no_vectors), wiring };
ask.heading("4. The plan");
if !show(&plan, &t) {
println!("nothing to change");
} else if !ask.yes_no("Do all of this?", true) {
println!("nothing written");
return Ok(0);
}
let stamp = format!("{}Z", install::utc(learn::now_ms()));
let mut warns = Vec::new();
let (code, mesh) = match plan.config.as_ref().map_or(0, |c| steps::config(c, &stamp)) {
0 => act(o, &plan, ask, &stamp, &mut warns),
code => {
warns
.push("nothing else was done (mesh, vectors, wiring): the collectives config could not be written; fix that and run `borg setup` again".into());
(code, None)
}
};
ask.heading("5. Check");
Ok(verify(o, &warns, mesh).max(code))
}
fn act<'p>(o: &Opts, plan: &'p Plan, ask: &mut Prompt, stamp: &str, warns: &mut Vec<String>) -> (i32, Option<&'p Mesh>) {
let mesh = plan.mesh.as_ref().filter(|m| steps::mesh(m, ask, warns));
let failed = i32::from(plan.mesh.is_some() && mesh.is_none());
steps::vectors(o.no_vectors, ask, warns);
let code = steps::wire(&plan.wiring, stamp).max(failed);
steps::import(ask);
(code, mesh)
}
fn detect(t: &Targets) -> Vec<&'static dyn Harness> {
let found: Vec<&'static dyn Harness> = harness::visible().filter(|h| install::which(h.probe().cli).is_some()).collect();
for h in harness::visible() {
let cli = if found.iter().any(|f| f.name() == h.name()) { "installed" } else { "not installed" };
println!("{:<9} {cli}; {}", h.name(), install::wiring(h, t));
}
let spec = std::env::var("BORG_PREDICT_MODEL").ok();
println!("{}", runner::availability(spec.as_deref(), &runner::on_path).unwrap_or_else(|e| e));
let cwd = std::env::current_dir().map(|d| d.display().to_string()).unwrap_or_default();
match learn::engineer(&cwd) {
e if e.is_empty() => println!("git: no user.email; lessons will name no author (git config --global user.email ...)"),
e => println!("git: you are {e}"),
}
let path = collective::config_path();
if path.exists() {
println!("config: {} exists; setup only adds to it", path.display());
} else {
println!("config: none yet at {}", path.display());
}
found
}
fn harnesses(o: &Opts, ask: &mut Prompt, found: &[&'static dyn Harness]) -> Result<Vec<&'static dyn Harness>, String> {
if let Some(h) = &o.harness {
println!("wire borg into: {h}");
return pick(h);
}
let installed = found.iter().map(|h| h.name()).collect::<Vec<_>>().join(",");
let q = format!("Wire borg into ({}, all, or - for none)", harness::names(", "));
loop {
match pick(&ask.line(&q, if installed.is_empty() { "-" } else { &installed })) {
Err(e) if !ask.yes() => println!("{e}"),
picked => return picked,
}
}
}
fn pick(answer: &str) -> Result<Vec<&'static dyn Harness>, String> {
let mut out: Vec<&'static dyn Harness> = Vec::new();
for w in prompt::split(answer) {
let hs: Vec<_> = if w == "all" { harness::visible().collect() } else { vec![harness::by_name(&w).ok_or(format!("unknown harness {w:?}"))?] };
out.extend(hs.into_iter().filter(|h| !out.iter().any(|o| o.name() == h.name())).collect::<Vec<_>>());
}
Ok(out)
}
fn wiring(hs: &[&'static dyn Harness], t: &Targets) -> Result<Vec<Wire>, String> {
let bin = install::bin();
let mut out = Vec::new();
for &h in hs {
let (changes, notes) = install::plan(Action::Install, h, t, &bin).map_err(|e| format!("{}: {e}", h.name()))?;
for n in ¬es {
println!("{}: {n}", h.name());
}
if changes.is_empty() {
println!("{}: already wired", h.name());
} else {
out.push((h, changes));
}
}
Ok(out)
}
fn show(p: &Plan, t: &Targets) -> bool {
p.config.iter().for_each(|c| print!("{}", install::diff(&c.path, c.before.as_deref(), c.after.as_deref())));
for (h, changes) in &p.wiring {
let skill = t.skill_dir(*h);
for c in changes {
if own(*h, c, skill.as_deref()) {
let n = c.after.as_deref().map_or(0, |a| a.lines().count());
println!("{}: {} ({n} lines of borg's own; `borg install {}` shows them)", h.name(), c.path.display(), h.name());
} else {
print!("{}", install::diff(&c.path, c.before.as_deref(), c.after.as_deref()));
}
}
}
if let Some(m) = &p.mesh {
println!("then: borg mesh {}", m.args().join(" "));
}
p.vectors.iter().for_each(|v| println!("then: {v}"));
p.config.is_some() || !p.wiring.is_empty() || p.mesh.is_some() || p.vectors.is_some()
}
fn own(h: &dyn Harness, c: &Change, skill: Option<&Path>) -> bool {
skill.is_some_and(|d| c.path.starts_with(d)) || (c.before.is_none() && c.after.as_deref() == Some(h.wiring().adapter()))
}
fn verify(o: &Opts, warns: &[String], mesh: Option<&Mesh>) -> i32 {
let home: Vec<&str> = o.home.as_deref().map(|h| vec!["--home", h]).unwrap_or_default();
let code = install::main("doctor", &home);
for w in warns {
println!("WARN {w}");
}
println!("\nnext:\n borg status what each collective holds\n borg collective [DIR] which collective a repo belongs to");
match mesh {
Some(Mesh::Init(name)) => {
println!(" borg mesh serve --collective {name} keep it running (a service); then `borg mesh invite --collective {name}`");
}
Some(Mesh::Join { .. }) => println!(" borg mesh serve keep it running inside one of the team's repos"),
None => {}
}
code
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flags_parse_and_conflicts_are_refused() {
let o = parse(&["--yes", "--private", "~/a", "~/b", "--team", "corp", "--paths", "~/w", "--relay", "disabled", "--no-vectors"]).unwrap();
assert!(o.yes && o.no_vectors);
assert_eq!(o.private.as_deref(), Some(&["~/a".to_string(), "~/b".to_string()][..]));
assert_eq!((o.team.as_deref(), o.paths.as_slice(), o.relay.as_deref()), (Some("corp"), &["~/w".to_string()][..], Some("disabled")));
assert!(parse(&["--team", "a", "--join", "BORG1X"]).is_err());
assert!(parse(&["--paths", "~/w"]).is_err());
assert!(parse(&["--join", "BORG1X", "--relay", "disabled"]).is_err());
assert!(parse(&["--team", "a", "--relay", "relay.corp"]).is_err());
assert!(parse(&["--yes", "--team", "a"]).unwrap_err().contains("--paths"), "--yes founds no team without repos");
assert!(parse(&["--join", "notaticket"]).unwrap_err().contains("BORG1"));
assert!(parse(&["--team"]).unwrap_err().contains("needs a value"));
assert!(parse(&["--bogus"]).unwrap_err().contains("unknown flag"));
}
#[test]
fn every_listed_flag_parses() {
for f in FLAGS {
let rest: &[&str] = if matches!(*f, "--yes" | "--no-vectors") { &[] } else { &["x"] };
let mut o = Opts::default();
assert!(o.set(f, rest).is_ok(), "{f}");
}
}
}