mod catalog;
mod config_tui;
mod config_ui;
mod doctor;
mod installer;
mod manual;
mod migration;
mod model;
mod paths;
use anyhow::Result;
use std::path::PathBuf;
use clap::{ArgGroup, Args, Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(
name = "skiller",
version,
about = "Declaratively manage project and global skills from registered catalogs"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
AddCatalog {
alias: String,
source: String,
},
Catalog {
#[command(subcommand)]
command: CatalogCommand,
},
Config {
#[arg(short = 'g', long)]
global: bool,
#[arg(long, conflicts_with_all = ["set", "set_gitignore", "agent"])]
print: bool,
#[arg(long, value_name = "SKILL=MODE")]
set: Vec<String>,
#[arg(long, value_name = "SKILL=BOOL")]
set_gitignore: Vec<String>,
#[arg(long, value_name = "AGENT")]
agent: Vec<String>,
},
Doctor {
#[arg(short = 'g', long)]
global: bool,
#[arg(long, conflicts_with = "repair")]
print: bool,
#[arg(long)]
repair: bool,
#[arg(long, requires = "repair")]
yes: bool,
},
Migrate {
#[arg(long, value_name = "PATH", conflicts_with_all = ["plan", "check", "apply", "yes"])]
init: Option<PathBuf>,
#[arg(long, value_name = "PATH", conflicts_with = "init")]
plan: Option<PathBuf>,
#[arg(long, requires = "plan", conflicts_with = "apply")]
check: bool,
#[arg(long, requires = "plan", conflicts_with = "check")]
apply: bool,
#[arg(long, requires = "apply")]
yes: bool,
},
Install {
#[arg(short = 'g', long)]
global: bool,
},
}
#[derive(Debug, Subcommand)]
enum CatalogCommand {
AddSkill(AddSkillArgs),
}
#[derive(Debug, Args)]
#[command(group(
ArgGroup::new("eligibility")
.required(true)
.multiple(false)
.args(["global", "project"])
))]
struct AddSkillArgs {
#[arg(long)]
root: PathBuf,
#[arg(long)]
source: PathBuf,
#[arg(long)]
scope: String,
#[arg(long)]
global: bool,
#[arg(long)]
project: bool,
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::AddCatalog { alias, source } => catalog::add_catalog(&alias, &source),
Command::Catalog {
command: CatalogCommand::AddSkill(args),
} => catalog::add_skill(
&args.root,
&args.source,
&args.scope,
if args.global {
catalog::CatalogEligibility::Global
} else {
debug_assert!(args.project);
catalog::CatalogEligibility::Project
},
),
Command::Config {
global,
print,
set,
set_gitignore,
agent,
} => {
let scope = if global {
installer::InstallScope::Global
} else {
installer::InstallScope::Project(paths::project_root()?)
};
config_ui::configure(scope, print, &set, &set_gitignore, &agent)
}
Command::Doctor {
global,
print,
repair,
yes,
} => {
let scope = if global {
installer::InstallScope::Global
} else {
installer::InstallScope::Project(paths::project_root()?)
};
doctor::run(scope, print, repair, yes)
}
Command::Migrate {
init,
plan,
check: _,
apply,
yes,
} => match (init, plan) {
(Some(path), None) => migration::initialize(&path),
(None, Some(path)) => migration::run_plan(&path, apply, yes),
(None, None) => migration::interactive(),
(Some(_), Some(_)) => unreachable!("Clap rejects conflicting migration inputs"),
},
Command::Install { global } => {
let scope = if global {
installer::InstallScope::Global
} else {
installer::InstallScope::Project(paths::project_root()?)
};
installer::install(scope)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn doctor_requires_repair_for_yes_and_separates_print() {
assert!(Cli::try_parse_from(["skiller", "doctor", "--yes"]).is_err());
assert!(Cli::try_parse_from(["skiller", "doctor", "--repair", "--print"]).is_err());
assert!(Cli::try_parse_from(["skiller", "doctor", "-g", "--repair", "--yes"]).is_ok());
}
#[test]
fn migration_cli_separates_check_apply_and_noninteractive_confirmation() {
assert!(Cli::try_parse_from(["skiller", "migrate", "--yes"]).is_err());
assert!(
Cli::try_parse_from([
"skiller",
"migrate",
"--plan",
"plan.json",
"--check",
"--apply"
])
.is_err()
);
assert!(
Cli::try_parse_from([
"skiller",
"migrate",
"--plan",
"plan.json",
"--apply",
"--yes"
])
.is_ok()
);
}
#[test]
fn catalog_add_skill_requires_one_eligibility_flag() {
let base = [
"skiller",
"catalog",
"add-skill",
"--root",
"catalog",
"--source",
"candidate",
"--scope",
"learning",
];
assert!(Cli::try_parse_from(base).is_err());
assert!(Cli::try_parse_from(base.into_iter().chain(["--global", "--project"])).is_err());
assert!(Cli::try_parse_from(base.into_iter().chain(["--global"])).is_ok());
assert!(Cli::try_parse_from(base.into_iter().chain(["--project"])).is_ok());
}
}