use std::io::{self, BufRead, Write};
fn ask(prompt: &str) -> Option<String> {
print!("{prompt}");
let _ = io::stdout().flush();
let mut line = String::new();
match io::stdin().lock().read_line(&mut line) {
Ok(0) | Err(_) => None,
Ok(_) => Some(line.trim().to_string()),
}
}
fn yes(prompt: &str) -> bool {
matches!(
ask(prompt).as_deref().map(str::trim),
Some("y") | Some("Y") | Some("yes") | Some("YES")
)
}
fn rule() {
println!("\n----------------------------------------------------------------\n");
}
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
println!("gage quickstart");
println!("Three things: who you are, who you trust, and one handoff.");
println!("Press Ctrl-C at any point, or leave an answer empty to skip it.");
rule();
let id = match crate::identity::Identity::load() {
Ok(id) => {
println!("You already have an identity, so this leaves it alone.");
id
}
Err(_) => {
println!("First, an identity. This makes a keypair on this machine.");
println!("A name is just a label for you; leave it empty and gage");
println!("derives two words from the key itself.");
let name = ask("\n a name for yourself (or empty): ").unwrap_or_default();
let chosen = if name.is_empty() { None } else { Some(name) };
let id = crate::identity::Identity::create_named(chosen.as_deref())?;
println!(
"\nmade you an identity in {}",
crate::identity::home().display()
);
id
}
};
let token = id.full_key();
let fp = crate::words::fingerprint_of(&id.public_b64(), Some(&id.box_public_b64()));
println!("\n you are : {}", crate::manifest::one_line(&id.handle));
println!(" this key says : {fp}");
println!();
println!(" Those four word-pairs come from your key, so nobody picks their");
println!(" own. They are what a colleague reads back to you out loud, on a");
println!(" call, to check the key they got is really yours. That one human");
println!(" moment is what makes SENDER VERIFIED mean anything later.");
println!("\n Send them this line (it is public, so any channel will do):");
println!(
"\n gage add {} {}",
crate::manifest::one_line(&crate::manifest::shell_word(&id.handle)),
token
);
rule();
println!("Second, their key. When they run `gage connect` or");
println!("`gage quickstart`, they get a line like the one above.");
let mut added: Option<String> = None;
if yes("\n do you have their line to paste now? [y/N] ") {
println!("\n Paste the whole `gage add ...` line, or just the key.");
let pasted = ask(" > ").unwrap_or_default();
if pasted.is_empty() {
println!(" nothing pasted -- skipping. `gage add <name> <key>` later.");
} else {
let (name, key) = parse_add(&pasted);
let key = match key {
Some(k) => k,
None => {
println!(" that does not look like a key -- skipping.");
String::new()
}
};
if !key.is_empty() {
let name = match name {
Some(n) => n,
None => ask(" what do you want to call them? ").unwrap_or_default(),
};
if name.is_empty() {
println!(" no name given -- skipping.");
} else {
match crate::trust::add_contact(&name, &key, crate::trust::AddOpts::plain()) {
Ok(lines) => {
println!();
for l in lines {
println!("{l}");
}
println!();
println!(" NOW the part nothing can do for you: ask them to read");
println!(" those four pairs back, on a call or in person -- not in");
println!(" the channel the line came through. If one word differs,");
println!(" it is not their key and somebody is in between.");
added = Some(name);
}
Err(e) => println!("\n not added: {e}"),
}
}
}
}
} else {
println!(" skipped. `gage add <name> <key>` when you have it.");
}
rule();
println!("Third, a handoff. This builds a signed bundle addressed to them.");
if !yes("\n send one now? [y/N] ") {
println!(" skipped.");
finish(added.as_deref());
return Ok(());
}
let to = match added.clone() {
Some(n) => {
println!("\n sending to {}", crate::manifest::one_line(&n));
n
}
None => ask("\n who to (a name on your list): ").unwrap_or_default(),
};
if to.is_empty() {
println!(" no recipient -- skipping.");
finish(added.as_deref());
return Ok(());
}
let subject = {
let s = ask(" subject: ").unwrap_or_default();
if s.is_empty() {
"hello from gage".to_string()
} else {
s
}
};
let path = ask(" a file to send (empty writes a short note for you): ").unwrap_or_default();
let out_dir = std::env::current_dir()?.join("gage-first-handoff");
let file = if path.is_empty() {
let tmp = std::env::temp_dir().join(format!("gage-hello-{}.md", std::process::id()));
std::fs::write(&tmp, "Hello -- this is my first gage handoff.\n")?;
println!(" (no file given, so sending a one-line note)");
tmp
} else {
std::path::PathBuf::from(path)
};
let args = crate::outbound::SendArgs {
to: vec![to.clone()],
subject,
out: Some(out_dir.clone()),
file: vec![file],
prompt: vec![],
decision: vec![],
memory: vec![],
dir: vec![],
allow_secrets: false,
reply_to: None,
msg_type: "share".to_string(),
allow_findings: false,
lan: false,
agent: None,
model: None,
record_send: true,
ttl: None,
expires: None,
context: None,
};
let mut say = Vec::new();
let sent = crate::outbound::send_bundle(args, &mut say);
for l in &say {
println!("{l}");
}
match sent {
Ok(_) => {
println!();
println!(" To get it to them, pick whichever you have:");
println!(
" gage push {} --to {} --yes (through a relay, if you joined one)",
out_dir.display(),
crate::manifest::shell_word(&to)
);
println!(" or hand that directory over any way you like -- a shared");
println!(" folder, git, email. It is signed; the transport is not");
println!(" trusted with anything.");
println!();
println!(" On their side, one command: gage open <that directory>");
println!(" which is where they see who signed it and that nothing");
println!(" inside it carries any authority. That screen is the product.");
}
Err(e) => println!("\n not sent: {e}"),
}
finish(added.as_deref());
Ok(())
}
pub(crate) fn parse_add(s: &str) -> (Option<String>, Option<String>) {
let looks_like_key = |t: &str| t.len() > 40 && t.contains('=');
let rest = s
.trim()
.strip_prefix("gage add")
.map(str::trim)
.unwrap_or_else(|| s.trim());
if let Some(after) = rest.strip_prefix('\'')
&& let Some((name, tail)) = after.split_once('\'')
{
let key = tail.split_whitespace().find(|t| looks_like_key(t));
return (Some(name.to_string()), key.map(str::to_string));
}
let toks: Vec<&str> = rest.split_whitespace().collect();
let key = toks
.iter()
.find(|t| looks_like_key(t))
.map(|t| t.to_string());
let name = match (&key, toks.len()) {
(Some(_), 2) if !looks_like_key(toks[0]) => Some(toks[0].to_string()),
_ => None,
};
(name, key)
}
fn finish(added: Option<&str>) {
rule();
println!("That is the whole of it. Worth knowing:");
println!(" gage ui a local page that says which steps are done");
println!(" gage inbox what is waiting, unread");
println!(" gage open <dir> read one, inside the trust frame");
println!(" gage whoami your line, and your word-pairs again");
if added.is_none() {
println!(" gage add <name> <key> put someone on your list");
}
println!("\nThe full tour is in README.md; driving this from a script is in");
println!("docs/agents.md.");
}
#[cfg(test)]
mod tests {
use super::parse_add;
const K: &str =
"3cEWfCOaxNwnYWOgB2wAQ4FotbAFBwC12ta7GRcsde4=.l64eR5VCbVPJycmQft4DTbePRDrFaMa+bHfybn0QZxU=";
#[test]
fn a_pasted_add_line_is_taken_apart() {
assert_eq!(
parse_add(&format!("gage add alice {K}")),
(Some("alice".into()), Some(K.into()))
);
assert_eq!(
parse_add(&format!("gage add 'crisp beetle' {K}")),
(Some("crisp beetle".into()), Some(K.into()))
);
assert_eq!(parse_add(K), (None, Some(K.into())));
assert_eq!(parse_add("nonsense"), (None, None));
assert_eq!(parse_add(""), (None, None));
}
}