svccat 0.10.0

Detect drift between your declared service catalog and what actually lives in the repo.
Documentation
use clap::{Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use std::path::PathBuf;

#[derive(Parser, Debug)]
#[command(
    name = "svccat",
    version,
    about = "Service catalog drift detection for multi-service repositories",
    long_about = "\
svccat reads your declared service manifest and compares it against what \
actually exists in the repo, flagging drift before it becomes toil.\n\n\
Typical workflow:\n  \
  svccat check                   # inspect drift in the current repo\n  \
  svccat check --fail-on-drift   # gate CI on zero drift\n  \
  svccat graph                   # emit a Mermaid diagram for docs\n  \
  svccat export --format json    # machine-readable catalog snapshot"
)]
pub struct Cli {
    /// Repository root (defaults to current directory)
    #[arg(short, long, global = true)]
    pub root: Option<PathBuf>,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Check declared services against the repo and report drift
    Check {
        /// Path to the manifest file (auto-detected if omitted)
        #[arg(short, long)]
        manifest: Option<PathBuf>,

        /// Output format
        #[arg(short, long, value_enum, default_value_t = OutputFormat::Terminal)]
        format: OutputFormat,

        /// Exit with code 1 when drift is detected (useful in CI)
        #[arg(long)]
        fail_on_drift: bool,

        /// Ping each service URL and report reachability
        #[arg(long)]
        ping: bool,

        /// Glob patterns to exclude from discovery (repeatable, e.g. --ignore "examples/*")
        #[arg(long, value_name = "PATTERN")]
        ignore: Vec<String>,

        /// Only check services owned by this team (matches the `team:` field)
        #[arg(long, value_name = "TEAM")]
        team: Option<String>,

        /// Compare against the manifest at this git ref; show only new / resolved drift
        ///
        /// Example: --since HEAD~1   or   --since main
        #[arg(long, value_name = "GIT_REF")]
        since: Option<String>,

        /// Exit with code 1 when --since reveals new drift items (ignores pre-existing drift)
        #[arg(long, requires = "since")]
        fail_on_new_drift: bool,
    },

    /// Generate a Mermaid or Markdown view of the service catalog
    Graph {
        /// Path to the manifest file (auto-detected if omitted)
        #[arg(short, long)]
        manifest: Option<PathBuf>,

        /// Output format
        #[arg(short, long, value_enum, default_value_t = GraphFormat::Mermaid)]
        format: GraphFormat,

        /// Only include services owned by this team; cross-team depends_on targets
        /// are shown as external nodes
        #[arg(long, value_name = "TEAM")]
        team: Option<String>,
    },

    /// Export the full service catalog with drift summary
    Export {
        /// Path to the manifest file (auto-detected if omitted)
        #[arg(short, long)]
        manifest: Option<PathBuf>,

        /// Export format
        #[arg(short, long, value_enum, default_value_t = ExportFormat::Json)]
        format: ExportFormat,

        /// Glob patterns to exclude from discovery (repeatable)
        #[arg(long, value_name = "PATTERN")]
        ignore: Vec<String>,
    },

    /// Scaffold a services.yaml by auto-discovering services in the repo
    Init {
        /// Where to write the manifest (default: services.yaml in repo root)
        #[arg(short, long)]
        output: Option<PathBuf>,

        /// Overwrite an existing manifest file
        #[arg(long)]
        force: bool,
    },

    /// Compare two svccat export JSON snapshots and show what changed
    ///
    /// Generate snapshots with: svccat export --format json > snapshot.json
    Diff {
        /// Path to the older snapshot (JSON)
        before: std::path::PathBuf,

        /// Path to the newer snapshot (JSON)
        after: std::path::PathBuf,
    },

    /// Watch the manifest and service directories for changes and re-run drift checks
    ///
    /// Continuously monitors the manifest file and service directories.
    /// Re-runs drift analysis whenever a file change is detected and prints
    /// a timestamped report. Press Ctrl-C to stop.
    Watch {
        /// Path to the manifest file (auto-detected if omitted)
        #[arg(short, long)]
        manifest: Option<PathBuf>,

        /// Exit with code 1 when drift is detected on the first check
        #[arg(long)]
        fail_on_drift: bool,

        /// Only watch services owned by this team
        #[arg(long, value_name = "TEAM")]
        team: Option<String>,

        /// Glob patterns to exclude from discovery (repeatable)
        #[arg(long, value_name = "PATTERN")]
        ignore: Vec<String>,
    },

    /// Generate a full ownership and drift report
    ///
    /// Outputs a per-team breakdown of every service with language, platform, oncall,
    /// and drift status, plus a dependency summary and full drift details.
    Report {
        /// Path to the manifest file (auto-detected if omitted)
        #[arg(short, long)]
        manifest: Option<PathBuf>,

        /// Output format
        #[arg(short, long, value_enum, default_value_t = ReportFormat::Markdown)]
        format: ReportFormat,

        /// Write output to this file instead of stdout
        #[arg(short, long, value_name = "FILE")]
        output: Option<PathBuf>,

        /// Glob patterns to exclude from discovery (repeatable)
        #[arg(long, value_name = "PATTERN")]
        ignore: Vec<String>,

        /// Show drift evolution across the last N git commits
        ///
        /// Requires a git repository. Loads the manifest at each of the last N commits
        /// and emits a Markdown table of errors/warnings over time.
        #[arg(long, value_name = "N")]
        history: Option<usize>,

        /// Emit a Shields.io badge Markdown snippet reflecting current drift status.
        ///
        /// Prints a Markdown image link you can paste into your README:
        ///   [![svccat drift: clean](…)](…)
        #[arg(long)]
        badge: bool,
    },

    /// Validate the manifest for structural issues
    ///
    /// Checks for duplicate service names, blank names, self-referential
    /// depends_on entries, duplicate depends_on entries, and unknown manifest versions.
    Lint {
        /// Path to the manifest file (auto-detected if omitted)
        #[arg(short, long)]
        manifest: Option<PathBuf>,
    },

    /// Print shell completion script to stdout
    ///
    /// Source the output to enable tab completion, e.g.:
    ///   source <(svccat completions bash)
    ///   svccat completions zsh > ~/.zfunc/_svccat
    Completions {
        /// Shell to generate completions for
        shell: Shell,
    },
}

#[derive(Debug, Clone, ValueEnum, PartialEq)]
pub enum OutputFormat {
    Terminal,
    Json,
    Sarif,
    Markdown,
    GithubAnnotation,
}

#[derive(Debug, Clone, ValueEnum, PartialEq)]
pub enum ReportFormat {
    Markdown,
    Html,
}

#[derive(Debug, Clone, ValueEnum, PartialEq)]
pub enum GraphFormat {
    Mermaid,
    Markdown,
}

#[derive(Debug, Clone, ValueEnum, PartialEq)]
pub enum ExportFormat {
    Json,
    Markdown,
}