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_layers(root)?;
145
146 eprintln!(
147 "\x1b[32m✓\x1b[0m Initialized omne volume '{}' with distro '{}'",
148 vars.volume, distro_name
149 );
150
151 Ok(())
152}
153
154fn parse_source(source: &str) -> (&str, &str) {
156 let (org, repo) = source.split_once('/').expect("source must be org/repo");
157 (org, repo)
158}
159
160fn read_distro_metadata(dist_dir: &Path) -> (String, String) {
163 let manifest_path = dist_dir.join("manifest.json");
164 if manifest_path.is_file() {
165 if let Ok(content) = fs::read_to_string(&manifest_path) {
166 if let Ok(data) = serde_json::from_str::<serde_json::Value>(&content) {
167 let name = data
168 .get("name")
169 .and_then(|v| v.as_str())
170 .unwrap_or("unknown")
171 .to_string();
172 let version = data
173 .get("version")
174 .and_then(|v| v.as_str())
175 .unwrap_or("0.0.0")
176 .to_string();
177 return (name, version);
178 }
179 }
180 }
181 ("unknown".to_string(), "0.0.0".to_string())
182}
183
184fn verify_top_level(target: &Path, expected: &str) -> Result<(), CliError> {
188 if !target.join(expected).is_dir() {
189 let found: Vec<String> = fs::read_dir(target)
190 .ok()
191 .map(|entries| {
192 entries
193 .filter_map(|e| e.ok())
194 .map(|e| e.file_name().to_string_lossy().into_owned())
195 .collect()
196 })
197 .unwrap_or_default();
198 return Err(CliError::TarballLayoutMismatch {
199 expected: expected.to_string(),
200 found,
201 });
202 }
203 Ok(())
204}
205
206fn chrono_today() -> String {
208 crate::clock::now_utc().format_date()
209}