Skip to main content

yosh_plugin_manager/
lib.rs

1pub mod config;
2pub mod github;
3pub mod install;
4pub mod lockfile;
5pub mod resolve;
6pub mod sync;
7pub mod verify;
8
9use clap::{Parser, Subcommand};
10
11const VERSION: &str = concat!(
12    env!("CARGO_PKG_VERSION"),
13    " (",
14    env!("YOSH_GIT_HASH"),
15    " ",
16    env!("YOSH_BUILD_DATE"),
17    ")"
18);
19
20#[derive(Parser)]
21#[command(name = "yosh-plugin", about = "Manage yosh shell plugins")]
22#[command(version = VERSION)]
23struct Cli {
24    #[command(subcommand)]
25    command: Commands,
26}
27
28#[derive(Subcommand)]
29enum Commands {
30    /// Install plugins from plugins.toml
31    Sync {
32        /// Remove plugins not in plugins.toml
33        #[arg(long)]
34        prune: bool,
35    },
36    /// Update installed plugins to latest version
37    Update {
38        /// Only update the named plugin
39        name: Option<String>,
40    },
41    /// List installed plugins
42    List,
43    /// Verify plugin integrity (SHA-256)
44    Verify,
45    /// Add a plugin from a GitHub URL or local path to plugins.toml
46    Install {
47        /// GitHub URL (https://github.com/owner/repo[@version]) or local file path
48        source: String,
49        /// Overwrite existing plugin with the same name
50        #[arg(long)]
51        force: bool,
52    },
53}
54
55pub fn run() -> i32 {
56    let cli = Cli::parse();
57    match cli.command {
58        Commands::Sync { prune } => cmd_sync(prune),
59        Commands::Update { name } => cmd_update(name.as_deref()),
60        Commands::List => cmd_list(),
61        Commands::Verify => cmd_verify(),
62        Commands::Install { source, force } => cmd_install(&source, force),
63    }
64}
65
66fn cmd_install(source: &str, force: bool) -> i32 {
67    let config_path = sync::config_path();
68    match install::install(source, force, &config_path, None) {
69        Ok(msg) => {
70            eprintln!("{}", msg);
71            if source.starts_with("https://github.com/") {
72                eprintln!("Run 'yosh plugin sync' to download.");
73            }
74            0
75        }
76        Err(e) => {
77            eprintln!("yosh-plugin: {}", e);
78            1
79        }
80    }
81}
82
83fn cmd_sync(prune: bool) -> i32 {
84    let result = match sync::sync(prune) {
85        Ok(r) => r,
86        Err(e) => {
87            eprintln!("yosh-plugin: {}", e);
88            return 2;
89        }
90    };
91
92    for name in &result.succeeded {
93        eprintln!("  \u{2713} {}", name);
94    }
95    for (name, err) in &result.failed {
96        eprintln!("  \u{2717} {}: {}", name, err);
97    }
98
99    if result.failed.is_empty() {
100        eprintln!(
101            "yosh-plugin: sync complete ({} plugins)",
102            result.succeeded.len()
103        );
104        0
105    } else {
106        eprintln!(
107            "yosh-plugin: sync partial ({} succeeded, {} failed)",
108            result.succeeded.len(),
109            result.failed.len()
110        );
111        1
112    }
113}
114
115fn cmd_update(name_filter: Option<&str>) -> i32 {
116    let config_path = sync::config_path();
117    let decls = match config::load_config(&config_path) {
118        Ok(d) => d,
119        Err(e) => {
120            eprintln!("yosh-plugin: {}", e);
121            return 2;
122        }
123    };
124
125    let client = github::GitHubClient::new();
126
127    let content = match std::fs::read_to_string(&config_path) {
128        Ok(c) => c,
129        Err(e) => {
130            eprintln!("yosh-plugin: {}: {}", config_path.display(), e);
131            return 2;
132        }
133    };
134    let mut new_content = content.clone();
135    let mut updated = false;
136
137    for decl in &decls {
138        if name_filter.is_some_and(|f| decl.name != f) {
139            continue;
140        }
141        if let config::PluginSource::GitHub { owner, repo } = &decl.source {
142            match client.latest_version(owner, repo) {
143                Ok(latest) => {
144                    let current = decl.version.as_deref().unwrap_or("");
145                    if latest != current {
146                        eprintln!("  {} {} \u{2192} {}", decl.name, current, latest);
147                        if !current.is_empty() {
148                            new_content = new_content.replacen(
149                                &format!("version = \"{}\"", current),
150                                &format!("version = \"{}\"", latest),
151                                1,
152                            );
153                        }
154                        updated = true;
155                    } else {
156                        eprintln!("  {} {} (already latest)", decl.name, current);
157                    }
158                }
159                Err(e) => {
160                    eprintln!("  \u{2717} {}: {}", decl.name, e);
161                }
162            }
163        }
164    }
165
166    if updated {
167        if let Err(e) = std::fs::write(&config_path, &new_content) {
168            eprintln!("yosh-plugin: write {}: {}", config_path.display(), e);
169            return 2;
170        }
171        return cmd_sync(false);
172    }
173
174    0
175}
176
177fn cmd_list() -> i32 {
178    let lock_path = sync::lock_path();
179    let lockfile = match lockfile::load_lockfile(&lock_path) {
180        Ok(l) => l,
181        Err(e) => {
182            eprintln!("yosh-plugin: {}", e);
183            return 2;
184        }
185    };
186
187    if lockfile.plugin.is_empty() {
188        eprintln!("no plugins installed (run 'yosh-plugin sync' first)");
189        return 0;
190    }
191
192    for entry in &lockfile.plugin {
193        let version = entry.version.as_deref().unwrap_or("-");
194        let verified =
195            match verify::verify_checksum(&config::expand_tilde_path(&entry.path), &entry.sha256) {
196                Ok(true) => "\u{2713} verified",
197                Ok(false) => "\u{2717} checksum mismatch",
198                Err(_) => "\u{2717} file missing",
199            };
200        println!(
201            "{:<16} {:<8} {:<48} {}",
202            entry.name, version, entry.source, verified
203        );
204    }
205
206    0
207}
208
209fn cmd_verify() -> i32 {
210    let lock_path = sync::lock_path();
211    let lockfile = match lockfile::load_lockfile(&lock_path) {
212        Ok(l) => l,
213        Err(e) => {
214            eprintln!("yosh-plugin: {}", e);
215            return 2;
216        }
217    };
218
219    let mut all_ok = true;
220    for entry in &lockfile.plugin {
221        let path = config::expand_tilde_path(&entry.path);
222        match verify::verify_checksum(&path, &entry.sha256) {
223            Ok(true) => {
224                eprintln!("  \u{2713} {}", entry.name);
225            }
226            Ok(false) => {
227                eprintln!("  \u{2717} {}: checksum mismatch", entry.name);
228                all_ok = false;
229            }
230            Err(e) => {
231                eprintln!("  \u{2717} {}: {}", entry.name, e);
232                all_ok = false;
233            }
234        }
235    }
236
237    if all_ok { 0 } else { 1 }
238}