use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::commands;
#[derive(Debug, Parser)]
#[command(name = "heal", version, about = "Code health hook-driven harness", long_about = None)]
pub struct Cli {
#[arg(long, global = true)]
pub project: Option<PathBuf>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Init {
#[arg(long)]
force: bool,
#[arg(long, short = 'y', conflicts_with = "no_skills")]
yes: bool,
#[arg(long)]
no_skills: bool,
#[arg(long)]
json: bool,
},
Hook {
#[command(subcommand)]
event: HookEvent,
},
Metrics {
#[arg(long)]
json: bool,
#[arg(long, value_enum)]
metric: Option<MetricKind>,
#[arg(long, value_name = "PATH")]
workspace: Option<std::path::PathBuf>,
#[arg(long)]
no_pager: bool,
},
Status(StatusArgs),
Diff(DiffArgs),
#[command(hide = true)]
Mark {
#[command(subcommand)]
action: MarkAction,
},
#[command(hide = true, name = "mark-fixed")]
MarkFixed {
#[arg(long, value_name = "ID")]
finding_id: String,
#[arg(long, value_name = "SHA")]
commit_sha: String,
#[arg(long)]
json: bool,
},
Skills {
#[command(subcommand)]
action: SkillsAction,
},
Calibrate {
#[arg(long)]
force: bool,
#[arg(long)]
json: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum MetricKind {
Loc,
Complexity,
Churn,
ChangeCoupling,
Duplication,
Hotspot,
Lcom,
}
impl MetricKind {
#[must_use]
pub fn json_key(self) -> &'static str {
match self {
Self::Loc => "loc",
Self::Complexity => "complexity",
Self::Churn => "churn",
Self::ChangeCoupling => "change_coupling",
Self::Duplication => "duplication",
Self::Hotspot => "hotspot",
Self::Lcom => "lcom",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum FindingMetric {
Ccn,
Cognitive,
Complexity,
Duplication,
Coupling,
Hotspot,
Lcom,
}
impl FindingMetric {
#[must_use]
pub fn matches(self, metric: &str) -> bool {
match self {
Self::Ccn => metric == "ccn",
Self::Cognitive => metric == "cognitive",
Self::Complexity => matches!(metric, "ccn" | "cognitive"),
Self::Duplication => metric == "duplication",
Self::Coupling => matches!(metric, "change_coupling" | "change_coupling.symmetric"),
Self::Hotspot => metric == "hotspot",
Self::Lcom => metric == "lcom",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum SeverityFilter {
Critical,
High,
Medium,
Ok,
}
impl SeverityFilter {
#[must_use]
pub fn into_severity(self) -> crate::core::severity::Severity {
use crate::core::severity::Severity;
match self {
Self::Critical => Severity::Critical,
Self::High => Severity::High,
Self::Medium => Severity::Medium,
Self::Ok => Severity::Ok,
}
}
}
#[derive(Debug, Clone, Copy, Subcommand)]
pub enum HookEvent {
Commit,
Edit,
Stop,
}
#[derive(Debug, clap::Args)]
#[allow(clippy::struct_excessive_bools)] pub struct StatusArgs {
#[arg(long, value_enum)]
pub metric: Option<FindingMetric>,
#[arg(long, value_name = "PATH")]
pub workspace: Option<String>,
#[arg(long)]
pub feature: Option<String>,
#[arg(long, value_enum)]
pub severity: Option<SeverityFilter>,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub refresh: bool,
#[arg(long, value_name = "N")]
pub top: Option<usize>,
#[arg(long)]
pub no_pager: bool,
}
#[derive(Debug, clap::Args)]
pub struct DiffArgs {
#[arg(value_name = "GIT_REF")]
pub revspec: Option<String>,
#[arg(long, value_name = "PATH")]
pub workspace: Option<String>,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub no_pager: bool,
}
#[derive(Debug, Clone, Subcommand)]
pub enum MarkAction {
Fix {
#[arg(long, value_name = "ID")]
finding_id: String,
#[arg(long, value_name = "SHA")]
commit_sha: String,
#[arg(long)]
json: bool,
},
Accept {
#[arg(long, value_name = "ID")]
finding_id: String,
#[arg(long, value_name = "TEXT", default_value = "")]
reason: String,
#[arg(long)]
json: bool,
},
}
#[derive(Debug, Clone, Copy, Subcommand)]
pub enum SkillsAction {
Install {
#[arg(long)]
force: bool,
#[arg(long)]
json: bool,
},
Update {
#[arg(long)]
force: bool,
#[arg(long)]
json: bool,
},
Status {
#[arg(long)]
json: bool,
},
Uninstall {
#[arg(long)]
json: bool,
},
}
impl Cli {
pub fn run(self) -> Result<()> {
let project = self
.project
.unwrap_or_else(|| std::env::current_dir().expect("cwd"));
match self.command {
Command::Init {
force,
yes,
no_skills,
json,
} => commands::init::run(&project, force, yes, no_skills, json),
Command::Hook { event } => commands::hook::run(&project, event),
Command::Metrics {
json,
metric,
workspace,
no_pager,
} => commands::metrics::run(&project, json, metric, workspace.as_deref(), no_pager),
Command::Status(args) => commands::status::run(&project, &args),
Command::Diff(args) => commands::diff::run(
&project,
args.revspec.as_deref(),
args.workspace.as_deref(),
args.all,
args.json,
args.no_pager,
),
Command::Mark { action } => match action {
MarkAction::Fix {
finding_id,
commit_sha,
json,
} => commands::mark::run_fix(&project, &finding_id, &commit_sha, json),
MarkAction::Accept {
finding_id,
reason,
json,
} => commands::mark::run_accept(&project, &finding_id, &reason, json),
},
Command::MarkFixed {
finding_id,
commit_sha,
json,
} => commands::mark::run_fix_legacy(&project, &finding_id, &commit_sha, json),
Command::Skills { action } => commands::skills::run(&project, action),
Command::Calibrate { force, json } => commands::calibrate::run(&project, force, json),
}
}
}