yosh_plugin_manager/
lib.rs1pub 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 Sync {
32 #[arg(long)]
34 prune: bool,
35 },
36 Update {
38 name: Option<String>,
40 },
41 List,
43 Verify,
45 Install {
47 source: String,
49 #[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}