omne_cli/commands/
init.rs1#![allow(dead_code)]
14
15use std::fs;
16use std::path::Path;
17
18use clap::Args as ClapArgs;
19
20use crate::claude_skills;
21use crate::defaults;
22use crate::distro;
23use crate::error::CliError;
24use crate::fetch;
25use crate::github::GithubClient;
26use crate::manifest;
27use crate::scaffold;
28use crate::tarball;
29
30#[derive(Debug, ClapArgs)]
32pub struct Args {
33 #[arg(long_help = "Distro specifier.\n\
35\n\
36Accepted forms:\n\
37 bare name: omne-nosce\n\
38 org/repo: omne-org/omne-nosce\n\
39 HTTPS URL: https://github.com/omne-org/omne-nosce.git\n\
40 SSH URL: git@github.com:omne-org/omne-nosce.git\n\
41\n\
42file:// URLs and non-github.com hosts are rejected.")]
43 pub distro: String,
44}
45
46pub fn run(args: &Args) -> Result<(), CliError> {
47 let root = std::env::current_dir()
48 .map_err(|e| CliError::Io(format!("cannot determine current directory: {e}")))?;
49 let github = GithubClient::from_env("https://api.github.com", "omne-cli");
50 init_with_client(&args.distro, &root, &github)
51}
52
53pub fn init_with_client(
55 distro_spec: &str,
56 root: &Path,
57 github: &GithubClient,
58) -> Result<(), CliError> {
59 let spec = distro::parse(distro_spec)?;
60
61 let omne = root.join(".omne");
62 if omne.exists() {
63 return Err(CliError::VolumeAlreadyExists { path: omne });
64 }
65
66 claude_skills::preflight()?;
69
70 scaffold::create_volume_dirs(root)?;
71 scaffold::write_docs_baseline(root)?;
72 scaffold::write_gitignore(root)?;
73
74 let (kernel_org, kernel_repo) = parse_source(defaults::DEFAULT_KERNEL_SOURCE);
76 let kernel_tag = github.latest_release_tag(kernel_org, kernel_repo)?;
77 fetch::download_and_extract(github, kernel_org, kernel_repo, &kernel_tag, &omne, "core")?;
78
79 let distro_tag = github.latest_release_tag(&spec.org, &spec.repo)?;
81 fetch::download_and_extract(github, &spec.org, &spec.repo, &distro_tag, &omne, "dist")?;
82
83 stamp_and_finalize(root, &omne, &spec)
85}
86
87pub fn init_with_tarballs(
89 distro_spec: &str,
90 root: &Path,
91 kernel_tarball: &Path,
92 distro_tarball: &Path,
93) -> Result<(), CliError> {
94 let spec = distro::parse(distro_spec)?;
95
96 let omne = root.join(".omne");
97 if omne.exists() {
98 return Err(CliError::VolumeAlreadyExists { path: omne });
99 }
100
101 claude_skills::preflight()?;
102
103 scaffold::create_volume_dirs(root)?;
104 scaffold::write_docs_baseline(root)?;
105 scaffold::write_gitignore(root)?;
106
107 let kernel_file = fs::File::open(kernel_tarball)?;
108 tarball::extract_safe(kernel_file, &omne)?;
109 verify_top_level(&omne, "core")?;
110
111 let distro_file = fs::File::open(distro_tarball)?;
112 tarball::extract_safe(distro_file, &omne)?;
113 verify_top_level(&omne, "dist")?;
114
115 stamp_and_finalize(root, &omne, &spec)
116}
117
118fn stamp_and_finalize(root: &Path, omne: &Path, spec: &distro::DistroSpec) -> Result<(), CliError> {
121 let (distro_name, distro_version) = read_distro_metadata(&omne.join("dist"));
122 let volume_name = root
123 .file_name()
124 .map(|n| n.to_string_lossy().into_owned())
125 .unwrap_or_else(|| "unknown".to_string());
126
127 let today = chrono_today();
128 let vars = manifest::Vars {
129 volume: volume_name,
130 distro: distro_name.clone(),
131 distro_version,
132 created: today,
133 kernel_source: defaults::DEFAULT_KERNEL_SOURCE.to_string(),
134 distro_source: format!("{}/{}", spec.org, spec.repo),
135 };
136 let stamped = manifest::stamp(&vars);
137 scaffold::write_omne_readme(root, &stamped)?;
138 scaffold::write_bootloader(root)?;
139
140 claude_skills::link_skills(root)?;
144
145 eprintln!(
146 "\x1b[32m✓\x1b[0m Initialized omne volume '{}' with distro '{}'",
147 vars.volume, distro_name
148 );
149
150 Ok(())
151}
152
153fn parse_source(source: &str) -> (&str, &str) {
155 let (org, repo) = source.split_once('/').expect("source must be org/repo");
156 (org, repo)
157}
158
159fn read_distro_metadata(dist_dir: &Path) -> (String, String) {
162 let manifest_path = dist_dir.join("manifest.json");
163 if manifest_path.is_file() {
164 if let Ok(content) = fs::read_to_string(&manifest_path) {
165 if let Ok(data) = serde_json::from_str::<serde_json::Value>(&content) {
166 let name = data
167 .get("name")
168 .and_then(|v| v.as_str())
169 .unwrap_or("unknown")
170 .to_string();
171 let version = data
172 .get("version")
173 .and_then(|v| v.as_str())
174 .unwrap_or("0.0.0")
175 .to_string();
176 return (name, version);
177 }
178 }
179 }
180 ("unknown".to_string(), "0.0.0".to_string())
181}
182
183fn verify_top_level(target: &Path, expected: &str) -> Result<(), CliError> {
187 if !target.join(expected).is_dir() {
188 let found: Vec<String> = fs::read_dir(target)
189 .ok()
190 .map(|entries| {
191 entries
192 .filter_map(|e| e.ok())
193 .map(|e| e.file_name().to_string_lossy().into_owned())
194 .collect()
195 })
196 .unwrap_or_default();
197 return Err(CliError::TarballLayoutMismatch {
198 expected: expected.to_string(),
199 found,
200 });
201 }
202 Ok(())
203}
204
205fn chrono_today() -> String {
207 crate::clock::now_utc().format_date()
208}