mod check;
mod fmt;
mod import;
mod init;
mod json;
mod merge_driver;
mod publish;
mod render;
mod skill;
mod verify;
mod version;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use loftspec::lint::Severity;
use loftspec_store::merge_driver::LAUNCHER;
#[derive(Parser)]
#[command(
name = "loft",
about = "Author, check and format a Loft specification in git",
after_help = "Exit codes: 0 on success, 1 on an error, a refusal or a failed check, 2 on a usage \
error.\nWith `--format json` a command prints one JSON document on standard \
output, described by its schema under `schemas/` in the Loft repository."
)]
#[command(arg_required_else_help = true, disable_version_flag = true)]
struct Cli {
#[arg(long)]
version: bool,
#[arg(long, value_name = "FORMAT", requires = "version")]
format: Option<Format>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Init {
#[arg(long)]
no_skill: bool,
#[command(flatten)]
output: Output,
},
Fmt {
#[arg(long)]
check: bool,
#[command(flatten)]
output: Output,
},
Check {
#[arg(long, value_name = "SEVERITY")]
fail_on: Option<FailOn>,
#[command(flatten)]
output: Output,
},
Import {
#[command(subcommand)]
source: ImportSource,
},
Publish {
#[command(flatten)]
output: Output,
},
Verify {
#[command(flatten)]
output: Output,
},
MergeDriver {
#[arg(long, conflicts_with_all = ["base", "ours", "theirs", "path"])]
install: bool,
#[arg(long, requires = "install", value_name = "COMMAND")]
command: Option<String>,
#[arg(required_unless_present = "install")]
base: Option<PathBuf>,
#[arg(required_unless_present = "install")]
ours: Option<PathBuf>,
#[arg(required_unless_present = "install")]
theirs: Option<PathBuf>,
#[arg(required_unless_present = "install")]
path: Option<String>,
#[command(flatten)]
output: Output,
},
Skill {
#[command(subcommand)]
action: SkillAction,
},
Render {
#[arg(long, value_name = "PATH")]
markdown: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
json: Option<PathBuf>,
#[command(flatten)]
output: Output,
},
}
#[derive(Subcommand)]
enum SkillAction {
Install {
#[command(flatten)]
output: Output,
},
}
#[derive(Subcommand)]
enum ImportSource {
V1 {
export: PathBuf,
#[arg(long, value_name = "FILE")]
counters: Option<PathBuf>,
#[command(flatten)]
output: Output,
},
}
#[derive(Clone, Copy, ValueEnum)]
enum FailOn {
Warning,
Info,
Hint,
}
impl From<FailOn> for Severity {
fn from(fail_on: FailOn) -> Self {
match fail_on {
FailOn::Warning => Self::Warning,
FailOn::Info => Self::Info,
FailOn::Hint => Self::Hint,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Format {
Json,
}
#[derive(Args)]
struct Output {
#[arg(long, value_name = "FORMAT")]
format: Option<Format>,
}
impl Output {
fn json(&self) -> bool {
self.format == Some(Format::Json)
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
if cli.version {
version::run(cli.format == Some(Format::Json));
return ExitCode::SUCCESS;
}
let Some(command) = cli.command else {
let _ = Cli::command().print_help();
return ExitCode::from(2);
};
let root = Path::new(".");
match command {
Command::Init { no_skill, output } => init::run(root, !no_skill, output.json()),
Command::Fmt { check, output } => fmt::run(root, check, output.json()),
Command::Check { fail_on, output } => {
check::run(root, fail_on.map(Severity::from), output.json())
}
Command::Import {
source:
ImportSource::V1 {
export,
counters,
output,
},
} => import::run(root, &export, counters.as_deref(), output.json()),
Command::Publish { output } => publish::run(root, output.json()),
Command::Verify { output } => verify::run(root, output.json()),
Command::MergeDriver {
install: true,
command,
output,
..
} => merge_driver::install(root, command.as_deref().unwrap_or(LAUNCHER), output.json()),
Command::MergeDriver {
base: Some(base),
ours: Some(ours),
theirs: Some(theirs),
path: Some(path),
output,
..
} => merge_driver::run(
root,
&merge_driver::Inputs {
base: &base,
ours: &ours,
theirs: &theirs,
path: &path,
},
output.json(),
),
Command::MergeDriver { .. } => ExitCode::from(2),
Command::Render {
markdown,
json,
output,
} => render::run(root, markdown, json, output.json()),
Command::Skill {
action: SkillAction::Install { output },
} => skill::run(root, output.json()),
}
}
fn report(errors: &[impl std::fmt::Display]) {
for error in errors {
eprintln!("{error}");
}
}