mod catalog;
mod config_tui;
mod config_ui;
mod doctor;
mod installer;
mod manual;
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"])]
print: bool,
#[arg(long, value_name = "SKILL=MODE")]
set: Vec<String>,
#[arg(long, value_name = "SKILL=BOOL")]
set_gitignore: 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,
},
Install {
#[arg(short = 'g', long)]
global: bool,
#[arg(long)]
migrate: 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,
} => {
let scope = if global {
installer::InstallScope::Global
} else {
installer::InstallScope::Project(paths::project_root()?)
};
config_ui::configure(scope, print, &set, &set_gitignore)
}
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::Install { global, migrate } => {
let scope = if global {
installer::InstallScope::Global
} else {
installer::InstallScope::Project(paths::project_root()?)
};
installer::install(scope, migrate)
}
}
}
#[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 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());
}
}