flake-edit 0.3.6

Edit your flake inputs with ease.
Documentation
use clap::{Parser, Subcommand, ValueEnum};

#[derive(Parser, Debug)]
#[command(author, version = CliArgs::unstable_version(), about, long_about)]
#[command(name = "flake-edit")]
#[command(next_line_help = true)]
/// Edit your flake inputs with ease
pub struct CliArgs {
    /// Path to `flake.nix`, or a directory containing `flake.nix`.
    /// Defaults to `flake.nix` in the current directory.
    #[arg(long)]
    flake: Option<String>,
    /// Location of the `flake.lock` file.
    /// Defaults to `flake.lock` in the current directory.
    #[arg(long)]
    lock_file: Option<String>,
    /// Print a diff of the changes instead of writing them to disk.
    #[arg(long, default_value_t = false)]
    diff: bool,
    /// Skip updating the lockfile after editing flake.nix.
    #[arg(long, default_value_t = false)]
    no_lock: bool,
    /// Disable interactive prompts.
    #[arg(long, default_value_t = false)]
    non_interactive: bool,
    /// Disable reading from and writing to the completion cache.
    #[arg(long, default_value_t = false)]
    no_cache: bool,
    /// Path to a custom cache file.
    #[arg(long)]
    cache: Option<String>,
    /// Path to a custom configuration file.
    #[arg(long, global = true)]
    config: Option<String>,

    #[command(subcommand)]
    subcommand: Command,
}

impl CliArgs {
    /// Version string with embedded git revision and date, when available.
    fn unstable_version() -> &'static str {
        const VERSION: &str = env!("CARGO_PKG_VERSION");
        let date = option_env!("GIT_DATE").unwrap_or("no_date");
        let rev = option_env!("GIT_REV").unwrap_or("no_rev");
        // Leaks per call. Only invoked once at startup.
        Box::leak(format!("{VERSION} - {date} - {rev}").into_boxed_str())
    }

    pub fn subcommand(&self) -> &Command {
        &self.subcommand
    }

    pub fn flake(&self) -> Option<&String> {
        self.flake.as_ref()
    }

    pub fn lock_file(&self) -> Option<&String> {
        self.lock_file.as_ref()
    }

    pub fn diff(&self) -> bool {
        self.diff
    }

    pub fn no_lock(&self) -> bool {
        self.no_lock
    }

    pub fn non_interactive(&self) -> bool {
        self.non_interactive
    }

    pub fn no_cache(&self) -> bool {
        self.no_cache
    }

    pub fn cache(&self) -> Option<&String> {
        self.cache.as_ref()
    }

    pub fn config(&self) -> Option<&String> {
        self.config.as_ref()
    }
}

#[derive(Subcommand, Debug)]
pub enum Command {
    /// Add a new flake reference.
    #[clap(alias = "a")]
    Add {
        /// The name of an input attribute.
        id: Option<String>,
        /// The uri that should be added to the input.
        uri: Option<String>,
        #[arg(long)]
        /// Pin to a specific ref_or_rev
        ref_or_rev: Option<String>,
        /// The input itself is not a flake.
        #[arg(long, short)]
        no_flake: bool,
        /// Use shallow clone for the input.
        #[arg(long, short)]
        shallow: bool,
    },
    /// Remove a specific flake reference based on its id.
    #[clap(alias = "rm")]
    Remove { id: Option<String> },
    /// Change an existing flake reference's URI.
    #[clap(alias = "c")]
    Change {
        /// The name of an existing input attribute.
        id: Option<String>,
        /// The new URI for the input.
        uri: Option<String>,
        #[arg(long)]
        /// Pin to a specific ref_or_rev
        ref_or_rev: Option<String>,
        /// Use shallow clone for the input.
        #[arg(long, short)]
        shallow: bool,
    },
    /// List flake inputs
    #[clap(alias = "l")]
    List {
        #[arg(long, value_enum, default_value_t = ListFormat::default())]
        format: ListFormat,
    },
    /// Update inputs to their latest specified release.
    #[clap(alias = "u")]
    Update {
        /// The id of an input attribute.
        /// If omitted will update all inputs.
        id: Option<String>,
        /// Whether the latest semver release of the remote should be used even thought the release
        /// itself isn't yet pinned to a specific release.
        #[arg(long)]
        init: bool,
    },
    /// Pin inputs to their current or a specified rev.
    #[clap(alias = "p")]
    Pin {
        /// The id of an input attribute.
        id: Option<String>,
        /// Optionally specify a rev for the inputs attribute.
        rev: Option<String>,
    },
    /// Unpin an input so it tracks the upstream default again.
    #[clap(alias = "up")]
    Unpin {
        /// The id of an input attribute.
        id: Option<String>,
    },
    /// Automatically add and remove follows declarations.
    ///
    /// Analyzes the flake.lock to find nested inputs that match top-level inputs,
    /// then adds appropriate follows declarations and removes stale ones.
    ///
    /// With file paths, processes multiple flakes in batch.
    /// For every `flake.nix` file passed in it will assume a
    /// `flake.lock` file exists in the same directory.
    #[clap(alias = "f")]
    Follow {
        /// Enable transitive follows deduplication, promoting shared nested
        /// inputs to top-level when they appear at least N times.
        /// Defaults to 2 if no value is given. Overrides the config file's
        /// `follow.transitive_min`.
        #[arg(long, num_args = 0..=1, default_missing_value = "2")]
        transitive: Option<usize>,
        /// Maximum depth of follows declarations to write.
        /// Omitting the flag writes follows at every depth the lockfile
        /// graph supports. `--depth N` caps emission: 1 writes only
        /// `parent.child.follows`, 2 also writes
        /// `parent.child.grandchild.follows`, and so on. Overrides the
        /// config file's `follow.max_depth`.
        #[arg(long)]
        depth: Option<usize>,
        /// Flake.nix paths to process. If empty, runs on current directory.
        #[arg(trailing_var_arg = true, num_args = 0..)]
        paths: Vec<std::path::PathBuf>,
    },
    /// Manually add a single follows declaration.
    ///
    /// Example: `flake-edit add-follow rust-overlay.nixpkgs nixpkgs`
    ///
    /// This creates: `rust-overlay.inputs.nixpkgs.follows = "nixpkgs";`
    ///
    /// Without arguments, starts an interactive selection.
    #[clap(alias = "af")]
    AddFollow {
        /// The input path in dot notation (e.g., "rust-overlay.nixpkgs" means
        /// the nixpkgs input of rust-overlay).
        input: Option<String>,
        /// The target input to follow (e.g., "nixpkgs").
        target: Option<String>,
    },
    #[clap(hide = true)]
    #[command(name = "completion")]
    /// Meant for shell completions.
    Completion {
        #[arg(long)]
        inputs: bool,
        #[arg(value_enum)]
        mode: CompletionMode,
    },
    /// Manage flake-edit configuration.
    #[clap(alias = "cfg", arg_required_else_help = true)]
    Config {
        /// Output the default configuration to stdout.
        #[arg(long)]
        print_default: bool,
        /// Show where configuration would be loaded from.
        #[arg(long)]
        path: bool,
    },
}

/// Which subcommand to complete.
#[derive(Debug, Clone, ValueEnum)]
pub enum CompletionMode {
    Add,
    Change,
    Follow,
}

/// Output format for the `list` subcommand.
#[derive(Debug, Clone, Default, ValueEnum)]
pub enum ListFormat {
    Simple,
    Toplevel,
    #[default]
    Detailed,
    Json,
}