pub mod config;
pub mod github;
pub mod install;
pub mod lockfile;
pub mod metadata_extract;
pub mod precompile;
pub mod resolve;
pub mod runner;
pub mod scenario;
pub mod sync;
pub mod test_host;
pub mod update;
pub mod verify;
pub mod generated {
wasmtime::component::bindgen!({
path: "wit",
world: "plugin-world",
async: false,
});
}
use clap::{Parser, Subcommand};
const VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
" (",
env!("YOSH_GIT_HASH"),
" ",
env!("YOSH_BUILD_DATE"),
")"
);
#[derive(Parser)]
#[command(name = "yosh-plugin", about = "Manage yosh shell plugins")]
#[command(version = VERSION)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
pub enum RunAction {
Exec { command: String, args: Vec<String> },
Hook {
#[command(subcommand)]
which: HookKind,
},
}
#[derive(Subcommand)]
pub enum HookKind {
PreExec {
command_line: String,
},
PostExec {
command_line: String,
exit_code: i32,
},
OnCd {
old: String,
new: String,
},
PrePrompt,
}
#[derive(Copy, Clone, clap::ValueEnum, Debug)]
pub enum OutputFormat {
Human,
Json,
}
fn parse_kv(s: &str) -> Result<(String, String), String> {
let (k, v) = s
.split_once('=')
.ok_or_else(|| format!("expected KEY=VALUE, got `{}`", s))?;
Ok((k.to_string(), v.to_string()))
}
#[derive(Subcommand)]
enum Commands {
Sync {
#[arg(long)]
prune: bool,
},
Update {
name: Option<String>,
},
List,
Verify,
Install {
source: String,
#[arg(long)]
force: bool,
},
Run {
wasm: std::path::PathBuf,
#[command(subcommand)]
action: RunAction,
#[arg(long, value_delimiter = ',')]
cap: Vec<String>,
#[arg(long = "var", value_parser = parse_kv)]
vars: Vec<(String, String)>,
#[arg(long = "export", value_parser = parse_kv)]
exports: Vec<(String, String)>,
#[arg(long, default_value = ".")]
cwd: std::path::PathBuf,
#[arg(long = "allow-exec")]
allow_exec: Vec<String>,
#[arg(long = "sandbox-root")]
sandbox_root: Option<std::path::PathBuf>,
#[arg(long, default_value_t = 5000)]
timeout: u64,
#[arg(long, value_enum, default_value_t = OutputFormat::Human)]
format: OutputFormat,
},
Test {
#[arg(default_value = "tests")]
path: std::path::PathBuf,
#[arg(long)]
filter: Option<String>,
#[arg(long, value_enum, default_value_t = OutputFormat::Human)]
format: OutputFormat,
},
}
pub fn run() -> i32 {
let cli = Cli::parse();
match cli.command {
Commands::Sync { prune } => cmd_sync(prune),
Commands::Update { name } => cmd_update(name.as_deref()),
Commands::List => cmd_list(),
Commands::Verify => cmd_verify(),
Commands::Install { source, force } => cmd_install(&source, force),
Commands::Run {
wasm,
action,
cap,
vars,
exports,
cwd,
allow_exec,
sandbox_root,
timeout,
format,
} => cmd_run(
wasm,
action,
cap,
vars,
exports,
cwd,
allow_exec,
sandbox_root,
timeout,
format,
),
Commands::Test {
path,
filter,
format,
} => cmd_test(path, filter, format),
}
}
fn cmd_test(path: std::path::PathBuf, filter: Option<String>, format: OutputFormat) -> i32 {
let reports = crate::scenario::run_dir(&path, filter.as_deref());
let all_passed = reports.iter().all(|r| r.passed());
match format {
OutputFormat::Human => print!("{}", crate::scenario::format_summary_human(&reports)),
OutputFormat::Json => print!("{}", crate::scenario::format_summary_json(&reports)),
}
if all_passed { 0 } else { 1 }
}
#[allow(clippy::too_many_arguments)]
fn cmd_run(
wasm: std::path::PathBuf,
action: RunAction,
cap: Vec<String>,
vars: Vec<(String, String)>,
exports: Vec<(String, String)>,
cwd: std::path::PathBuf,
allow_exec: Vec<String>,
sandbox_root: Option<std::path::PathBuf>,
timeout: u64,
format: OutputFormat,
) -> i32 {
use crate::runner::{
HookCall, format_human, format_json, invoke_exec, invoke_hook, load_plugin,
};
use crate::test_host::TestState;
use yosh_plugin_api::pattern::CommandPattern;
use yosh_plugin_api::{capabilities_to_bitflags, parse_capability};
let mut state = TestState::default();
let parsed_caps: Vec<_> = cap.iter().filter_map(|s| parse_capability(s)).collect();
state.caps = if cap.is_empty() {
let bytes = match std::fs::read(&wasm) {
Ok(b) => b,
Err(e) => {
eprintln!("yosh-plugin: read {}: {}", wasm.display(), e);
return 99;
}
};
let engine = match crate::precompile::make_engine() {
Ok(e) => e,
Err(e) => {
eprintln!("yosh-plugin: engine: {}", e);
return 99;
}
};
match crate::metadata_extract::extract(&engine, &bytes) {
Ok(m) => {
let caps: Vec<_> = m
.required_capabilities
.iter()
.filter_map(|s| parse_capability(s))
.collect();
capabilities_to_bitflags(&caps)
}
Err(e) => {
eprintln!("yosh-plugin: metadata: {}", e);
return 99;
}
}
} else {
capabilities_to_bitflags(&parsed_caps)
};
for (k, v) in vars {
state.vars.insert(k, v);
}
for (k, v) in exports {
state.vars.insert(k.clone(), v);
state.exported.insert(k);
}
state.cwd = cwd;
state.allow_exec = allow_exec
.iter()
.filter_map(|p| match CommandPattern::parse(p) {
Ok(pat) => Some(pat),
Err(e) => {
eprintln!(
"yosh-plugin: ignoring invalid --allow-exec pattern {:?}: {}",
p, e
);
None
}
})
.collect();
state.sandbox_root = sandbox_root.map(|p| std::fs::canonicalize(&p).unwrap_or(p));
let loaded = match load_plugin(&wasm, state, std::time::Duration::from_millis(timeout)) {
Ok(l) => l,
Err(e) => {
eprintln!("yosh-plugin: {}", e);
return 99;
}
};
let outcome = match action {
RunAction::Exec { command, args } => invoke_exec(loaded, &command, &args),
RunAction::Hook { which } => {
let call = match which {
HookKind::PreExec { command_line } => HookCall::PreExec { command_line },
HookKind::PostExec {
command_line,
exit_code,
} => HookCall::PostExec {
command_line,
exit_code,
},
HookKind::OnCd { old, new } => HookCall::OnCd { old, new },
HookKind::PrePrompt => HookCall::PrePrompt,
};
invoke_hook(loaded, call)
}
};
match format {
OutputFormat::Human => print!("{}", format_human(&outcome)),
OutputFormat::Json => println!("{}", format_json(&outcome)),
}
match outcome.error_kind {
Some(_) => 99,
None => outcome.exit_code.unwrap_or(0),
}
}
fn cmd_install(source: &str, force: bool) -> i32 {
let config_path = sync::config_path();
match install::install(source, force, &config_path, None) {
Ok(msg) => {
eprintln!("{}", msg);
if source.starts_with("https://github.com/") {
eprintln!("Run 'yosh plugin sync' to download.");
}
0
}
Err(e) => {
eprintln!("yosh-plugin: {}", e);
1
}
}
}
fn cmd_sync(prune: bool) -> i32 {
let result = match sync::sync(prune) {
Ok(r) => r,
Err(e) => {
eprintln!("yosh-plugin: {}", e);
return 2;
}
};
for name in &result.succeeded {
eprintln!(" \u{2713} {}", name);
}
for (name, err) in &result.failed {
eprintln!(" \u{2717} {}: {}", name, err);
}
if result.failed.is_empty() {
eprintln!(
"yosh-plugin: sync complete ({} plugins)",
result.succeeded.len()
);
0
} else {
eprintln!(
"yosh-plugin: sync partial ({} succeeded, {} failed)",
result.succeeded.len(),
result.failed.len()
);
1
}
}
fn cmd_update(name_filter: Option<&str>) -> i32 {
let config_path = sync::config_path();
let client = github::GitHubClient::new();
let outcome = match update::update(&config_path, name_filter, &client) {
Ok(o) => o,
Err(e) => {
eprintln!("yosh-plugin: {}", e);
return 2;
}
};
for result in &outcome.results {
match &result.status {
update::UpdateStatus::Updated { from, to } => {
eprintln!(" {} {} \u{2192} {}", result.name, from, to);
}
update::UpdateStatus::AlreadyLatest { current } => {
eprintln!(" {} {} (already latest)", result.name, current);
}
update::UpdateStatus::Failed(e) => {
eprintln!(" \u{2717} {}: {}", result.name, e);
}
update::UpdateStatus::Skipped(_) => {
}
}
}
if outcome.any_updated {
return cmd_sync(false);
}
0
}
fn cmd_list() -> i32 {
let lock_path = sync::lock_path();
let lockfile = match lockfile::load_lockfile(&lock_path) {
Ok(l) => l,
Err(e) => {
eprintln!("yosh-plugin: {}", e);
return 2;
}
};
if lockfile.plugin.is_empty() {
eprintln!("no plugins installed (run 'yosh-plugin sync' first)");
return 0;
}
for entry in &lockfile.plugin {
let version = entry.version.as_deref().unwrap_or("-");
let verified =
match verify::verify_checksum(&config::expand_tilde_path(&entry.path), &entry.sha256) {
Ok(true) => "\u{2713} verified",
Ok(false) => "\u{2717} checksum mismatch",
Err(_) => "\u{2717} file missing",
};
let cached = match (&entry.cwasm_path, &entry.wasmtime_version) {
(Some(p), Some(wv))
if std::path::Path::new(&config::expand_tilde_path(p)).exists()
&& wv == precompile::WASMTIME_VERSION =>
{
"\u{2713} cached"
}
_ => "\u{2717} stale",
};
let caps = entry
.required_capabilities
.as_ref()
.map(|v| {
if v.is_empty() {
"[- (no capabilities)]".to_string()
} else {
format!("[{}]", v.join(", "))
}
})
.unwrap_or_else(|| "[?]".into());
println!(
"{:<16} {:<8} {:<48} {} {} {}",
entry.name, version, entry.source, verified, cached, caps
);
}
0
}
fn cmd_verify() -> i32 {
let lock_path = sync::lock_path();
let lockfile = match lockfile::load_lockfile(&lock_path) {
Ok(l) => l,
Err(e) => {
eprintln!("yosh-plugin: {}", e);
return 2;
}
};
let mut all_ok = true;
for entry in &lockfile.plugin {
let path = config::expand_tilde_path(&entry.path);
match verify::verify_checksum(&path, &entry.sha256) {
Ok(true) => {
eprintln!(" \u{2713} {}", entry.name);
}
Ok(false) => {
eprintln!(" \u{2717} {}: checksum mismatch", entry.name);
all_ok = false;
}
Err(e) => {
eprintln!(" \u{2717} {}: {}", entry.name, e);
all_ok = false;
}
}
}
if all_ok { 0 } else { 1 }
}