mod actions;
mod cache_paths;
mod init;
mod list;
mod migrate;
mod plugins;
mod show_env;
use crate::config::loader::{self, Discovered};
use crate::config::validate;
use crate::error::{Result, ShlaneError};
use crate::runtime;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use std::env;
use std::io;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "shlane")]
#[command(version)]
#[command(about = "A fastlane-like tool written in Rust", long_about = None)]
pub struct Cli {
#[arg(short = 'f', long, global = true, value_name = "PATH")]
file: Option<PathBuf>,
#[arg(short = 'C', long, global = true, value_name = "DIR")]
cwd: Option<PathBuf>,
#[arg(short = 'v', long, global = true, conflicts_with = "quiet")]
verbose: bool,
#[arg(short = 'q', long, global = true)]
quiet: bool,
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true, value_name = "PROFILE")]
env: Option<String>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum ActionCommands {
List,
Show {
#[arg(help = "Action name")]
name: String,
},
}
#[derive(Subcommand)]
enum PluginCommands {
Add {
#[arg(value_name = "SOURCE", help = "github:owner/repo@tag, or a git URL")]
source: String,
},
Remove {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
force: bool,
},
Install {
#[arg(long)]
force: bool,
},
List,
Lock,
Verify,
}
#[derive(Subcommand)]
enum Commands {
Run {
#[arg(help = "Name of the lane to execute")]
name: String,
#[arg(value_name = "KEY=VALUE")]
params: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long, value_name = "FORMAT:PATH")]
report: Vec<String>,
},
#[command(alias = "lanes")]
List,
Validate,
Init {
#[arg(long)]
force: bool,
},
Action {
#[command(subcommand)]
command: ActionCommands,
},
Env {
#[arg(long)]
all: bool,
},
Migrate {
#[arg(long, value_name = "PATH")]
fastfile: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
out: Option<PathBuf>,
#[arg(long)]
force: bool,
},
Plugin {
#[command(subcommand)]
command: PluginCommands,
},
#[command(name = "cache-paths")]
CachePaths,
Completions {
#[arg(value_enum)]
shell: Shell,
},
}
pub fn dispatch(cli: Cli) -> Result<()> {
let base = match &cli.cwd {
Some(dir) => dir.clone(),
None => env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
};
let file = cli.file.clone();
let verbosity = if cli.quiet {
runtime::Verbosity::Quiet
} else if cli.verbose {
runtime::Verbosity::Verbose
} else {
runtime::Verbosity::Normal
};
let profile = cli.env.clone();
let json = cli.json;
match cli.command {
Commands::Run {
name,
params,
dry_run,
report,
} => {
let reports = report
.iter()
.map(|spec| crate::report::parse(spec))
.collect::<std::result::Result<Vec<_>, String>>()
.map_err(|message| ShlaneError::ConfigProblems {
path: PathBuf::from("--report"),
problems: vec![message],
})?;
let found = load(file.as_deref(), &base)?;
let params = runtime::parse_params(params);
runtime::run_lane(
std::rc::Rc::new(found.config),
&found.root,
&name,
params,
runtime::Options {
dry_run,
reports,
verbosity,
json,
profile,
},
)
}
Commands::List => {
let found = load(file.as_deref(), &base)?;
list::print(&found.config, &found.path);
Ok(())
}
Commands::Validate => {
let found = load(file.as_deref(), &base)?;
let registry = registry_for(&found)?;
let problems = validate::check(&found.config, ®istry);
if problems.is_empty() {
let lanes = found.config.lanes.len();
println!("{} is valid ({lanes} lane(s))", found.path.display());
return Ok(());
}
Err(ShlaneError::ConfigProblems {
path: found.path,
problems,
})
}
Commands::Action { command } => {
let registry = match load(file.as_deref(), &base) {
Ok(found) => registry_for(&found)?,
Err(_) => crate::actions::Registry::builtins(),
};
match command {
ActionCommands::List => {
actions::list(®istry);
Ok(())
}
ActionCommands::Show { name } => actions::show(®istry, &name),
}
}
Commands::Plugin { command } => {
let found = load(file.as_deref(), &base)?;
match command {
PluginCommands::Add { source } => plugins::add(&found, &source),
PluginCommands::Remove { name, force } => plugins::remove(&found, &name, force),
PluginCommands::Install { force } => plugins::install(&found, force),
PluginCommands::List => plugins::list(&found),
PluginCommands::Lock => plugins::lock(&found),
PluginCommands::Verify => plugins::verify(&found),
}
}
Commands::Env { all } => {
let found = load(file.as_deref(), &base)?;
show_env::show(&found, profile.as_deref(), all)
}
Commands::Migrate {
fastfile,
out,
force,
} => migrate::run(&base, fastfile.as_deref(), out.as_deref(), force),
Commands::CachePaths => {
let found = load(file.as_deref(), &base)?;
cache_paths::print(&found.config, json);
Ok(())
}
Commands::Init { force } => init::write(&base, force),
Commands::Completions { shell } => {
clap_complete::generate(
shell,
&mut Cli::command(),
"shlane",
&mut io::stdout().lock(),
);
Ok(())
}
}
}
fn registry_for(found: &Discovered) -> Result<crate::actions::Registry> {
let loaded = crate::plugin::load_all(&found.config, &found.root)?;
Ok(crate::actions::Registry::builtins().with_plugins(crate::plugin::actions(loaded)))
}
fn load(file: Option<&std::path::Path>, base: &std::path::Path) -> Result<Discovered> {
match file {
Some(path) => loader::open(path),
None => loader::discover(base),
}
}