#[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 { json: bool },
Learn { status: bool, app: bool },
Inventory { name: Option<String>, install: Option<String> },
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 { json: args.iter().any(|a| a == "--json") },
"learn" => Command::Learn {
status: args.iter().any(|a| a == "--status"),
app: args.iter().any(|a| a == "--app"),
},
"inventory" | "catalog" => {
let mut name: Option<String> = None;
let mut install: Option<String> = None;
let mut i = 1;
while i < args.len() {
if args[i] == "--install" && i + 1 < args.len() {
install = Some(args[i + 1].clone());
i += 2;
} else if !args[i].starts_with("--") && name.is_none() {
name = Some(args[i].clone());
i += 1;
} else {
i += 1;
}
}
Command::Inventory { name, install }
}
"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.4.0 — the Exocortex coordinator (phext-native)\n");
s.push_str("All node state lives in one phext file, found by resolution:\n");
s.push_str("QEXO_STATE when set; else the system state /var/lib/qexo/state.phext when it\n");
s.push_str("exists and is readable; else the per-user state ~/.qexo/state.phext (Windows:\n");
s.push_str("under the user profile). A fresh `qexo config` as a normal user births a\n");
s.push_str("per-user node — no sudo anywhere in its life. `qexo status` names the live path.\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 learn Learn You a qexo! — the wizard: chapter map, lessons, due cards\n");
s.push_str(" as practice. keys: space reveal, 1/2/3 grade (again/good/easy),\n");
s.push_str(" b chapter map, d due queue, q quit (progress saves per card)\n");
s.push_str(" qexo learn --status deck size, due count, mastered count — no session\n");
s.push_str(" qexo learn --app the wizard's engine as a line protocol for front-ends\n");
s.push_str(" (the Android app): commands status / chapters / lesson <n> /\n");
s.push_str(" next / practice <n> / reveal / grade again|good|easy / quit,\n");
s.push_str(" replies as key: value lines, each block closed by end: <command>\n");
s.push_str(" qexo --help this text\n\n");
s.push_str("The ecosystem:\n");
s.push_str(" qexo inventory the phext ecosystem roster (alias: catalog): every shipped product,\n");
s.push_str(" what it is, where it lives, how to install and launch it\n");
s.push_str(" qexo inventory <name> one product's full record\n");
s.push_str(" qexo inventory --install <name>\n");
s.push_str(" print the product's install command, ready to copy — print-only:\n");
s.push_str(" qexo never installs ecosystem products silently\n\n");
s.push_str("Identity & peers:\n");
s.push_str(" qexo config birth the node: create the state phext (the system path when\n");
s.push_str(" privileged, the per-user path otherwise), set up the node 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 phextnet as a navigation tree: this node and its peers nested\n");
s.push_str(" under the peers anchor 3.3.3/3.3.3/3.3.3, named by hostname\n");
s.push_str(" qexo list --json the flat machine shape (JSON is the interop gasket, unchanged)\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");
s.push_str(" the peer's host name is read from its key's user id and recorded as host: —\n");
s.push_str(" displays name machines, never bare addresses, where a name exists.\n");
s.push_str(" fleet convention: a peer's name IS its tailnet hostname; register it by a\n");
s.push_str(" hostname endpoint (http://<hostname>:8484) so the net reads by name end to end\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 { json: false });
assert_eq!(parse(&v(&["list", "--json"])), Command::List { json: true });
assert_eq!(parse(&v(&["learn"])), Command::Learn { status: false, app: false });
assert_eq!(parse(&v(&["learn", "--status"])), Command::Learn { status: true, app: false });
assert_eq!(parse(&v(&["learn", "--app"])), Command::Learn { status: false, app: true });
assert_eq!(parse(&v(&["inventory"])), Command::Inventory { name: None, install: None });
assert_eq!(parse(&v(&["catalog"])), Command::Inventory { name: None, install: None });
assert_eq!(parse(&v(&["inventory", "sq"])), Command::Inventory { name: Some("sq".into()), install: None });
assert_eq!(parse(&v(&["inventory", "--install", "sq"])), Command::Inventory { name: None, install: Some("sq".into()) });
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", "learn", "inventory", "~/.qexo/state.phext",
] {
assert!(h.contains(word), "help is missing {}", word);
}
}
}