1use 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#[derive(Args, Debug, Clone)]
31pub struct GlobalArgs {
32 #[arg(long, env = "SOCKET_CWD", default_value = ".")]
34 pub cwd: PathBuf,
35
36 #[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 #[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 #[arg(long = "api-token", env = "SOCKET_API_TOKEN")]
54 pub api_token: Option<String>,
55
56 #[arg(long = "org", short = 'o', env = "SOCKET_ORG_SLUG")]
58 pub org: Option<String>,
59
60 #[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 #[arg(
70 long = "ecosystems",
71 short = 'e',
72 env = "SOCKET_ECOSYSTEMS",
73 value_delimiter = ',',
74 )]
75 pub ecosystems: Option<Vec<String>>,
76
77 #[arg(
81 long = "download-mode",
82 env = "SOCKET_DOWNLOAD_MODE",
83 default_value = "diff",
84 )]
85 pub download_mode: String,
86
87 #[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 #[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 #[arg(long = "global-prefix", env = "SOCKET_GLOBAL_PREFIX")]
109 pub global_prefix: Option<PathBuf>,
110
111 #[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 #[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 #[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 #[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 #[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 #[arg(long = "lock-timeout", env = "SOCKET_LOCK_TIMEOUT")]
168 pub lock_timeout: Option<u64>,
169
170 #[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 #[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 #[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 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 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
234pub 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 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}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
294 fn api_client_overrides_forwards_set_values() {
295 let args = GlobalArgs {
296 api_url: "https://api.example.com".to_string(),
297 api_token: Some("tok123".to_string()),
298 org: Some("acme".to_string()),
299 proxy_url: "https://proxy.example.com".to_string(),
300 ..GlobalArgs::default()
301 };
302 let o = args.api_client_overrides();
303 assert_eq!(o.api_url.as_deref(), Some("https://api.example.com"));
304 assert_eq!(o.api_token.as_deref(), Some("tok123"));
305 assert_eq!(o.org_slug.as_deref(), Some("acme"));
306 assert_eq!(o.proxy_url.as_deref(), Some("https://proxy.example.com"));
307 }
308
309 #[test]
314 fn api_client_overrides_default_is_all_none() {
315 let o = GlobalArgs::default().api_client_overrides();
316 assert!(o.api_url.is_none(), "empty api_url must not be forwarded");
317 assert!(o.proxy_url.is_none(), "empty proxy_url must not be forwarded");
318 assert!(o.api_token.is_none());
319 assert!(o.org_slug.is_none());
320 }
321
322 #[test]
325 fn api_client_overrides_filters_empty_strings() {
326 let args = GlobalArgs {
327 api_url: String::new(),
328 api_token: Some(String::new()),
329 org: Some(String::new()),
330 proxy_url: String::new(),
331 ..GlobalArgs::default()
332 };
333 let o = args.api_client_overrides();
334 assert!(o.api_url.is_none());
335 assert!(o.api_token.is_none());
336 assert!(o.org_slug.is_none());
337 assert!(o.proxy_url.is_none());
338 }
339
340 #[test]
342 fn resolved_manifest_path_joins_relative_against_cwd() {
343 let args = GlobalArgs {
344 cwd: PathBuf::from("/work/project"),
345 manifest_path: ".socket/manifest.json".to_string(),
346 ..GlobalArgs::default()
347 };
348 assert_eq!(
349 args.resolved_manifest_path(),
350 PathBuf::from("/work/project/.socket/manifest.json"),
351 );
352 }
353
354 #[test]
356 fn resolved_manifest_path_passes_absolute_through() {
357 let args = GlobalArgs {
358 cwd: PathBuf::from("/work/project"),
359 manifest_path: "/etc/socket/manifest.json".to_string(),
360 ..GlobalArgs::default()
361 };
362 assert_eq!(
363 args.resolved_manifest_path(),
364 PathBuf::from("/etc/socket/manifest.json"),
365 );
366 }
367
368 #[test]
372 #[serial_test::serial]
373 fn apply_env_toggles_mirrors_flags_into_env() {
374 let saved_debug = std::env::var("SOCKET_DEBUG").ok();
375 let saved_telemetry = std::env::var("SOCKET_TELEMETRY_DISABLED").ok();
376 std::env::remove_var("SOCKET_DEBUG");
377 std::env::remove_var("SOCKET_TELEMETRY_DISABLED");
378
379 apply_env_toggles(&GlobalArgs::default());
381 assert!(std::env::var("SOCKET_DEBUG").is_err());
382 assert!(std::env::var("SOCKET_TELEMETRY_DISABLED").is_err());
383
384 let args = GlobalArgs {
386 debug: true,
387 no_telemetry: true,
388 ..GlobalArgs::default()
389 };
390 apply_env_toggles(&args);
391 assert_eq!(std::env::var("SOCKET_DEBUG").as_deref(), Ok("1"));
392 assert_eq!(std::env::var("SOCKET_TELEMETRY_DISABLED").as_deref(), Ok("1"));
393
394 match saved_debug {
395 Some(v) => std::env::set_var("SOCKET_DEBUG", v),
396 None => std::env::remove_var("SOCKET_DEBUG"),
397 }
398 match saved_telemetry {
399 Some(v) => std::env::set_var("SOCKET_TELEMETRY_DISABLED", v),
400 None => std::env::remove_var("SOCKET_TELEMETRY_DISABLED"),
401 }
402 }
403}