Skip to main content

heddle_cli_args/cli/cli_args/
cli_base.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Base CLI flags.
3//!
4//! ## Short-flag conventions
5//!
6//! The CLI is small and the short forms have to mean the same thing
7//! everywhere they appear. The table below is the source of truth — new
8//! verbs should reuse these letters before claiming new ones. Verbs that
9//! diverge (e.g. `-n` for `--steps` on `undo`/`redo` vs `--limit` on
10//! `log`/`list`) keep the muscle memory consistent within the verb's own
11//! family: "n" is always "how many," "m" is always "message."
12//!
13//! | Short | Long(s)                           | Used by                       |
14//! |-------|-----------------------------------|-------------------------------|
15//! | `-m`  | `--message`, `--intent`           | capture, revert, context,     |
16//! |       |                                   | discuss                       |
17//! | `-n`  | `--limit` (queries),              | log, list, query (limit);     |
18//! |       | `--steps` (undo/redo)             | undo, redo (steps)            |
19//! | `-f`  | `--force`                         | capture, push, revert, purge  |
20//! | `-s`  | `--short`                         | status                        |
21//! | `-U`  | `--unified`                       | diff                          |
22//! | `-C`  | `--repo`                          | global                        |
23//! | `-v`  | `--verbose` (repeatable)          | global                        |
24//! | `-q`  | `--quiet`                         | global                        |
25//!
26//! Heddle is pre-1.0 and contracts misleading surface area instead of
27//! preserving obsolete spellings. Add a short alias only when the letter is
28//! already reserved for that semantic in the current table above.
29
30use std::sync::OnceLock;
31
32use clap::Parser;
33use repo::{Config, OutputFormat};
34
35use super::{CliOutputMode, Commands};
36
37/// Heddle: An AI-native version control system.
38#[derive(Parser)]
39#[command(name = "heddle")]
40#[command(author, version, about, long_about = None)]
41// We ship our own `Help` subcommand (curated everyday/advanced
42// surface + topic pages). clap's auto-generated `help` subcommand
43// would shadow it; turn it off so `heddle help [topic]` reaches our
44// printer instead.
45#[command(disable_help_subcommand = true)]
46pub struct Cli {
47    #[command(subcommand)]
48    pub command: Commands,
49
50    // This is a `global = true` arg, so clap stamps its help onto every
51    // subcommand's --help. Keep it to ONE line; the full contract
52    // (json vs json-compact fields, no-TTY-autodetect guarantee) lives in
53    // `heddle help output-formats` (help.rs OUTPUT_FORMATS_TOPIC) and the
54    // top-level `heddle help` Output paragraph, stated exactly once each
55    // (heddle#652).
56    /// Output format: `text` (default), `json`, or `json-compact`. See `heddle help output-formats`
57    #[arg(long, global = true, value_enum)]
58    pub output: Option<CliOutputMode>,
59
60    /// Disable colored output.
61    #[arg(long, global = true)]
62    pub no_color: bool,
63
64    /// Repository path (default: find .heddle in ancestors).
65    #[arg(short = 'C', long, global = true, value_name = "PATH")]
66    pub repo: Option<std::path::PathBuf>,
67
68    /// Increase verbosity.
69    #[arg(short, long, global = true, action = clap::ArgAction::Count)]
70    pub verbose: u8,
71
72    /// Decrease verbosity.
73    #[arg(short, long, global = true)]
74    pub quiet: bool,
75
76    // Global like --output, and revealed on every `supports_op_id`
77    // command's help — keep it to one line (heddle#652). Replay semantics
78    // (`supports_op_id` advertisement, same-body replay, typed conflicts)
79    // live in `heddle help operation-ids`. Hidden from default `--help` to
80    // keep the human surface uncluttered.
81    /// Operation id (UUID v4) for idempotent retries. See `heddle help operation-ids`
82    #[arg(long, global = true, env = "HEDDLE_OPERATION_ID", hide = true)]
83    pub op_id: Option<String>,
84}
85
86impl Cli {
87    /// Load and cache the process-wide user configuration.
88    pub fn user_config_or_exit() -> &'static cli_shared::UserConfig {
89        static USER_CONFIG: OnceLock<cli_shared::UserConfig> = OnceLock::new();
90        USER_CONFIG.get_or_init(|| cli_shared::UserConfig::load_default().unwrap_or_default())
91    }
92
93    pub fn output_mode(&self) -> Option<cli_shared::OutputMode> {
94        self.output.map(Into::into)
95    }
96
97    /// Open the Heddle repository the command should act on: the `--repo`
98    /// path if given, otherwise the current working directory (resolved
99    /// lazily so a supplied `--repo` never touches the cwd).
100    pub fn open_repo(&self) -> anyhow::Result<repo::Repository> {
101        use anyhow::Context as _;
102        let cwd;
103        let repo_path = match self.repo.as_ref() {
104            Some(path) => path,
105            None => {
106                cwd = std::env::current_dir().context("get current working directory")?;
107                &cwd
108            }
109        };
110        repo::Repository::open(repo_path).context("open Heddle repository")
111    }
112}
113
114impl weft_client_shim::CliContext for Cli {
115    fn repo_path(&self) -> Option<&std::path::Path> {
116        self.repo.as_deref()
117    }
118
119    fn operation_id_wire(&self) -> String {
120        self.op_id.clone().unwrap_or_default()
121    }
122
123    fn should_output_json(&self, repo_config: Option<&Config>) -> bool {
124        should_output_json(self, repo_config)
125    }
126}
127
128/// Resolve whether command output should use JSON after applying user,
129/// repository, and explicit CLI output settings.
130pub fn should_output_json(cli: &Cli, repo_config: Option<&Config>) -> bool {
131    let mut format = repo_config
132        .and_then(|config| config.output.format)
133        .unwrap_or(Cli::user_config_or_exit().output.format);
134
135    if let Some(output) = cli.output_mode() {
136        format = match output {
137            cli_shared::OutputMode::Json | cli_shared::OutputMode::JsonCompact => {
138                OutputFormat::Json
139            }
140            cli_shared::OutputMode::Text => OutputFormat::Text,
141        };
142    }
143
144    matches!(format, OutputFormat::Json)
145}