Skip to main content

grex_core/
add.rs

1//! Shared pack-registration helper used by `grex add` and import.
2
3use crate::manifest::{self, Event, PackId, SCHEMA_VERSION};
4use chrono::Utc;
5use std::path::Path;
6use thiserror::Error;
7
8/// Add request after CLI / MCP edge parsing has resolved defaults.
9#[non_exhaustive]
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AddRequest {
12    pub url: String,
13    pub path: String,
14    pub pack_type: String,
15}
16
17impl AddRequest {
18    pub fn new(
19        url: impl Into<String>,
20        path: impl Into<String>,
21        pack_type: impl Into<String>,
22    ) -> Self {
23        Self { url: url.into(), path: path.into(), pack_type: pack_type.into() }
24    }
25}
26
27/// Runtime options for add dispatch.
28#[non_exhaustive]
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub struct AddOpts {
31    pub dry_run: bool,
32}
33
34impl AddOpts {
35    pub fn new(dry_run: bool) -> Self {
36        Self { dry_run }
37    }
38}
39
40/// Result of an add dispatch.
41#[non_exhaustive]
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct AddReport {
44    pub id: PackId,
45    pub url: String,
46    pub path: String,
47    pub pack_type: String,
48    pub dry_run: bool,
49    pub appended: bool,
50}
51
52#[non_exhaustive]
53#[derive(Debug, Error)]
54pub enum AddError {
55    #[error("manifest write failed: {0}")]
56    Manifest(#[from] manifest::ManifestError),
57}
58
59/// Append the manifest event for a pack registration unless this is a dry-run.
60pub fn add_pack(
61    manifest_path: &Path,
62    request: AddRequest,
63    opts: AddOpts,
64) -> Result<AddReport, AddError> {
65    let id = PackId::from(request.path.clone());
66    if !opts.dry_run {
67        let ev = Event::Add {
68            ts: Utc::now(),
69            id: id.clone(),
70            url: request.url.clone(),
71            path: request.path.clone(),
72            pack_type: request.pack_type.clone(),
73            schema_version: SCHEMA_VERSION.to_string(),
74        };
75        manifest::append_event(manifest_path, &ev)?;
76    }
77
78    Ok(AddReport {
79        id,
80        url: request.url,
81        path: request.path,
82        pack_type: request.pack_type,
83        dry_run: opts.dry_run,
84        appended: !opts.dry_run,
85    })
86}
87
88/// Infer the default workspace-relative path from a repository URL.
89pub fn infer_path_from_url(url: &str) -> String {
90    let trimmed = url.trim_end_matches(['/', '\\']);
91    let tail = trimmed.rsplit(['/', '\\', ':']).next().unwrap_or(trimmed);
92    tail.strip_suffix(".git").unwrap_or(tail).to_string()
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn add_pack_appends_add_event() {
101        let dir = tempfile::tempdir().unwrap();
102        let manifest = dir.path().join(".grex/events.jsonl");
103        let report = add_pack(
104            &manifest,
105            AddRequest {
106                url: "https://example.com/repo.git".into(),
107                path: "repo".into(),
108                pack_type: "scripted".into(),
109            },
110            AddOpts { dry_run: false },
111        )
112        .unwrap();
113
114        assert!(report.appended);
115        assert_eq!(report.id, "repo");
116        let events = manifest::read_all(&manifest).unwrap();
117        assert_eq!(events.len(), 1);
118        match &events[0] {
119            Event::Add { id, url, path, pack_type, schema_version, .. } => {
120                assert_eq!(id, "repo");
121                assert_eq!(url, "https://example.com/repo.git");
122                assert_eq!(path, "repo");
123                assert_eq!(pack_type, "scripted");
124                assert_eq!(schema_version, SCHEMA_VERSION);
125            }
126            _ => panic!("expected add event"),
127        }
128    }
129
130    #[test]
131    fn add_pack_dry_run_does_not_write_manifest() {
132        let dir = tempfile::tempdir().unwrap();
133        let manifest = dir.path().join(".grex/events.jsonl");
134        let report = add_pack(
135            &manifest,
136            AddRequest { url: "".into(), path: "local".into(), pack_type: "declarative".into() },
137            AddOpts { dry_run: true },
138        )
139        .unwrap();
140
141        assert!(!report.appended);
142        assert!(!manifest.exists());
143    }
144
145    #[test]
146    fn infer_path_from_https_git_url() {
147        assert_eq!(infer_path_from_url("https://example.com/org/repo.git"), "repo");
148    }
149
150    #[test]
151    fn infer_path_from_scp_like_url() {
152        assert_eq!(infer_path_from_url("git@example.com:org/repo.git"), "repo");
153    }
154
155    #[test]
156    fn infer_path_from_trailing_slash() {
157        assert_eq!(infer_path_from_url("https://example.com/org/repo/"), "repo");
158    }
159
160    #[test]
161    fn infer_path_from_empty_url_is_empty() {
162        assert_eq!(infer_path_from_url(""), "");
163    }
164}