Skip to main content

grex_cli/cli/verbs/
add.rs

1use crate::cli::args::{AddArgs, GlobalFlags};
2use anyhow::{Context, Result};
3use grex_core::add::{add_pack, infer_path_from_url, AddOpts, AddReport, AddRequest};
4use grex_core::import::classify;
5use grex_core::manifest::{
6    append::read_all, ensure_event_log_migrated, find_workspace_root, fold::fold,
7};
8use grex_core::refspec::parse_ref;
9use tokio_util::sync::CancellationToken;
10
11pub fn run(args: AddArgs, global: &GlobalFlags, _cancel: &CancellationToken) -> Result<()> {
12    let path = args.path.unwrap_or_else(|| infer_path_from_url(&args.url));
13    let pack_type = classify(&args.url).as_str().to_string();
14    // v1.3.3 B10 — parse `--ref <git-ref>` if present.
15    let parsed_ref = match args.git_ref.as_deref() {
16        Some(token) => Some(parse_ref(token).with_context(|| format!("parse --ref `{token}`"))?),
17        None => None,
18    };
19    // Resolve the workspace root by walking up from cwd looking for a
20    // `.grex/` marker. Falls back to cwd when no marker is found, so a
21    // fresh workspace just uses the user's directory. This fixes the
22    // v1.x cwd-relative bug where running `grex add` from a subdir
23    // created a stray event log in the subdir instead of writing to
24    // the parent workspace's log.
25    let cwd = std::env::current_dir().context("resolve cwd for workspace root")?;
26    let workspace = find_workspace_root(&cwd);
27    let manifest = ensure_event_log_migrated(&workspace).context("migrate v1.x event log")?;
28
29    check_path_collision(&manifest, &path, global.json)?;
30
31    let mut request = AddRequest::new(args.url, path, pack_type);
32    if let Some(r) = parsed_ref {
33        request = request.with_ref(r);
34    }
35    let report =
36        add_pack(&manifest, request, AddOpts::new(global.dry_run)).context("grex add failed")?;
37
38    if global.json {
39        emit_json(&report)?;
40    } else {
41        emit_human(&report);
42    }
43    Ok(())
44}
45
46/// v1.4.0 B15 — refuse to register a pack whose path collides with an
47/// already-tracked entry. Hard exit 1 (no state mutation) on hit;
48/// operators must run `grex rm <path>` first to replace the entry.
49fn check_path_collision(manifest: &std::path::Path, path: &str, json: bool) -> Result<()> {
50    if !manifest.exists() {
51        return Ok(());
52    }
53    let events = read_all(manifest).context("read event log for collision check")?;
54    let state = fold(events);
55    let Some(existing) = state.values().find(|s| s.path == path) else {
56        return Ok(());
57    };
58    let msg = format!("path '{path}' already registered to {}", existing.url);
59    if json {
60        let doc = serde_json::json!({
61            "verb": "add",
62            "error": {
63                "kind": "path_collision",
64                "message": msg,
65                "path": path,
66                "existing_url": existing.url,
67            },
68        });
69        println!("{}", serde_json::to_string(&doc)?);
70    } else {
71        eprintln!("grex add: {msg}; not adding");
72    }
73    std::process::exit(1);
74}
75
76fn emit_human(report: &AddReport) {
77    let prefix = if report.dry_run { "DRY-RUN: would add" } else { "added" };
78    println!(
79        "{prefix} {path:<32} {kind:<12} {url}",
80        path = report.path,
81        kind = report.pack_type,
82        url = if report.url.is_empty() { "-" } else { &report.url },
83    );
84    // v1.3.3 B10 — surface the resolved `<refdir>` + FA action when
85    // the caller passed `--ref`.
86    if let (Some(dir), Some(action)) = (report.refdir.as_deref(), report.ref_action) {
87        println!("  ref: refdir={dir} action={action:?}");
88    }
89}
90
91fn emit_json(report: &AddReport) -> Result<()> {
92    let out = serde_json::json!({
93        "dry_run": report.dry_run,
94        "id": report.id,
95        "url": report.url,
96        "path": report.path,
97        "type": report.pack_type,
98        "appended": report.appended,
99        "refdir": report.refdir,
100        "ref_action": report.ref_action.map(|a| format!("{a:?}")),
101    });
102    println!("{}", serde_json::to_string(&out)?);
103    Ok(())
104}