socket_patch_cli/args.rs
1//! Shared CLI arguments flattened into every subcommand.
2//!
3//! `GlobalArgs` defines the flags that apply uniformly across every
4//! `socket-patch` subcommand. Each subcommand `#[command(flatten)]`s this
5//! struct into its own `Args` struct so the surface stays consistent.
6//!
7//! Subcommands that don't actually use a given global flag still accept it
8//! silently (no-op). See `CLI_CONTRACT.md` for the full contract.
9//!
10//! Precedence for every flag: CLI arg > env var > default.
11//!
12//! All env-var names use the `SOCKET_*` prefix. Three legacy `SOCKET_PATCH_*`
13//! names are still read at runtime (via `socket_patch_core::env_compat`) with
14//! a one-shot deprecation warning; they will be removed in the next major.
15
16use std::path::PathBuf;
17
18use clap::Args;
19
20use socket_patch_core::api::client::ApiClientEnvOverrides;
21use socket_patch_core::constants::{
22 DEFAULT_PATCH_API_PROXY_URL, DEFAULT_PATCH_MANIFEST_PATH, DEFAULT_SOCKET_API_URL,
23};
24
25/// Arguments inherited by every subcommand via `#[command(flatten)]`.
26///
27/// **Every** global flag is parseable on **every** subcommand. Commands that
28/// don't use a given flag ignore it silently — e.g. `list --global` parses
29/// fine and the `global` field is unused at runtime.
30#[derive(Args, Debug, Clone)]
31pub struct GlobalArgs {
32 /// Working directory.
33 #[arg(long, env = "SOCKET_CWD", default_value = ".")]
34 pub cwd: PathBuf,
35
36 /// Path to patch manifest file (resolved relative to --cwd).
37 #[arg(
38 long = "manifest-path",
39 env = "SOCKET_MANIFEST_PATH",
40 default_value = DEFAULT_PATCH_MANIFEST_PATH,
41 )]
42 pub manifest_path: String,
43
44 /// Socket API URL (authenticated endpoint).
45 #[arg(
46 long = "api-url",
47 env = "SOCKET_API_URL",
48 default_value = DEFAULT_SOCKET_API_URL,
49 )]
50 pub api_url: String,
51
52 /// Socket API token. Absence selects the public patch proxy.
53 #[arg(long = "api-token", env = "SOCKET_API_TOKEN")]
54 pub api_token: Option<String>,
55
56 /// Organization slug. Auto-resolved when omitted and a token is set.
57 #[arg(long = "org", short = 'o', env = "SOCKET_ORG_SLUG")]
58 pub org: Option<String>,
59
60 /// Public proxy URL used when no API token is set.
61 #[arg(
62 long = "proxy-url",
63 env = "SOCKET_PROXY_URL",
64 default_value = DEFAULT_PATCH_API_PROXY_URL,
65 )]
66 pub proxy_url: String,
67
68 /// Restrict to these ecosystems (comma-separated).
69 #[arg(
70 long = "ecosystems",
71 short = 'e',
72 env = "SOCKET_ECOSYSTEMS",
73 value_delimiter = ',',
74 )]
75 pub ecosystems: Option<Vec<String>>,
76
77 /// Which kind of patch artifact to download when local files are missing.
78 /// `diff` (default) fetches the smallest delta archive; `package` fetches
79 /// a full per-package tarball; `file` falls back to legacy per-file blobs.
80 #[arg(
81 long = "download-mode",
82 env = "SOCKET_DOWNLOAD_MODE",
83 default_value = "diff",
84 )]
85 pub download_mode: String,
86
87 /// Strict airgap: never contact the network. Operations that need remote
88 /// data fail loudly when this is set.
89 #[arg(
90 long,
91 env = "SOCKET_OFFLINE",
92 default_value_t = false,
93 value_parser = clap::builder::BoolishValueParser::new(),
94 )]
95 pub offline: bool,
96
97 /// Operate on globally-installed packages.
98 #[arg(
99 long = "global",
100 short = 'g',
101 env = "SOCKET_GLOBAL",
102 default_value_t = false,
103 value_parser = clap::builder::BoolishValueParser::new(),
104 )]
105 pub global: bool,
106
107 /// Override the path used to discover globally-installed packages.
108 #[arg(long = "global-prefix", env = "SOCKET_GLOBAL_PREFIX")]
109 pub global_prefix: Option<PathBuf>,
110
111 /// Emit machine-readable JSON output.
112 #[arg(
113 long = "json",
114 short = 'j',
115 env = "SOCKET_JSON",
116 default_value_t = false,
117 value_parser = clap::builder::BoolishValueParser::new(),
118 )]
119 pub json: bool,
120
121 /// Show extra detail in human-readable output.
122 #[arg(
123 long = "verbose",
124 short = 'v',
125 env = "SOCKET_VERBOSE",
126 default_value_t = false,
127 value_parser = clap::builder::BoolishValueParser::new(),
128 )]
129 pub verbose: bool,
130
131 /// Suppress non-error output.
132 #[arg(
133 long = "silent",
134 short = 's',
135 env = "SOCKET_SILENT",
136 default_value_t = false,
137 value_parser = clap::builder::BoolishValueParser::new(),
138 )]
139 pub silent: bool,
140
141 /// Preview the operation without making any mutations.
142 #[arg(
143 long = "dry-run",
144 env = "SOCKET_DRY_RUN",
145 default_value_t = false,
146 value_parser = clap::builder::BoolishValueParser::new(),
147 )]
148 pub dry_run: bool,
149
150 /// Skip interactive prompts.
151 #[arg(
152 long = "yes",
153 short = 'y',
154 env = "SOCKET_YES",
155 default_value_t = false,
156 value_parser = clap::builder::BoolishValueParser::new(),
157 )]
158 pub yes: bool,
159
160 /// Seconds to wait for `<.socket>/apply.lock` before giving up.
161 /// Default (`None`) and `0` both mean a single non-blocking try
162 /// — failing immediately if another process holds the lock. A
163 /// positive value retries with a 100 ms backoff until the lock
164 /// frees or the budget elapses. Only meaningful for the mutating
165 /// subcommands (`apply`, `rollback`, `repair`, `remove`); other
166 /// commands accept it silently.
167 #[arg(long = "lock-timeout", env = "SOCKET_LOCK_TIMEOUT")]
168 pub lock_timeout: Option<u64>,
169
170 /// Force-remove `<.socket>/apply.lock` before attempting
171 /// acquisition. Use when you are certain no other socket-patch
172 /// process is running (e.g. a previous run crashed in a way that
173 /// stripped the OS lock but left the file). Emits a
174 /// `lock_broken` warning event in the JSON envelope so the
175 /// action is auditable. Only meaningful for mutating
176 /// subcommands; other commands accept it silently.
177 #[arg(
178 long = "break-lock",
179 env = "SOCKET_BREAK_LOCK",
180 default_value_t = false,
181 value_parser = clap::builder::BoolishValueParser::new(),
182 )]
183 pub break_lock: bool,
184
185 /// Emit verbose debug logs to stderr.
186 #[arg(
187 long = "debug",
188 env = "SOCKET_DEBUG",
189 default_value_t = false,
190 value_parser = clap::builder::BoolishValueParser::new(),
191 )]
192 pub debug: bool,
193
194 /// Disable anonymous usage telemetry.
195 #[arg(
196 long = "no-telemetry",
197 env = "SOCKET_TELEMETRY_DISABLED",
198 default_value_t = false,
199 value_parser = clap::builder::BoolishValueParser::new(),
200 )]
201 pub no_telemetry: bool,
202}
203
204impl GlobalArgs {
205 /// Resolve `manifest_path` against `cwd`. See
206 /// `socket_patch_core::manifest::operations::resolve_manifest_path`.
207 pub fn resolved_manifest_path(&self) -> PathBuf {
208 socket_patch_core::manifest::operations::resolve_manifest_path(
209 &self.cwd,
210 &self.manifest_path,
211 )
212 }
213
214 /// Build [`ApiClientEnvOverrides`] from the CLI flags.
215 ///
216 /// `api_token` and `org` are forwarded as `Some(_)` only when set.
217 /// `api_url` and `proxy_url` are forwarded only when non-empty;
218 /// `GlobalArgs::default()` leaves both empty so integration tests
219 /// that mutate env vars *after* constructing args still get env-var
220 /// resolution from `get_api_client_with_overrides`. In production
221 /// clap always populates them with either the CLI value, the env
222 /// value, or the clap-declared default — all non-empty — so the
223 /// resolved value still flows through.
224 pub fn api_client_overrides(&self) -> ApiClientEnvOverrides {
225 ApiClientEnvOverrides {
226 api_url: Some(self.api_url.clone()).filter(|s| !s.is_empty()),
227 api_token: self.api_token.clone().filter(|s| !s.is_empty()),
228 org_slug: self.org.clone().filter(|s| !s.is_empty()),
229 proxy_url: Some(self.proxy_url.clone()).filter(|s| !s.is_empty()),
230 }
231 }
232}
233
234/// Apply CLI-flag toggles for env-driven knobs by mirroring them into env
235/// vars. This is how `--debug` / `--no-telemetry` reach core code that
236/// reads `SOCKET_DEBUG` / `SOCKET_TELEMETRY_DISABLED` directly. Idempotent
237/// and a no-op when the flags are off.
238pub fn apply_env_toggles(common: &GlobalArgs) {
239 if common.debug {
240 std::env::set_var("SOCKET_DEBUG", "1");
241 }
242 if common.no_telemetry {
243 std::env::set_var("SOCKET_TELEMETRY_DISABLED", "1");
244 }
245}
246
247impl Default for GlobalArgs {
248 /// Defaults intended for **test struct literals** (e.g. `..GlobalArgs::default()`).
249 ///
250 /// In production every field is populated by clap (with the
251 /// `default_value = ".."` attribute providing the documented defaults
252 /// when neither CLI flag nor env var is set), so this `Default` is
253 /// only reached from tests building `GlobalArgs` directly.
254 ///
255 /// `api_url` and `proxy_url` are intentionally **empty** here (not
256 /// the production default URLs). That lets tests set
257 /// `SOCKET_API_URL` / `SOCKET_PROXY_URL` via `std::env::set_var`
258 /// *after* constructing the args struct and have those env vars
259 /// flow through to the API client — `api_client_overrides` skips
260 /// empty values so the underlying `get_api_client_with_overrides`
261 /// falls back to env-var resolution.
262 fn default() -> Self {
263 Self {
264 cwd: PathBuf::from("."),
265 manifest_path: DEFAULT_PATCH_MANIFEST_PATH.to_string(),
266 api_url: String::new(),
267 api_token: None,
268 org: None,
269 proxy_url: String::new(),
270 ecosystems: None,
271 download_mode: "diff".to_string(),
272 offline: false,
273 global: false,
274 global_prefix: None,
275 json: false,
276 verbose: false,
277 silent: false,
278 dry_run: false,
279 yes: false,
280 lock_timeout: None,
281 break_lock: false,
282 debug: false,
283 no_telemetry: false,
284 }
285 }
286}