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}