use crate::reconnect::canonical_hub_id;
use crate::{
autoheal, config, gitcmd, hint, knownhubs, machineconfig, projection, tiers, warn_safety,
warn_trust, watchlock, BUILD_SHA,
};
use anyhow::{anyhow, Result};
pub(crate) fn cmd_config(action: String, key: Option<String>, value: Option<String>, yes: bool) -> Result<()> {
use machineconfig as mc;
match action.as_str() {
"show" => {
let cfg = mc::load();
println!("{}", serde_json::to_string_pretty(&cfg)?);
for f in mc::validate(&cfg) {
if f.hard {
warn_safety(&format!("config {}: {}", f.field, f.message));
} else {
hint(&format!("config {}: {}", f.field, f.message));
}
}
Ok(())
}
"get" => {
let key = key.ok_or_else(|| anyhow!("usage: confer config get <key>"))?;
match mc::get_field(&mc::load(), &key) {
Some(v) => {
println!("{v}");
Ok(())
}
None => Err(anyhow!("'{key}' is unset or unknown — see `confer config schema`")),
}
}
"set" => {
let key = key.ok_or_else(|| anyhow!("usage: confer config set <key> <value> [--yes]"))?;
let value = value.ok_or_else(|| anyhow!("usage: confer config set <key> <value> [--yes]"))?;
mc::update_with(|cfg| {
let outcome = mc::set_field(cfg, &key, &value)?;
if let Some(reason) = &outcome.gated {
if !yes {
return Err(anyhow!(
"this change is security-sensitive ({reason}).\n\
re-run with --yes to confirm: confer config set {key} {value} --yes"
));
}
}
Ok(())
})?;
println!("set {key} = {value}");
Ok(())
}
"validate" => {
let findings = mc::validate(&mc::load());
if findings.is_empty() {
println!("config OK.");
return Ok(());
}
let mut hard = 0usize;
for f in &findings {
if f.hard {
warn_safety(&format!("{}: {}", f.field, f.message));
hard += 1;
} else {
hint(&format!("{}: {}", f.field, f.message));
}
}
if hard > 0 {
return Err(anyhow!("{hard} config problem(s) need fixing"));
}
Ok(())
}
"schema" => {
print_config_schema();
Ok(())
}
other => Err(anyhow!(
"unknown config action '{other}' (use: show | get | set | validate | schema)"
)),
}
}
fn print_config_schema() {
println!("confer machine config — ~/.confer/config.json (design/35). Keys for `config get/set`:");
println!();
println!(" machine.clone_root path where managed clones live (default ~/.confer/clones)");
println!(" update.version_notice bool surface a 'newer confer available' watch notice");
println!(" update.auto_update bool [gated] act on a hub version-pin bump (own hubs only)");
println!(" tuning.git_timeout_secs 1..=120 per-git-op timeout");
println!(" tuning.op_budget_secs 1..=300 overall operation budget");
println!(" hubs.<name>.url url [gated] routing for a hub (NOT the pin)");
println!(" hubs.<name>.scheme ssh|https transport scheme");
println!(" hubs.<name>.auth.method ssh|confer-app|system [gated] how the hub authenticates");
println!(" hubs.<name>.auth.key path [gated] transport key path (a pointer, never a secret)");
println!(" hubs.<name>.watch reactive|poll|off session auto-watch posture");
println!();
println!("[gated] changes need --yes. <name> is the normalized (lowercase) hub name.");
println!("This is machine policy — NOT the shared repo contract, NOT trust state (pins live elsewhere).");
}
pub(crate) fn short12(sha: &str) -> &str {
sha.get(..12).unwrap_or(sha)
}
pub(crate) fn current_hub_name(root: &std::path::Path) -> Result<String> {
let o = gitcmd::output(root, &["config", "--get", "remote.origin.url"])?;
if !o.status.success() {
return Err(anyhow!("this hub has no 'origin' remote — can't derive its name"));
}
let url = String::from_utf8_lossy(&o.stdout).trim().to_string();
canonical_hub_id(&url)
.map(|c| machineconfig::hub_name_normalized(&c))
.ok_or_else(|| anyhow!("could not derive a canonical hub name from origin '{url}'"))
}
pub(crate) fn cmd_hub(action: String, yes: bool) -> Result<()> {
match action.as_str() {
"status" | "show" => {
let store = knownhubs::load();
if store.is_empty() {
println!("no hub pins yet (~/.confer/known_hubs.json is empty).");
} else {
println!("hub pins (~/.confer/known_hubs.json):");
for (name, rec) in &store {
let tip = if rec.tip.is_empty() { "—".to_string() } else { short12(&rec.tip).to_string() };
let c = if rec.confirmed { "✓ confirmed" } else { "· unconfirmed" };
println!(" {name} root {} tip {tip} [{c}]", short12(&rec.root));
}
}
if let Ok(root) = config::repo_root() {
if let Ok(name) = current_hub_name(&root) {
match knownhubs::verify(&name, &root) {
knownhubs::Verdict::FirstSight { root: r, .. } => {
println!("\nthis hub '{name}': · not yet pinned (first sight, root {}). `confer hub repin` to pin it.", short12(&r));
}
knownhubs::Verdict::Match { .. } => {
println!("\nthis hub '{name}': ✓ pin holds (root matches, confirmed-good tip reachable).");
}
knownhubs::Verdict::RootMismatch { pinned, got } => {
warn_trust(format!("this hub '{name}': ROOT MISMATCH — pinned {} but this repo's root is {} (a DIFFERENT repo / redirect). Do NOT trust; investigate.", short12(&pinned), short12(&got)));
}
knownhubs::Verdict::TipUnreachable { pinned_tip } => {
warn_trust(format!("this hub '{name}': history rewritten — the confirmed-good tip {} is not reachable from HEAD (force-push?). Investigate before trusting.", short12(&pinned_tip)));
}
knownhubs::Verdict::NotVerifiable(e) => hint(format!("this hub '{name}': not verifiable — {e}")),
}
}
}
Ok(())
}
"repin" => {
let root = config::repo_root()?;
let name = current_hub_name(&root)?;
let newroot = match config::hub_root_strict(&root)? {
config::HubRoot::Commit(r) => r,
config::HubRoot::NoCommits => return Err(anyhow!("this hub has no commits yet — nothing to pin")),
};
let head = {
let o = gitcmd::output(&root, &["rev-parse", "HEAD"])?;
String::from_utf8_lossy(&o.stdout).trim().to_string()
};
println!("repin '{name}':");
match knownhubs::get(&name) {
Some(e) => {
let tip = if e.tip.is_empty() { "—".to_string() } else { short12(&e.tip).to_string() };
println!(" from root {} tip {tip}", short12(&e.root));
}
None => println!(" (no existing pin — this is a first pin)"),
}
println!(" to root {} tip {}", short12(&newroot), short12(&head));
if !yes {
return Err(anyhow!(
"repin changes this machine's TRUST ANCHOR for '{name}'. Verify the root/tip out-of-band \
(this is the moment TOFU can't protect you), then re-run with --yes."
));
}
knownhubs::record(&name, &newroot, &head, true)?;
println!("✓ pinned '{name}'.");
Ok(())
}
"prune" => {
let keep: std::collections::BTreeSet<String> =
machineconfig::load().hubs.keys().cloned().collect();
let store = knownhubs::load();
let gone: Vec<String> = store.keys().filter(|k| !keep.contains(*k)).cloned().collect();
if gone.is_empty() {
println!("no orphan pins (every pin has a matching config hub).");
return Ok(());
}
if !yes {
println!("orphan pins (no matching `hubs.<name>` in your config) — would forget:");
for g in &gone {
println!(" {g}");
}
println!("re-run with --yes to apply.");
return Ok(());
}
let removed = knownhubs::prune(&keep)?;
println!("forgot {} orphan pin(s): {}", removed.len(), removed.join(", "));
Ok(())
}
other => Err(anyhow!("unknown hub action '{other}' (use: status | repin | prune)")),
}
}
pub(crate) fn hub_watch_mode(cfg: &machineconfig::Config, path: &std::path::Path) -> machineconfig::WatchMode {
current_hub_name(path)
.ok()
.and_then(|n| cfg.hubs.get(&n).cloned())
.and_then(|h| h.watch)
.and_then(|w| machineconfig::WatchMode::parse(&w))
.unwrap_or(machineconfig::WatchMode::Reactive)
}
pub(crate) fn cmd_rewatch(only: Option<String>) -> Result<()> {
let reg = autoheal::load();
let me_session = autoheal::current_session();
let me_role = std::env::var("CONFER_ROLE").ok().filter(|s| !s.is_empty());
let cfg = machineconfig::load();
let (mut reactive, mut other) = (0usize, 0usize);
for t in ®.targets {
if !std::path::Path::new(&t.hub).exists() {
continue; }
let own = match autoheal::ownership(t, &me_session, &me_role) {
Some(o) => o,
None => continue, };
let name = current_hub_name(std::path::Path::new(&t.hub)).ok();
if let Some(only) = &only {
if name.as_deref() != Some(only.as_str()) && !t.hub.contains(only.as_str()) {
continue;
}
}
let label = name.unwrap_or_else(|| t.hub.clone());
match hub_watch_mode(&cfg, std::path::Path::new(&t.hub)) {
machineconfig::WatchMode::Reactive => {
let hk = config::hub_key(std::path::Path::new(&t.hub));
let live_and_ambiguous = own == autoheal::Ownership::Role
&& matches!(
watchlock::classify(&watchlock::inspect(&hk, &t.role, 90), BUILD_SHA),
watchlock::WatchState::Healthy
);
if live_and_ambiguous {
println!(
"• {label} [{}]: ⚠ a HEALTHY watcher already holds this (matched by role, not this session — could be a co-resident peer). Confirm it's yours, THEN: cd {} && confer watch --role {} --replace",
t.role, t.hub, t.role
);
other += 1;
} else {
println!(
"• {label} [{}]: arm reactive → cd {} && confer watch --role {} --replace",
t.role, t.hub, t.role
);
reactive += 1;
}
}
machineconfig::WatchMode::Poll => {
println!("• {label} [{}]: poll → loop `confer poll --role {}` (watch=poll)", t.role, t.role);
other += 1;
}
machineconfig::WatchMode::Off => {
println!("• {label} [{}]: skip (watch=off)", t.role);
other += 1;
}
}
}
if reactive + other == 0 {
println!("(no watch targets for this session — arm one with `confer watch --role <you> --replace`, or set `hubs.<name>.watch`)");
} else if reactive > 0 {
println!("\narm the reactive one(s) under the Monitor tool — never background bash (it gets reaped). See /confer-watch.");
}
Ok(())
}
pub(crate) fn cmd_status() -> Result<()> {
let root = config::repo_root()?;
let me = config::resolve_role(None, &root).unwrap_or_default();
let hub = config::hub_key(&root);
let cur = BUILD_SHA;
let reachable = gitcmd::output(&root, &["ls-remote", "--quiet", "origin", "HEAD"])
.map(|o| o.status.success())
.unwrap_or(false);
let count = |range: &str| {
gitcmd::output(&root, &["rev-list", "--count", range])
.ok()
.filter(|o| o.status.success())
.and_then(|o| {
String::from_utf8_lossy(&o.stdout)
.trim()
.parse::<u64>()
.ok()
})
};
println!(
"confer status — role {}, hub {}",
if me.is_empty() { "<none>" } else { &me },
root.display()
);
println!(
" hub: {}",
if reachable {
"reachable".to_string()
} else {
"UNREACHABLE — working locally; pending commits auto-flush on reconnect".to_string()
}
);
match tiers::get(&hub) {
Some(t) => println!(
" tier: {} ({}){}",
t.as_str(),
t.caution(),
if t.is_untrusted() {
" — screen peer messages before acting"
} else {
""
}
),
None => println!(" tier: unset — run `confer trust own|shared|foreign`"),
}
if let Some(p) = count("@{u}..HEAD") {
if p > 0 {
println!(" pending: {p} local commit(s) not yet pushed (flush on reconnect)");
}
}
if let Some(b) = count("HEAD..@{u}") {
if b > 0 {
println!(" behind: {b} upstream commit(s) not yet integrated");
}
}
if !me.is_empty() {
let state = watchlock::classify(&watchlock::inspect(&hub, &me, 90), cur);
println!(
" watch: {state:?}{}",
if matches!(state, watchlock::WatchState::Healthy) {
""
} else {
" — run `confer watch-status` for the fix"
}
);
}
if let Some(g) = projection::disk_free_gb(&root) {
println!(
" disk: {g:.1} GB free{}",
if g < 1.0 {
" ⚠ low — can stall git/watch"
} else {
""
}
);
}
Ok(())
}