ref:0430ea2cdebfb93074aa52a637b5dbd2b27d00f2

feat(cli): --json flag, pr diff/checkout, commit view, milestone create

CLI gap audit follow-ups (closes #10, #11, #12, #13, #17). All work is CLI-only — no new server endpoints required for any of these. * **--json** (#10): top-level global flag. When passed, every list/view command short-circuits the table renderer and pretty-prints the raw server JSON. Implemented as a process-global atomic so the flag propagates without threading through every fn signature. Wired into the most-used surfaces: pr list/view, issue list/view, epic list/view, ci list/view, milestone list/view, repo view, branch list, commit list. Other list/view commands (label, ssh-key, requirement, release, deploy, agent, board, runner) remain table-only — follow-up. * **anvil pr diff <num>** (#11): resolves the PR via existing `GET /pulls/:n` to get base/head branches, runs `git fetch origin base head`, then `git diff origin/base...origin/head`. --name-only flag for file list. --remote to override. Pure local git wrapper. * **anvil pr checkout <num>** (#12): resolves PR head_branch, refuses to clobber uncommitted changes (mirrors `gh pr checkout`), `git fetch` + `git checkout -B <head> origin/<head>`. Pure local git wrapper. * **anvil commit view <sha>** (#17): wraps `git show` (with --stat --no-patch by default; --diff to include patch). Pure local git. * **anvil milestone create** (#13): server already has `POST /milestones`, just adds the CLI verb. The other CRUD verbs (edit/close/reopen/delete) need server endpoints and are blocked. The duplicate `anvil issue milestones` and `anvil issue create-milestone` now emit deprecation warnings and delegate to the milestone command. Tests: * src/output.rs: roundtrip tests for set_json_mode/is_json + smoke test for print_json. * src/commands/pr.rs: clap parsing for `pr diff` and `pr checkout`. * src/commands/commit.rs: clap parsing for `commit view`. * src/commands/milestone.rs: clap parsing for `milestone create`. 39/39 cargo tests pass. cargo clippy --all-targets -D warnings clean. cargo fmt clean. Verified live against production: - `anvil pr list fangorn/anvil-cli --json` returns valid JSON - `anvil pr view 13 --repo fangorn/anvil-cli --json` returns valid JSON - `anvil milestone --help` shows new `create` verb - `anvil pr --help` shows new `diff` and `checkout` verbs - `anvil commit --help` shows new `view` verb Blocked on server work (filed separately): #14 anvil pr rebase — needs POST /pulls/:n/rebase #15 anvil branch ... — needs branch CRUD + protection endpoints #16 anvil status/inbox — needs aggregation endpoint Closes #10, #11, #12, #13, #17
SHA: 0430ea2cdebfb93074aa52a637b5dbd2b27d00f2
Author: CI <ci@anvil.test>
Date: 2026-05-09 03:52
Parents: 337a754
10 files changed +511 -56
Type
src/output.rs +64 −0
@@ -1,5 +1,32 @@
use colored::Colorize;
use std::sync::atomic::{AtomicBool, Ordering};
/// Output format: human-readable (default) vs. JSON for scripting.
/// Set once at program start by main.rs based on the top-level --json flag.
static JSON_MODE: AtomicBool = AtomicBool::new(false);
/// Switch all subsequent renderers into JSON mode. Idempotent.
pub fn set_json_mode(enabled: bool) {
JSON_MODE.store(enabled, Ordering::Relaxed);
}
/// Whether the CLI was invoked with --json. List/view subcommands check
/// this and emit raw JSON instead of a table.
pub fn is_json() -> bool {
JSON_MODE.load(Ordering::Relaxed)
}
/// Pretty-print a serde_json::Value to stdout. Used by list/view
/// subcommands when `--json` is in effect.
pub fn print_json(value: &serde_json::Value) {
match serde_json::to_string_pretty(value) {
Ok(s) => println!("{s}"),
// to_string_pretty only fails on non-string map keys; serde_json::Value
// can't construct those, so this is unreachable in practice.
Err(_) => println!("{value}"),
}
}
/// Print a key-value detail line.
pub fn detail(key: &str, value: &str) {
println!("{:>14} {}", key.bold(), value);
@@ -87,5 +114,42 @@
})
.collect();
println!(" {}", line.join(" "));
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Note: JSON_MODE is process-global, so these tests run serially via
/// `set_json_mode(false)` cleanup in each test. They share the same
/// process under `cargo test`, so explicit reset prevents flakes.
#[test]
fn json_mode_off_by_default() {
// Reset first in case another test left it on.
set_json_mode(false);
assert!(!is_json());
}
#[test]
fn set_json_mode_round_trip() {
set_json_mode(true);
assert!(is_json());
set_json_mode(false);
assert!(!is_json());
}
#[test]
fn print_json_pretty_prints() {
// We can't easily capture stdout from a library test without
// restructuring print_json to take a Writer. Instead, smoke-test
// that it doesn't panic on common shapes — the actual output
// format is verified end-to-end via the CLI smoke tests.
let v = serde_json::json!({"foo": "bar", "n": 42, "arr": [1, 2, 3]});
print_json(&v);
let v_array = serde_json::json!([1, 2, 3]);
print_json(&v_array);
let v_null = serde_json::Value::Null;
print_json(&v_null);
}
}