use std::fs;
use std::path::Path;
use anyhow::{Context, Result};
const RAW_BASE: &str = "https://raw.githubusercontent.com/urmzd/oag";
const API_BASE: &str = "https://api.github.com/repos/urmzd/oag/contents/packs";
pub const KNOWN_PACKS: &[&str] = &["node-client", "react-swr-client", "fastapi-server"];
pub fn parse_pack_specifier(specifier: &str) -> (&str, &str) {
match specifier.split_once('@') {
Some((id, git_ref)) if !git_ref.is_empty() => (id, git_ref),
_ => (specifier, "main"),
}
}
pub fn download_pack(pack_id: &str, git_ref: &str, target_dir: &Path) -> Result<()> {
fs::create_dir_all(target_dir)
.with_context(|| format!("failed to create {}", target_dir.display()))?;
let pack_toml_url = format!("{RAW_BASE}/{git_ref}/packs/{pack_id}/oag.pack.toml");
let pack_toml = fetch_text(&pack_toml_url).with_context(|| {
format!("failed to download oag.pack.toml for '{pack_id}' at ref '{git_ref}'")
})?;
fs::write(target_dir.join("oag.pack.toml"), &pack_toml)?;
let templates_api_url = format!("{API_BASE}/{pack_id}/templates?ref={git_ref}");
let listing_json = fetch_text(&templates_api_url)
.with_context(|| format!("failed to list templates for '{pack_id}' at ref '{git_ref}'"))?;
let entries: Vec<GitHubContent> = serde_json::from_str(&listing_json)
.with_context(|| "failed to parse GitHub API response")?;
let templates_dir = target_dir.join("templates");
fs::create_dir_all(&templates_dir)?;
for entry in &entries {
if entry.r#type != "file" {
continue;
}
let url = format!(
"{RAW_BASE}/{git_ref}/packs/{pack_id}/templates/{}",
entry.name
);
let content =
fetch_text(&url).with_context(|| format!("failed to download {}", entry.name))?;
fs::write(templates_dir.join(&entry.name), &content)?;
}
Ok(())
}
fn fetch_text(url: &str) -> Result<String> {
let mut response = ureq::get(url)
.header("User-Agent", "oag-cli")
.call()
.with_context(|| format!("HTTP request failed: {url}"))?;
let status = response.status();
if status.as_u16() == 404 {
anyhow::bail!("not found (404): {url}");
}
if status.as_u16() >= 400 {
anyhow::bail!("HTTP {status}: {url}");
}
response
.body_mut()
.read_to_string()
.with_context(|| format!("failed to read response body from {url}"))
}
#[derive(serde::Deserialize)]
struct GitHubContent {
name: String,
r#type: String,
#[allow(dead_code)]
download_url: Option<String>,
#[allow(dead_code)]
url: String,
}