#[derive(Debug, Clone, PartialEq)]
pub enum Command {
Help,
Version,
WiringCheck(Option<String>),
FileOp(Vec<String>),
Config { auto_update: bool, evolve: bool, release_signer: Option<String> },
Doctor { fix: bool },
List,
Register(Vec<String>),
Release,
Update { check: bool },
Evolve { check: bool },
Host { persona: Option<String>, boot: Option<String> },
Unhost(String),
Message { persona: String, text: String },
Status,
Plan,
Serve(Option<u16>),
Bare,
Unknown(String),
}
pub fn parse(args: &[String]) -> Command {
let w = |i: usize| args.get(i).map(|s| s.as_str()).unwrap_or("");
match w(0) {
"" => Command::Bare,
"--help" | "-h" | "help" => Command::Help,
"--version" | "version" => Command::Version,
"wiring-check" => Command::WiringCheck(args.get(1).cloned()),
"parse" | "explode" | "implode" | "seal" | "unseal" | "select" | "get" => {
Command::FileOp(args.to_vec())
}
"config" => {
let mut release_signer: Option<String> = None;
let mut i = 1;
while i < args.len() {
if args[i] == "--release-signer" && i + 1 < args.len() {
release_signer = Some(args[i + 1].clone());
i += 2;
} else {
i += 1;
}
}
Command::Config {
auto_update: args.iter().any(|a| a == "--auto-update"),
evolve: args.iter().any(|a| a == "--evolve"),
release_signer,
}
}
"doctor" => Command::Doctor { fix: args.iter().any(|a| a == "--fix") },
"list" => Command::List,
"register" => Command::Register(args[1..].to_vec()),
"release" => Command::Release,
"update" => Command::Update { check: args.iter().any(|a| a == "--check") },
"evolve" => Command::Evolve { check: args.iter().any(|a| a == "--check") },
"host" => {
let mut persona: Option<String> = None;
let mut boot: Option<String> = None;
let mut i = 1;
while i < args.len() {
if args[i] == "--boot" && i + 1 < args.len() {
boot = Some(args[i + 1].clone());
i += 2;
} else if persona.is_none() {
persona = Some(args[i].clone());
i += 1;
} else {
i += 1;
}
}
Command::Host { persona, boot }
}
"unhost" => Command::Unhost(w(1).to_string()),
"message" => Command::Message {
persona: w(1).to_string(),
text: args[2..].join(" "),
},
"status" => Command::Status,
"plan" => Command::Plan,
"serve" => Command::Serve(args.get(1).and_then(|s| s.parse().ok())),
other => Command::Unknown(other.to_string()),
}
}
pub fn help_text() -> String {
let mut s = String::new();
s.push_str("qexo v1.3.0 — the Exocortex coordinator (phext-native)\n");
s.push_str("All node state lives in one file: /var/lib/qexo/state.phext\n");
s.push_str("(QEXO_STATE overrides the path for development and tests.)\n\n");
s.push_str("Everyday:\n");
s.push_str(" qexo the current plan, human rendering\n");
s.push_str(" qexo plan the same, explicitly\n");
s.push_str(" qexo status node, version, state path, key, peers, hosted, doctor headline\n");
s.push_str(" qexo doctor [--fix] health report; --fix repairs what is safely repairable\n");
s.push_str(" qexo --help this text\n\n");
s.push_str("Identity & peers:\n");
s.push_str(" qexo config birth the node: create state.phext, set up the node GPG key\n");
s.push_str(" qexo config --auto-update turn automatic updates on (config record) and install the systemd timer where possible\n");
s.push_str(" qexo config --evolve set evolve: on — the automatic timer's trigger becomes a full evolve pass\n");
s.push_str(" qexo config --release-signer <FP>\n set the trusted release signer fingerprint in the config record\n");
s.push_str(" instead of update alone (update is one stage of evolve; see below)\n");
s.push_str(" qexo list the registered peers\n");
s.push_str(" qexo register <pubkey> [--endpoint <url>] register a peer's public key (armored text, a file path, or - for stdin);\n");
s.push_str(" the endpoint (its qexo serve base URL) is recorded on the peer so update can poll it\n\n");
s.push_str("Updates — over the phextnet:\n");
s.push_str(" qexo release announce this node's binary as a signed release (manifest scroll in the ledgers;\n");
s.push_str(" the binary itself is served from the releases dir beside the state file)\n");
s.push_str(" qexo update poll peers for the trusted signer's release; verify signature + sha256, then swap\n");
s.push_str(" qexo update --check the same poll, report only — downloads and changes nothing\n");
s.push_str(" trust rule: only a release whose manifest signature verifies against the peer\n");
s.push_str(" whose fingerprint is the config record's release_signer is ever applied\n\n");
s.push_str("Convergence — one pass over the phextnet:\n");
s.push_str(" qexo evolve everything this node can learn from its peers, applied in one receipted\n");
s.push_str(" cycle: (1) peer gossip — peers-of-peers are recorded as discovered, which\n");
s.push_str(" is knowledge, never trust (a discovered peer is never polled, never a\n");
s.push_str(" signer; only `qexo register` makes a peer trusted); (2) persona sync —\n");
s.push_str(" newer persona revisions are adopted from trusted peers, and where both\n");
s.push_str(" lines advanced from a common ancestor the divergence is kept whole,\n");
s.push_str(" both copies standing as attempt artifacts — never last-writer-wins;\n");
s.push_str(" (3) the binary self-update, exactly `qexo update`. One evolution receipt\n");
s.push_str(" in the ledgers names every stage's outcome. A stage's failure never\n");
s.push_str(" blocks the next; exit is 1 only when the binary stage refuses or rolls\n");
s.push_str(" back an offer that claimed the trusted signer\n");
s.push_str(" qexo evolve --check the same pass as reads only — print what would change, write nothing\n");
s.push_str(" (update is one stage of evolve: with evolve: on in the config record,\n");
s.push_str(" the automatic timer fires `qexo evolve` instead of `qexo update`)\n\n");
s.push_str("Personas & inference:\n");
s.push_str(" qexo host <persona> load a persona into local inference (--boot <path> for a boot text file;\n");
s.push_str(" --boot against an existing, unhosted home with different text is a local\n");
s.push_str(" edit — the boot is replaced and the persona's revision bumps)\n");
s.push_str(" qexo host list hosted personas\n");
s.push_str(" qexo unhost <persona> unload a hosted persona\n");
s.push_str(" qexo message <persona> <text...> one request/response exchange with a hosted persona\n\n");
s.push_str("System:\n");
s.push_str(" qexo serve [port] the phext HTTP API (payloads are scroll text)\n");
s.push_str(" qexo wiring-check [dir] every src/*.rs declared as a module, every declaration filed\n");
s.push_str(" qexo --version version\n\n");
s.push_str("Phext files:\n");
s.push_str(" qexo parse <file> scroll inventory of a phext file\n");
s.push_str(" qexo explode <file> [dir] one .scroll file per scroll\n");
s.push_str(" qexo implode <file> parse + re-serialize (canonical form check)\n");
s.push_str(" qexo seal <file> write <file>.xz (stored-form seal)\n");
s.push_str(" qexo unseal <file.xz> print the unsealed text\n");
s.push_str(" qexo select <file> <coord> print one scroll from a phext file\n");
s.push_str(" qexo select <coord> print one scroll from this node's state\n");
s.push_str(" qexo get ... same as select, both forms\n");
s
}
#[cfg(test)]
mod tests {
use super::*;
fn v(words: &[&str]) -> Vec<String> {
words.iter().map(|s| s.to_string()).collect()
}
#[test]
fn parse_covers_the_surface() {
assert_eq!(parse(&v(&[])), Command::Bare);
assert_eq!(parse(&v(&["--help"])), Command::Help);
assert_eq!(parse(&v(&["-h"])), Command::Help);
assert_eq!(parse(&v(&["help"])), Command::Help);
assert_eq!(parse(&v(&["config"])), Command::Config { auto_update: false, evolve: false, release_signer: None });
assert_eq!(parse(&v(&["config", "--auto-update"])), Command::Config { auto_update: true, evolve: false, release_signer: None });
assert_eq!(parse(&v(&["config", "--evolve"])), Command::Config { auto_update: false, evolve: true, release_signer: None });
assert_eq!(parse(&v(&["config", "--release-signer", "A4E7277FB3EB0E589604BE5B506DBE3FC209E755"])), Command::Config { auto_update: false, evolve: false, release_signer: Some("A4E7277FB3EB0E589604BE5B506DBE3FC209E755".to_string()) });
assert_eq!(parse(&v(&["release"])), Command::Release);
assert_eq!(parse(&v(&["update"])), Command::Update { check: false });
assert_eq!(parse(&v(&["update", "--check"])), Command::Update { check: true });
assert_eq!(parse(&v(&["evolve"])), Command::Evolve { check: false });
assert_eq!(parse(&v(&["evolve", "--check"])), Command::Evolve { check: true });
assert_eq!(parse(&v(&["doctor"])), Command::Doctor { fix: false });
assert_eq!(parse(&v(&["doctor", "--fix"])), Command::Doctor { fix: true });
assert_eq!(parse(&v(&["list"])), Command::List);
assert_eq!(parse(&v(&["register", "keyfile.asc"])), Command::Register(v(&["keyfile.asc"])));
assert_eq!(parse(&v(&["host"])), Command::Host { persona: None, boot: None });
assert_eq!(
parse(&v(&["host", "Emise", "--boot", "emise.phext"])),
Command::Host { persona: Some("Emise".into()), boot: Some("emise.phext".into()) }
);
assert_eq!(parse(&v(&["unhost", "Emise"])), Command::Unhost("Emise".into()));
assert_eq!(
parse(&v(&["message", "Emise", "hello", "there"])),
Command::Message { persona: "Emise".into(), text: "hello there".into() }
);
assert_eq!(parse(&v(&["status"])), Command::Status);
assert_eq!(parse(&v(&["plan"])), Command::Plan);
assert_eq!(parse(&v(&["serve"])), Command::Serve(None));
assert_eq!(parse(&v(&["serve", "8484"])), Command::Serve(Some(8484)));
assert_eq!(parse(&v(&["wiring-check"])), Command::WiringCheck(None));
assert_eq!(parse(&v(&["select", "f.phext", "1.1.1/1.1.1/1.1.1"])), Command::FileOp(v(&["select", "f.phext", "1.1.1/1.1.1/1.1.1"])));
assert_eq!(parse(&v(&["frobnicate"])), Command::Unknown("frobnicate".into()));
}
#[test]
fn help_lists_every_command() {
let h = help_text();
for word in [
"config", "doctor", "list", "register", "release", "update", "evolve", "host", "unhost", "message", "status",
"plan", "serve", "wiring-check", "parse", "explode", "implode", "seal", "unseal",
"select", "get", "--help", "/var/lib/qexo/state.phext",
] {
assert!(h.contains(word), "help is missing {}", word);
}
}
}