| | |
| 289 | 289 | match cmd { |
| 290 | 290 | "setup" => setup(&args), |
| 291 | 291 | "login" => login(&args), |
| 292 | "join" => join(&args), |
| 293 | "watch" => watch(&args), |
| 292 | 294 | "init" => init(&args), |
| 293 | 295 | "checkout" => checkout(&args), |
| 294 | 296 | "status" | "st" => status(), |
| | |
| 322 | 324 | |
| 323 | 325 | const HELP: &str = "grove: where agents build software for people |
| 324 | 326 | |
| 327 | join <server> <username> [--agent NAME] create an account on a grove server |
| 325 | 328 | login <server> --token T [--person-token P] work against a grove server |
| 326 | 329 | init <name> [--owner GROUP] [--private] [-m M] make this folder a new project |
| 327 | 330 | checkout <owner/name> [DIR] get a project's files |
| | |
| 335 | 338 | attach [#N] FILE [--type T] [--title T] | --url U evidence for the latest revision |
| 336 | 339 | changes a project's changes |
| 337 | 340 | show [#N] one change: intent, revisions, discussion |
| 341 | watch [#N] [--follow] wait for someone else to add to a change |
| 338 | 342 | land #N land a change (in person) |
| 339 | 343 | decline #N decline a change (in person) |
| 340 | 344 | log main, newest first |
| | |
| 408 | 412 | Ok(()) |
| 409 | 413 | } |
| 410 | 414 | |
| 415 | fn join(args: &Args) -> Result<()> { |
| 416 | check_config_is_ours()?; |
| 417 | let (server, username) = match (args.pos(0), args.pos(1)) { |
| 418 | (Some(s), Some(u)) => (s, u), |
| 419 | _ => return e!("usage: grove join <server> <username> [--agent NAME]"), |
| 420 | }; |
| 421 | let mut body = vec![("username", Value::from(username))]; |
| 422 | if let Some(d) = args.value(&["--display"]) { |
| 423 | body.push(("display_name", d.into())); |
| 424 | } |
| 425 | if let Some(a) = args.value(&["--agent"]) { |
| 426 | body.push(("agent", a.into())); |
| 427 | } |
| 428 | let r = crate::client::Client::new(server, None).post("/api/people", &Value::Object(body.into_iter().map(|(k, v)| (k.to_string(), v)).collect()))?; |
| 429 | let get = |k: &str| r.get(k).and_then(Value::as_str).map(str::to_string); |
| 430 | let person_token = get("person_token").ok_or("the server sent no token")?; |
| 431 | let agent_token = get("agent_token"); |
| 432 | write_config(vec![ |
| 433 | ("server", server.into()), |
| 434 | ("token", agent_token.clone().unwrap_or_else(|| person_token.clone()).into()), |
| 435 | ("person_token", person_token.into()), |
| 436 | ("person", username.into()), |
| 437 | ("agent", get("agent").into()), |
| 438 | ])?; |
| 439 | match args.value(&["--agent"]) { |
| 440 | Some(a) => println!("joined {server} as {username}; {a} acts for you"), |
| 441 | None => println!("joined {server} as {username}"), |
| 442 | } |
| 443 | Ok(()) |
| 444 | } |
| 445 | |
| 411 | 446 | fn init(args: &Args) -> Result<()> { |
| 412 | 447 | let cfg = Config::load()?; |
| 413 | 448 | let name = args.pos(0).ok_or("usage: grove init <name>")?; |
| | |
| 683 | 718 | Ok(()) |
| 684 | 719 | } |
| 685 | 720 | |
| 721 | /// One line (or a few) for an event in a change's timeline. |
| 722 | fn describe(c: &Change, ev: &Event) -> String { |
| 723 | match ev { |
| 724 | Event::Opened => "opened".to_string(), |
| 725 | Event::Revised(n) => { |
| 726 | let r = &c.revisions[*n as usize - 1]; |
| 727 | let mut l = format!("r{n} on @{}: {}", r.base, plural(r.steps.len(), "step")); |
| 728 | for st in &r.steps { |
| 729 | l += &format!("\n {} {}", short(&st.snapshot), st.message); |
| 730 | } |
| 731 | if let Some(note) = &r.note { |
| 732 | l += &format!("\n note: {note}"); |
| 733 | } |
| 734 | l |
| 735 | } |
| 736 | Event::State(s) => format!("→ {}", s.name()), |
| 737 | Event::Said(id) => format!("said: {}", c.messages[*id as usize - 1].body), |
| 738 | Event::Attached(i) => { |
| 739 | let ev = &c.evidence[*i]; |
| 740 | format!("evidence on r{}: {} ({})", ev.revision, ev.title, ev.url.clone().unwrap_or_else(|| ev.media_type.clone())) |
| 741 | } |
| 742 | Event::Landed { revision, points } => format!("landed r{revision} as @{}..@{}", points.0, points.1), |
| 743 | } |
| 744 | } |
| 745 | |
| 686 | 746 | fn show(args: &Args) -> Result<()> { |
| 687 | 747 | let cfg = Config::load()?; |
| 688 | 748 | let local = Local::find()?; |
| | |
| 693 | 753 | println!("#{} {} [{}]", c.number, c.intent, c.state.name()); |
| 694 | 754 | println!("proposed by {}, {}", who(&*b, &c.proposer), ago(c.at)); |
| 695 | 755 | for (at, by, ev) in &c.timeline { |
| 696 | | let line = match ev { |
| 697 | | Event::Opened => "opened".to_string(), |
| 698 | | Event::Revised(n) => { |
| 699 | | let r = &c.revisions[*n as usize - 1]; |
| 700 | | let mut l = format!("r{n} on @{}: {}", r.base, plural(r.steps.len(), "step")); |
| 701 | | for st in &r.steps { |
| 702 | | l += &format!("\n {} {}", short(&st.snapshot), st.message); |
| 703 | | } |
| 704 | | if let Some(note) = &r.note { |
| 705 | | l += &format!("\n note: {note}"); |
| 706 | | } |
| 707 | | l |
| 708 | | } |
| 709 | | Event::State(s) => format!("→ {}", s.name()), |
| 710 | | Event::Said(id) => format!("said: {}", c.messages[*id as usize - 1].body), |
| 711 | | Event::Attached(i) => { |
| 712 | | let ev = &c.evidence[*i]; |
| 713 | | format!("evidence on r{}: {} ({})", ev.revision, ev.title, ev.url.clone().unwrap_or_else(|| ev.media_type.clone())) |
| 714 | | } |
| 715 | | Event::Landed { revision, points } => format!("landed r{revision} as @{}..@{}", points.0, points.1), |
| 716 | | }; |
| 717 | | println!(" {:<10} {}: {}", ago(*at), who(&*b, by), line); |
| 756 | println!(" {:<10} {}: {}", ago(*at), who(&*b, by), describe(c, ev)); |
| 718 | 757 | } |
| 719 | 758 | if let (Some(r), true) = (c.latest(), c.state.is_open()) { |
| 720 | 759 | let base = p.point(r.base).map(|pt| pt.snapshot).ok_or("base missing")?; |
| | |
| 725 | 764 | print_diff(&*b, &base, &last, true)?; |
| 726 | 765 | } |
| 727 | 766 | Ok(()) |
| 767 | } |
| 768 | |
| 769 | /// Wait for activity on a change from someone other than us, print it and stop. With |
| 770 | /// --follow, keep printing until the change lands or closes. |
| 771 | fn watch(args: &Args) -> Result<()> { |
| 772 | let cfg = Config::load()?; |
| 773 | let local = Local::find()?; |
| 774 | let number = change_number(args.pos(0), Some(&local))?; |
| 775 | let follow = args.has("--follow"); |
| 776 | let me = cfg.actor(); |
| 777 | let mut b = connect(&cfg)?; |
| 778 | let mut seen = local.project(&mut *b)?.change(number).ok_or_else(|| format!("no change #{number}"))?.timeline.len(); |
| 779 | loop { |
| 780 | std::thread::sleep(std::time::Duration::from_secs(3)); |
| 781 | let p = local.project(&mut *b)?; |
| 782 | let c = p.change(number).ok_or_else(|| format!("no change #{number}"))?; |
| 783 | let mut from_others = false; |
| 784 | for (at, by, ev) in &c.timeline[seen..] { |
| 785 | println!(" {:<10} {}: {}", ago(*at), who(&*b, by), describe(c, ev)); |
| 786 | from_others |= *by != me; |
| 787 | } |
| 788 | seen = c.timeline.len(); |
| 789 | if !c.state.is_open() || (from_others && !follow) { |
| 790 | return Ok(()); |
| 791 | } |
| 792 | } |
| 728 | 793 | } |
| 729 | 794 | |
| 730 | 795 | fn land(args: &Args) -> Result<()> { |