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
72 let (kernel_org, kernel_repo) = parse_source(defaults::DEFAULT_KERNEL_SOURCE);
74 let kernel_tag = github.latest_release_tag(kernel_org, kernel_repo)?;
75 fetch::download_and_extract(github, kernel_org, kernel_repo, &kernel_tag, &omne, "core")?;
76
77 let distro_tag = github.latest_release_tag(&spec.org, &spec.repo)?;
79 fetch::download_and_extract(github, &spec.org, &spec.repo, &distro_tag, &omne, "image")?;
80
81 stamp_and_finalize(root, &omne, &spec)
83}
84
85pub fn init_with_tarballs(
87 distro_spec: &str,
88 root: &Path,
89 kernel_tarball: &Path,
90 distro_tarball: &Path,
91) -> Result<(), CliError> {
92 let spec = distro::parse(distro_spec)?;
93
94 let omne = root.join(".omne");
95 if omne.exists() {
96 return Err(CliError::VolumeAlreadyExists { path: omne });
97 }
98
99 claude_skills::preflight()?;
100
101 scaffold::create_volume_dirs(root)?;
102
103 let kernel_file = fs::File::open(kernel_tarball)?;
104 tarball::extract_safe(kernel_file, &omne)?;
105 verify_top_level(&omne, "core")?;
106
107 let distro_file = fs::File::open(distro_tarball)?;
108 tarball::extract_safe(distro_file, &omne)?;
109 verify_top_level(&omne, "image")?;
110
111 stamp_and_finalize(root, &omne, &spec)
112}
113
114fn stamp_and_finalize(root: &Path, omne: &Path, spec: &distro::DistroSpec) -> Result<(), CliError> {
117 let (distro_name, distro_version) = read_distro_metadata(&omne.join("image"));
118 let volume_name = root
119 .file_name()
120 .map(|n| n.to_string_lossy().into_owned())
121 .unwrap_or_else(|| "unknown".to_string());
122
123 let today = chrono_today();
124 let vars = manifest::Vars {
125 volume: volume_name,
126 distro: distro_name.clone(),
127 distro_version,
128 created: today,
129 kernel_source: defaults::DEFAULT_KERNEL_SOURCE.to_string(),
130 distro_source: format!("{}/{}", spec.org, spec.repo),
131 };
132 let stamped = manifest::stamp(&vars);
133 scaffold::write_manifest(root, &stamped)?;
134 scaffold::write_bootloader(root)?;
135
136 claude_skills::link_skills(root)?;
140
141 eprintln!(
142 "\x1b[32m✓\x1b[0m Initialized omne volume '{}' with distro '{}'",
143 vars.volume, distro_name
144 );
145
146 Ok(())
147}
148
149fn parse_source(source: &str) -> (&str, &str) {
151 let (org, repo) = source.split_once('/').expect("source must be org/repo");
152 (org, repo)
153}
154
155fn read_distro_metadata(image_dir: &Path) -> (String, String) {
158 let manifest_path = image_dir.join("manifest.json");
159 if manifest_path.is_file() {
160 if let Ok(content) = fs::read_to_string(&manifest_path) {
161 if let Ok(data) = serde_json::from_str::<serde_json::Value>(&content) {
162 let name = data
163 .get("name")
164 .and_then(|v| v.as_str())
165 .unwrap_or("unknown")
166 .to_string();
167 let version = data
168 .get("version")
169 .and_then(|v| v.as_str())
170 .unwrap_or("0.0.0")
171 .to_string();
172 return (name, version);
173 }
174 }
175 }
176 ("unknown".to_string(), "0.0.0".to_string())
177}
178
179fn verify_top_level(target: &Path, expected: &str) -> Result<(), CliError> {
183 if !target.join(expected).is_dir() {
184 let found: Vec<String> = fs::read_dir(target)
185 .ok()
186 .map(|entries| {
187 entries
188 .filter_map(|e| e.ok())
189 .map(|e| e.file_name().to_string_lossy().into_owned())
190 .collect()
191 })
192 .unwrap_or_default();
193 return Err(CliError::TarballLayoutMismatch {
194 expected: expected.to_string(),
195 found,
196 });
197 }
198 Ok(())
199}
200
201fn chrono_today() -> String {
203 let now = std::time::SystemTime::now();
204 let duration = now
205 .duration_since(std::time::UNIX_EPOCH)
206 .unwrap_or_default();
207 let secs = duration.as_secs();
208 let days = secs / 86400;
209 let z = days as i64 + 719468;
211 let era = if z >= 0 { z } else { z - 146096 } / 146097;
212 let doe = (z - era * 146097) as u64;
213 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
214 let y = (yoe as i64) + era * 400;
215 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
216 let mp = (5 * doy + 2) / 153;
217 let d = doy - (153 * mp + 2) / 5 + 1;
218 let m = if mp < 10 { mp + 3 } else { mp - 9 };
219 let y = if m <= 2 { y + 1 } else { y };
220 format!("{y:04}-{m:02}-{d:02}")
221}