mod archive;
mod commands;
mod error;
mod github;
mod http;
mod lock;
mod manifest;
mod project;
mod publish;
mod registry;
mod skill;
mod version;
use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use crate::archive::Limits;
use crate::commands::Ctx;
use crate::error::{Error, MANIFEST};
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("SKILLCASK_COMMIT"), ")");
#[derive(Parser)]
#[command(name = "skillcask", version = VERSION)]
struct Cli {
#[arg(short, long, global = true)]
verbose: bool,
#[arg(long, global = true, value_parser = parse_size)]
max_download: Option<u64>,
#[arg(long, global = true, value_parser = parse_size)]
max_unpacked: Option<u64>,
#[arg(long, global = true)]
max_files: Option<usize>,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Init,
Add {
package: String,
#[arg(long)]
repository: Option<String>,
},
Remove {
#[arg(required = true)]
packages: Vec<String>,
},
Install {
#[arg(long)]
frozen: bool,
},
Update { packages: Vec<String> },
Outdated,
Status,
Doctor {
#[arg(long)]
fix: bool,
},
Search { query: String },
Publish {
#[arg(long)]
vendor: String,
#[arg(long)]
base_url: String,
#[arg(long)]
out: PathBuf,
paths: Vec<PathBuf>,
},
#[command(external_subcommand)]
External(Vec<OsString>),
}
fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim();
let (num, mult) = match s.char_indices().last() {
Some((i, 'K' | 'k')) => (&s[..i], 1 << 10),
Some((i, 'M' | 'm')) => (&s[..i], 1 << 20),
Some((i, 'G' | 'g')) => (&s[..i], 1 << 30),
_ => (s, 1),
};
num.parse::<u64>().map(|n| n * mult).map_err(|_| format!("invalid size {s:?}, use e.g. 50M"))
}
fn ci() -> bool {
std::env::var("CI").is_ok_and(|v| !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"))
}
fn run(cli: Cli) -> Result<(), Error> {
let d = Limits::default();
let limits = Limits {
download: cli.max_download.unwrap_or(d.download),
unpacked: cli.max_unpacked.unwrap_or(d.unpacked),
files: cli.max_files.unwrap_or(d.files),
};
if let Cmd::External(args) = &cli.cmd {
return plugin(args);
}
let root = match cli.cmd {
Cmd::Init | Cmd::Publish { .. } => std::env::current_dir()?,
_ => project::find_root()?,
};
let ctx = Ctx::new(root, cli.verbose, limits);
match cli.cmd {
Cmd::Init => commands::init(&ctx),
Cmd::Add { package, repository } => commands::add(&ctx, &package, repository),
Cmd::Remove { packages } => commands::remove(&ctx, &packages),
Cmd::Install { frozen } => commands::install(&ctx, frozen || ci()),
Cmd::Update { packages } => commands::update(&ctx, &packages),
Cmd::Outdated => commands::outdated(&ctx),
Cmd::Status => commands::status(&ctx),
Cmd::Doctor { fix } => commands::doctor(&ctx, fix),
Cmd::Search { query } => commands::search(&ctx, &query),
Cmd::External(_) => unreachable!("handled above"),
Cmd::Publish { vendor, base_url, out, paths } => {
if vendor.is_empty() {
return Err(Error::new(MANIFEST, "--vendor must not be empty"));
}
publish::publish(&publish::PublishArgs { vendor, base_url, out, paths })
}
}
}
fn plugin(args: &[OsString]) -> Result<(), Error> {
let name = args[0].to_string_lossy();
if name.contains(['/', '\\']) {
return Err(Error::new(MANIFEST, format!("unknown command {name:?}")));
}
let exe = format!("skillcask-{name}");
let status = std::process::Command::new(&exe).args(&args[1..]).status().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Error::new(MANIFEST, format!("unknown command {name:?} (no {exe} on PATH)"))
} else {
Error::new(error::IO, format!("{exe}: {e}"))
}
})?;
std::process::exit(status.code().unwrap_or(1));
}
fn main() {
let cli = Cli::try_parse().unwrap_or_else(|e| {
let _ = e.print();
std::process::exit(if e.use_stderr() { MANIFEST.into() } else { 0 });
});
if let Err(e) = run(cli) {
eprintln!("error: {e}");
std::process::exit(e.code.into());
}
}