#![forbid(unsafe_code)]
mod archive;
mod commands;
mod credential;
mod deps;
mod diff;
mod error;
mod github;
mod http;
mod lock;
mod manifest;
mod policy;
mod project;
mod publish;
mod registry;
mod scope;
mod skill;
mod ui;
mod version;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use crate::archive::Limits;
use crate::commands::Ctx;
use crate::error::{Error, MANIFEST};
use crate::scope::Scope;
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(short, long, global = true)]
global: bool,
#[arg(long, global = true)]
agent: 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 AuthCmd {
Set {
host: String,
#[arg(long, default_value = credential::PRIVATE_TOKEN)]
username: String,
},
Remove {
host: String,
#[arg(long, default_value = credential::PRIVATE_TOKEN)]
username: String,
},
}
#[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>,
#[arg(long)]
diff: bool,
},
Outdated {
#[arg(long)]
strict: bool,
},
Diff {
package: Option<String>,
#[arg(long)]
to: Option<String>,
},
Info {
target: String,
},
Status,
Doctor {
#[arg(long)]
fix: bool,
},
Search { query: String },
#[command(subcommand)]
Auth(AuthCmd),
Publish {
#[arg(long)]
vendor: String,
#[arg(long)]
base_url: Option<String>,
#[arg(long)]
dist_url: Option<String>,
#[arg(long)]
inline: bool,
#[arg(long)]
out: PathBuf,
paths: Vec<PathBuf>,
},
Validate {
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>().ok().and_then(|n| n.checked_mul(mult)).ok_or_else(|| 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 cli.global && matches!(cli.cmd, Cmd::External(_) | Cmd::Publish { .. } | Cmd::Auth(_) | Cmd::Validate { .. }) {
return Err(Error::new(MANIFEST, "--global does not apply to this command"));
}
if let Cmd::External(args) = &cli.cmd {
return plugin(args);
}
if let Cmd::Auth(a) = &cli.cmd {
return auth(a);
}
if let Cmd::Validate { paths } = &cli.cmd {
return publish::validate(paths);
}
let scope = if cli.global {
Scope::global()?
} else {
Scope::project(match cli.cmd {
Cmd::Init => {
let cwd = std::env::current_dir()?;
project::check_root(&cwd)?;
cwd
}
Cmd::Publish { .. } => std::env::current_dir()?,
_ => project::find_root()?,
})
};
let interactive = !ci() && std::io::stdin().is_terminal() && std::io::stderr().is_terminal();
let mut ctx = Ctx::new(scope, cli.verbose, limits, interactive);
ctx.read_only = matches!(cli.cmd, Cmd::Info { .. } | Cmd::Diff { .. });
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, diff } => commands::update(&ctx, &packages, diff),
Cmd::Outdated { strict } => commands::outdated(&ctx, strict),
Cmd::Diff { package, to } => commands::diff(&ctx, package.as_deref(), to.as_deref()),
Cmd::Info { target } => commands::info(&ctx, &target),
Cmd::Status => commands::status(&ctx),
Cmd::Doctor { fix } => commands::doctor(&ctx, fix),
Cmd::Search { query } => commands::search(&ctx, &query),
Cmd::External(_) | Cmd::Auth(_) | Cmd::Validate { .. } => unreachable!("handled above"),
Cmd::Publish { vendor, base_url, dist_url, inline, out, paths } => {
if vendor.is_empty() {
return Err(Error::new(MANIFEST, "--vendor must not be empty"));
}
publish::publish(&publish::PublishArgs { vendor, base_url, dist_url, inline, out, paths })
}
}
}
fn auth(cmd: &AuthCmd) -> Result<(), Error> {
let (AuthCmd::Set { host, username } | AuthCmd::Remove { host, username }) = cmd;
let url = url::Url::parse(host).or_else(|_| url::Url::parse(&format!("https://{host}")));
let Some(key) = url
.ok()
.filter(|u| u.scheme() == "https")
.and_then(|u| Some(u.host_str()?.to_string() + &u.port().map(|p| format!(":{p}")).unwrap_or_default()))
else {
return Err(Error::new(MANIFEST, format!("invalid host {host:?}")));
};
match cmd {
AuthCmd::Set { .. } => {
let piped = !std::io::stdin().is_terminal();
let token = if piped { Some(std::io::read_to_string(std::io::stdin())?.trim().to_string()) } else { None };
let (chars, helpers) = credential::store(&key, username, token.as_deref())?;
ui::done("stored", format!("a {chars}-character token for {key} with git's credential helper ({helpers})"));
}
AuthCmd::Remove { .. } => credential::remove(&key, username)?,
}
Ok(())
}
fn plugin(args: &[OsString]) -> Result<(), Error> {
let name = args[0].to_string_lossy();
if name.is_empty() || 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}"))
}
})?;
#[cfg(unix)]
if let Some(sig) = std::os::unix::process::ExitStatusExt::signal(&status) {
std::process::exit(128 + sig);
}
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 });
});
ui::init(cli.agent);
if let Err(e) = run(cli) {
ui::error(&e.msg, e.hint.as_deref());
std::process::exit(e.code.into());
}
}