omne_cli/commands/
upgrade.rs1#![allow(dead_code)]
10
11use std::path::Path;
12
13use clap::{Args as ClapArgs, ValueEnum};
14
15use crate::error::CliError;
16use crate::fetch;
17use crate::github::GithubClient;
18use crate::manifest;
19use crate::volume;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
23pub enum Target {
24 Kernel,
25 Distro,
26}
27
28#[derive(Debug, ClapArgs)]
30pub struct Args {
31 #[arg(value_enum, default_value_t = Target::Kernel)]
33 pub target: Target,
34}
35
36pub fn run(args: &Args) -> Result<(), CliError> {
37 let cwd = std::env::current_dir()
38 .map_err(|e| CliError::Io(format!("cannot determine current directory: {e}")))?;
39 let root = volume::find_omne_root(&cwd).ok_or(CliError::NotAVolume)?;
40 let github = GithubClient::from_env("https://api.github.com", "omne-cli");
41 upgrade_with_client(args.target, &root, &github)
42}
43
44pub fn upgrade_with_client(
46 target: Target,
47 root: &Path,
48 github: &GithubClient,
49) -> Result<(), CliError> {
50 let omne = root.join(".omne");
51
52 let readme_path = omne.join("omne.md");
54 let readme_content = std::fs::read_to_string(&readme_path).map_err(|e| {
55 CliError::Io(format!(
56 "cannot read {}: {e} — is this an omne volume?",
57 readme_path.display()
58 ))
59 })?;
60 let frontmatter = manifest::parse_frontmatter(&readme_content)?;
61
62 let (org, repo, target_dir, label, expected_top_level) = match target {
64 Target::Kernel => {
65 let (org, repo) = parse_source(&frontmatter.kernel_source)?;
66 let target_dir = omne.join("core");
67 (org, repo, target_dir, "kernel".to_string(), "core")
68 }
69 Target::Distro => {
70 let (org, repo) = parse_source(&frontmatter.distro_source)?;
71 let target_dir = omne.join("dist");
72 (
73 org,
74 repo.clone(),
75 target_dir,
76 format!("distro ({repo})"),
77 "dist",
78 )
79 }
80 };
81
82 if target_dir.exists() {
84 verify_no_symlinks(&target_dir, &omne)?;
85 }
86
87 let tag = github.latest_release_tag(&org, &repo)?;
89 eprintln!("Upgrading {label} to {tag}...");
90
91 if target_dir.exists() {
94 verify_no_symlinks(&target_dir, &omne)?;
95 std::fs::remove_dir_all(&target_dir)?;
96 }
97
98 fetch::download_and_extract(github, &org, &repo, &tag, &omne, expected_top_level)?;
100
101 eprintln!("\x1b[32m✓\x1b[0m Upgrade complete ({label}).");
102 Ok(())
103}
104
105fn parse_source(source: &str) -> Result<(String, String), CliError> {
107 let (org, repo) =
108 source
109 .split_once('/')
110 .ok_or_else(|| crate::manifest::Error::InvalidSourceFormat {
111 value: source.to_string(),
112 })?;
113 Ok((org.to_string(), repo.to_string()))
114}
115
116fn verify_no_symlinks(target_dir: &Path, boundary: &Path) -> Result<(), CliError> {
120 if target_dir.symlink_metadata()?.file_type().is_symlink() {
122 return Err(CliError::UnsafeTarget {
123 path: target_dir.to_path_buf(),
124 });
125 }
126
127 let mut current = target_dir.to_path_buf();
129 while let Some(parent) = current.parent() {
130 if parent.symlink_metadata()?.file_type().is_symlink() {
131 return Err(CliError::UnsafeTarget {
132 path: parent.to_path_buf(),
133 });
134 }
135 if parent == boundary {
136 break;
137 }
138 current = parent.to_path_buf();
139 }
140
141 Ok(())
142}