use clap::Parser;
use lorum::commands;
#[derive(Parser)]
#[command(name = "lorum", version, about)]
struct Cli {
#[arg(long = "config", global = true)]
config: Option<String>,
#[arg(long = "verbose", global = true)]
verbose: bool,
#[command(subcommand)]
command: Option<Commands>,
#[arg(long = "no-welcome", global = true)]
no_welcome: bool,
}
#[derive(clap::Subcommand)]
enum Commands {
Init {
#[arg(long)]
local: bool,
#[arg(long)]
yes: bool,
},
Import {
#[arg(long = "from")]
from: String,
#[arg(long)]
dry_run: bool,
},
Sync {
#[arg(long)]
dry_run: bool,
#[arg(long = "tools", num_args = 1..)]
tools: Vec<String>,
#[arg(long = "expand-env")]
expand_env: bool,
#[arg(long)]
mcp: bool,
#[arg(long)]
hooks: bool,
#[arg(long)]
skills: bool,
#[arg(long)]
rules: bool,
#[arg(long)]
all: bool,
},
Check,
Status,
Doctor {
#[arg(long = "tools", num_args = 1.., value_delimiter = ',')]
tools: Vec<String>,
},
Config {
#[arg(long = "resolve-env")]
resolve_env: bool,
#[arg(long)]
local: bool,
#[arg(long)]
global: bool,
#[arg(long = "format", default_value = "yaml")]
format: String,
},
Backup {
#[command(subcommand)]
action: BackupAction,
},
Mcp {
#[command(subcommand)]
action: McpAction,
},
Rule {
#[command(subcommand)]
action: RuleAction,
},
Hook {
#[command(subcommand)]
action: HookAction,
},
Skill {
#[command(subcommand)]
action: SkillAction,
},
}
#[derive(clap::Subcommand)]
enum BackupAction {
List,
Create {
#[arg(num_args = 0..)]
tools: Vec<String>,
#[arg(long)]
all: bool,
},
Restore {
tool: String,
#[arg(long)]
backup: Option<String>,
},
}
#[derive(clap::Subcommand)]
enum McpAction {
Add {
name: String,
#[arg(long)]
command: String,
#[arg(long = "args", num_args = 1..)]
args: Vec<String>,
#[arg(long = "env", num_args = 1..)]
env: Vec<String>,
},
Remove {
name: String,
},
List,
Edit {
name: String,
#[arg(long)]
command: Option<String>,
#[arg(long = "args", num_args = 1..)]
args: Option<Vec<String>>,
#[arg(long = "env", num_args = 1..)]
env: Option<Vec<String>>,
},
}
#[derive(clap::Subcommand)]
enum RuleAction {
Init,
Add {
name: String,
#[arg(long)]
content: String,
},
Remove {
name: String,
},
Edit {
name: String,
#[arg(long)]
content: String,
},
List,
Show {
name: Option<String>,
},
Sync {
#[arg(long)]
dry_run: bool,
#[arg(long = "tools", num_args = 1..)]
tools: Vec<String>,
},
Import {
#[arg(long = "from")]
from: String,
},
}
#[derive(clap::Subcommand)]
enum SkillAction {
List,
Show {
name: String,
},
Add {
name: String,
#[arg(long = "from")]
from: String,
},
Remove {
name: String,
},
Sync {
#[arg(long)]
dry_run: bool,
#[arg(long = "tools", num_args = 1..)]
tools: Vec<String>,
},
}
#[derive(clap::Subcommand)]
enum HookAction {
Add {
event: String,
#[arg(long)]
matcher: String,
#[arg(long)]
command: String,
#[arg(long)]
timeout: Option<u64>,
#[arg(long)]
handler_type: Option<String>,
},
Remove {
event: String,
#[arg(long)]
matcher: Option<String>,
},
List,
Sync {
#[arg(long)]
dry_run: bool,
#[arg(long = "tools", num_args = 1..)]
tools: Vec<String>,
},
}
fn resolve_sync_dimensions(
mcp: bool,
hooks: bool,
skills: bool,
rules: bool,
all: bool,
) -> Vec<lorum::SyncDimension> {
use lorum::SyncDimension;
if all {
return vec![
SyncDimension::Mcp,
SyncDimension::Rules,
SyncDimension::Hooks,
SyncDimension::Skills,
];
}
let any_explicit = mcp || hooks || skills || rules;
if !any_explicit {
return vec![SyncDimension::Mcp];
}
let mut dims = Vec::new();
if mcp {
dims.push(SyncDimension::Mcp);
}
if rules {
dims.push(SyncDimension::Rules);
}
if hooks {
dims.push(SyncDimension::Hooks);
}
if skills {
dims.push(SyncDimension::Skills);
}
dims
}
impl Cli {
fn run(self) -> Result<(), lorum::error::LorumError> {
match self.command {
None => {
let config_path = self.config.as_deref().map(std::path::Path::new);
let has_config = match lorum::config::resolve_effective_config_from_cwd(config_path)
{
Ok(cfg) => {
!cfg.mcp.servers.is_empty() || !cfg.hooks.events.is_empty() || {
let cwd = std::env::current_dir()
.map_err(|e| lorum::error::LorumError::Io { source: e })?;
Ok::<_, lorum::error::LorumError>(
lorum::config::find_project_config(&cwd).is_some(),
)
}?
}
Err(_) => false,
};
if !has_config && !self.no_welcome {
println!("Welcome to lorum! You haven't created a configuration file yet.\n");
println!("Quick start:");
println!(" lorum init Create initial config");
println!(" lorum init --local Create project-level config");
println!(" lorum import --from all Import from existing tools");
return Ok(());
}
Err(lorum::error::LorumError::Other {
message: "no configuration found".into(),
})
}
Some(Commands::Init { local, yes }) => {
commands::run_init(self.config.as_deref(), local, yes)
}
Some(Commands::Import { from, dry_run }) => {
commands::run_import(&from, dry_run, self.config.as_deref())
}
Some(Commands::Sync {
dry_run,
tools,
expand_env,
mcp,
hooks,
skills,
rules,
all,
}) => {
let dimensions = resolve_sync_dimensions(mcp, hooks, skills, rules, all);
commands::run_sync_dimensions(
&dimensions,
dry_run,
&tools,
expand_env,
self.config.as_deref(),
)
}
Some(Commands::Check) => commands::run_check(self.config.as_deref()),
Some(Commands::Status) => commands::run_status(self.config.as_deref()),
Some(Commands::Doctor { tools }) => {
let results = commands::run_doctor(&tools)?;
commands::print_doctor_results(&results);
let mut consistency = commands::run_doctor_consistency(&tools)?;
consistency.extend(commands::run_doctor_hooks_consistency(&tools)?);
consistency.extend(commands::run_doctor_skills_consistency(&tools)?);
consistency.extend(commands::run_doctor_rules_consistency(&tools)?);
commands::print_consistency_reports(&consistency);
Ok(())
}
Some(Commands::Config {
resolve_env,
local,
global,
format,
}) => {
let fmt = match format.as_str() {
"json" => lorum::config::OutputFormat::Json,
"yaml" => lorum::config::OutputFormat::Yaml,
other => {
return Err(lorum::error::LorumError::Other {
message: format!("invalid format '{other}', expected 'yaml' or 'json'"),
});
}
};
commands::run_config(resolve_env, local, global, fmt, self.config.as_deref())
}
Some(Commands::Backup { action }) => match action {
BackupAction::List => commands::run_backup_list(self.config.as_deref()),
BackupAction::Create { tools, all } => {
commands::run_backup_create(&tools, all, self.config.as_deref())
}
BackupAction::Restore { tool, backup } => {
commands::run_backup_restore(&tool, backup.as_deref(), self.config.as_deref())
}
},
Some(Commands::Mcp { action }) => match action {
McpAction::Add {
name,
command,
args,
env,
} => {
commands::mcp::run_mcp_add(&name, &command, &args, &env, self.config.as_deref())
}
McpAction::Remove { name } => {
commands::mcp::run_mcp_remove(&name, self.config.as_deref())
}
McpAction::List => commands::mcp::run_mcp_list(self.config.as_deref()),
McpAction::Edit {
name,
command,
args,
env,
} => commands::mcp::run_mcp_edit(
&name,
command.as_deref(),
args.as_deref(),
env.as_deref(),
self.config.as_deref(),
),
},
Some(Commands::Rule { action }) => match action {
RuleAction::Init => commands::rule::run_rule_init(),
RuleAction::Add { name, content } => commands::rule::run_rule_add(&name, &content),
RuleAction::Remove { name } => commands::rule::run_rule_remove(&name),
RuleAction::Edit { name, content } => {
commands::rule::run_rule_edit(&name, &content)
}
RuleAction::List => commands::rule::run_rule_list(),
RuleAction::Show { name } => commands::rule::run_rule_show(name.as_deref()),
RuleAction::Sync { dry_run, tools } => {
commands::rule::run_rule_sync(dry_run, &tools)
}
RuleAction::Import { from } => commands::rule::run_rule_import(&from),
},
Some(Commands::Hook { action }) => match action {
HookAction::Add {
event,
matcher,
command,
timeout,
handler_type,
} => commands::hook::run_hook_add(
&event,
&matcher,
&command,
timeout,
handler_type.as_deref(),
self.config.as_deref(),
),
HookAction::Remove { event, matcher } => commands::hook::run_hook_remove(
&event,
matcher.as_deref(),
self.config.as_deref(),
),
HookAction::List => commands::hook::run_hook_list(self.config.as_deref()),
HookAction::Sync { dry_run, tools } => {
commands::hook::run_hook_sync(dry_run, &tools, self.config.as_deref())
}
},
Some(Commands::Skill { action }) => match action {
SkillAction::List => commands::skill::run_skill_list(None),
SkillAction::Show { name } => commands::skill::run_skill_show(&name, None),
SkillAction::Add { name, from } => {
commands::skill::run_skill_add(&name, &from, None)
}
SkillAction::Remove { name } => commands::skill::run_skill_remove(&name, None),
SkillAction::Sync { dry_run, tools } => {
commands::skill::run_skill_sync(dry_run, &tools, None)
}
},
}
}
}
fn main() {
let cli = Cli::parse();
if let Err(err) = cli.run() {
eprintln!("error: {err}");
std::process::exit(1);
}
}