yosh_plugin_manager/
lib.rs1pub mod config;
2pub mod github;
3pub mod install;
4pub mod lockfile;
5pub mod metadata_extract;
6pub mod precompile;
7pub mod resolve;
8pub mod sync;
9pub mod update;
10pub mod verify;
11
12pub mod generated {
27 wasmtime::component::bindgen!({
28 path: "wit",
29 world: "plugin-world",
30 async: false,
31 });
32}
33
34use clap::{Parser, Subcommand};
35
36const VERSION: &str = concat!(
37 env!("CARGO_PKG_VERSION"),
38 " (",
39 env!("YOSH_GIT_HASH"),
40 " ",
41 env!("YOSH_BUILD_DATE"),
42 ")"
43);
44
45#[derive(Parser)]
46#[command(name = "yosh-plugin", about = "Manage yosh shell plugins")]
47#[command(version = VERSION)]
48struct Cli {
49 #[command(subcommand)]
50 command: Commands,
51}
52
53#[derive(Subcommand)]
54enum Commands {
55 Sync {
57 #[arg(long)]
59 prune: bool,
60 },
61 Update {
63 name: Option<String>,
65 },
66 List,
68 Verify,
70 Install {
72 source: String,
74 #[arg(long)]
76 force: bool,
77 },
78}
79
80pub fn run() -> i32 {
81 let cli = Cli::parse();
82 match cli.command {
83 Commands::Sync { prune } => cmd_sync(prune),
84 Commands::Update { name } => cmd_update(name.as_deref()),
85 Commands::List => cmd_list(),
86 Commands::Verify => cmd_verify(),
87 Commands::Install { source, force } => cmd_install(&source, force),
88 }
89}
90
91fn cmd_install(source: &str, force: bool) -> i32 {
92 let config_path = sync::config_path();
93 match install::install(source, force, &config_path, None) {
94 Ok(msg) => {
95 eprintln!("{}", msg);
96 if source.starts_with("https://github.com/") {
97 eprintln!("Run 'yosh plugin sync' to download.");
98 }
99 0
100 }
101 Err(e) => {
102 eprintln!("yosh-plugin: {}", e);
103 1
104 }
105 }
106}
107
108fn cmd_sync(prune: bool) -> i32 {
109 let result = match sync::sync(prune) {
110 Ok(r) => r,
111 Err(e) => {
112 eprintln!("yosh-plugin: {}", e);
113 return 2;
114 }
115 };
116
117 for name in &result.succeeded {
118 eprintln!(" \u{2713} {}", name);
119 }
120 for (name, err) in &result.failed {
121 eprintln!(" \u{2717} {}: {}", name, err);
122 }
123
124 if result.failed.is_empty() {
125 eprintln!(
126 "yosh-plugin: sync complete ({} plugins)",
127 result.succeeded.len()
128 );
129 0
130 } else {
131 eprintln!(
132 "yosh-plugin: sync partial ({} succeeded, {} failed)",
133 result.succeeded.len(),
134 result.failed.len()
135 );
136 1
137 }
138}
139
140fn cmd_update(name_filter: Option<&str>) -> i32 {
141 let config_path = sync::config_path();
142 let client = github::GitHubClient::new();
143 let outcome = match update::update(&config_path, name_filter, &client) {
144 Ok(o) => o,
145 Err(e) => {
146 eprintln!("yosh-plugin: {}", e);
147 return 2;
148 }
149 };
150
151 for result in &outcome.results {
152 match &result.status {
153 update::UpdateStatus::Updated { from, to } => {
154 eprintln!(" {} {} \u{2192} {}", result.name, from, to);
155 }
156 update::UpdateStatus::AlreadyLatest { current } => {
157 eprintln!(" {} {} (already latest)", result.name, current);
158 }
159 update::UpdateStatus::Failed(e) => {
160 eprintln!(" \u{2717} {}: {}", result.name, e);
161 }
162 update::UpdateStatus::Skipped(_) => {
163 }
166 }
167 }
168
169 if outcome.any_updated {
170 return cmd_sync(false);
171 }
172
173 0
174}
175
176fn cmd_list() -> i32 {
177 let lock_path = sync::lock_path();
178 let lockfile = match lockfile::load_lockfile(&lock_path) {
179 Ok(l) => l,
180 Err(e) => {
181 eprintln!("yosh-plugin: {}", e);
182 return 2;
183 }
184 };
185
186 if lockfile.plugin.is_empty() {
187 eprintln!("no plugins installed (run 'yosh-plugin sync' first)");
188 return 0;
189 }
190
191 for entry in &lockfile.plugin {
192 let version = entry.version.as_deref().unwrap_or("-");
193 let verified =
194 match verify::verify_checksum(&config::expand_tilde_path(&entry.path), &entry.sha256) {
195 Ok(true) => "\u{2713} verified",
196 Ok(false) => "\u{2717} checksum mismatch",
197 Err(_) => "\u{2717} file missing",
198 };
199 let cached = match (&entry.cwasm_path, &entry.wasmtime_version) {
205 (Some(p), Some(wv))
206 if std::path::Path::new(&config::expand_tilde_path(p)).exists()
207 && wv == precompile::WASMTIME_VERSION =>
208 {
209 "\u{2713} cached"
210 }
211 _ => "\u{2717} stale",
212 };
213 let caps = entry
214 .required_capabilities
215 .as_ref()
216 .map(|v| {
217 if v.is_empty() {
218 "[- (no capabilities)]".to_string()
219 } else {
220 format!("[{}]", v.join(", "))
221 }
222 })
223 .unwrap_or_else(|| "[?]".into());
224 println!(
225 "{:<16} {:<8} {:<48} {} {} {}",
226 entry.name, version, entry.source, verified, cached, caps
227 );
228 }
229
230 0
231}
232
233fn cmd_verify() -> i32 {
234 let lock_path = sync::lock_path();
235 let lockfile = match lockfile::load_lockfile(&lock_path) {
236 Ok(l) => l,
237 Err(e) => {
238 eprintln!("yosh-plugin: {}", e);
239 return 2;
240 }
241 };
242
243 let mut all_ok = true;
244 for entry in &lockfile.plugin {
245 let path = config::expand_tilde_path(&entry.path);
246 match verify::verify_checksum(&path, &entry.sha256) {
247 Ok(true) => {
248 eprintln!(" \u{2713} {}", entry.name);
249 }
250 Ok(false) => {
251 eprintln!(" \u{2717} {}: checksum mismatch", entry.name);
252 all_ok = false;
253 }
254 Err(e) => {
255 eprintln!(" \u{2717} {}: {}", entry.name, e);
256 all_ok = false;
257 }
258 }
259 }
260
261 if all_ok { 0 } else { 1 }
262}