Skip to main content

fuelcheck_cli/
commands.rs

1use anyhow::{Result, anyhow};
2use fuelcheck_core::config::{Config, DetectResult};
3use fuelcheck_core::model::{OutputFormat, ProviderErrorPayload, ProviderPayload};
4use fuelcheck_core::providers::{ProviderRegistry, ProviderSelector};
5use fuelcheck_core::service::{
6    CostRequest, SetupRequest, UsageRequest, build_cost_report_collection, build_setup_config,
7    collect_cost_outputs, collect_report_provider_ids, collect_usage_outputs,
8};
9use fuelcheck_ui::reports as ui_reports;
10use fuelcheck_ui::text::{RenderOptions as TextRenderOptions, render_outputs};
11use fuelcheck_ui::tui::{self, UsageArgs as WatchUsageArgs};
12
13use crate::args::{
14    ConfigArgs, ConfigCommand, ConfigCommandArgs, CostArgs, GlobalArgs, SetupArgs, UsageArgs,
15};
16use crate::logger::{self, LogLevel};
17
18pub struct OutputPreferences {
19    pub format: OutputFormat,
20    pub pretty: bool,
21    pub json_only: bool,
22    pub no_color: bool,
23}
24
25impl OutputPreferences {
26    pub fn uses_json_output(&self) -> bool {
27        self.json_only || self.format == OutputFormat::Json
28    }
29
30    pub fn use_color(&self) -> bool {
31        if self.format == OutputFormat::Json {
32            return false;
33        }
34        if self.no_color {
35            return false;
36        }
37        if std::env::var("NO_COLOR").is_ok() {
38            return false;
39        }
40        std::io::stdout().is_terminal()
41    }
42}
43
44use std::io::IsTerminal;
45
46pub async fn run_usage(
47    args: UsageArgs,
48    registry: &ProviderRegistry,
49    global: &GlobalArgs,
50) -> Result<()> {
51    let config = Config::load(args.config.as_ref())?;
52    if let Ok(path) = Config::path(args.config.as_ref()) {
53        logger::log(
54            LogLevel::Info,
55            "config_loaded",
56            "Loaded config",
57            Some(
58                serde_json::json!({ "path": path.display().to_string(), "missing": !path.exists() }),
59            ),
60        );
61    }
62
63    let format = if args.json || global.json_only {
64        OutputFormat::Json
65    } else {
66        args.format.into()
67    };
68
69    if args.watch {
70        if format == OutputFormat::Json || global.json_only {
71            return Err(anyhow!("--watch only supports text output"));
72        }
73
74        let watch_args = WatchUsageArgs {
75            providers: args.providers.into_iter().map(Into::into).collect(),
76            source: args.source.into(),
77            status: args.status,
78            no_credits: args.no_credits,
79            refresh: args.refresh,
80            web_debug_dump_html: args.web_debug_dump_html,
81            web_timeout: args.web_timeout,
82            account: args.account,
83            account_index: args.account_index,
84            all_accounts: args.all_accounts,
85            antigravity_plan_debug: args.antigravity_plan_debug,
86            interval: args.interval,
87        };
88        return tui::run_usage_watch(watch_args, registry, config).await;
89    }
90
91    let request = UsageRequest {
92        providers: args.providers.into_iter().map(Into::into).collect(),
93        source: args.source.into(),
94        status: args.status,
95        no_credits: args.no_credits,
96        refresh: args.refresh,
97        web_debug_dump_html: args.web_debug_dump_html,
98        web_timeout: args.web_timeout,
99        account: args.account,
100        account_index: args.account_index,
101        all_accounts: args.all_accounts,
102        antigravity_plan_debug: args.antigravity_plan_debug,
103    };
104
105    let outputs = collect_usage_outputs(&request, &config, registry).await?;
106    let prefs = OutputPreferences {
107        format,
108        pretty: args.pretty,
109        json_only: global.json_only,
110        no_color: global.no_color,
111    };
112    print_outputs(&outputs, &prefs)
113}
114
115pub async fn run_cost(
116    args: CostArgs,
117    registry: &ProviderRegistry,
118    global: &GlobalArgs,
119) -> Result<()> {
120    let config = Config::load(args.config.as_ref())?;
121
122    let format = if args.json || global.json_only {
123        OutputFormat::Json
124    } else {
125        args.format.into()
126    };
127
128    if let Some(report_kind) = args.report {
129        let providers = collect_report_provider_ids(
130            &args
131                .providers
132                .iter()
133                .copied()
134                .map(Into::into)
135                .collect::<Vec<ProviderSelector>>(),
136        );
137        let report_collection = build_cost_report_collection(
138            report_kind.into(),
139            providers,
140            args.since.as_deref(),
141            args.until.as_deref(),
142            args.timezone.as_deref(),
143        )?;
144
145        if format == OutputFormat::Json || global.json_only {
146            let value = fuelcheck_core::reports::collection_to_json_value(&report_collection)?;
147            if args.pretty {
148                println!("{}", serde_json::to_string_pretty(&value)?);
149            } else {
150                println!("{}", serde_json::to_string(&value)?);
151            }
152            return Ok(());
153        }
154
155        if !global.json_only {
156            println!(
157                "{}",
158                ui_reports::render_collection_text(
159                    &report_collection,
160                    args.compact,
161                    args.timezone.as_deref()
162                )
163            );
164        }
165        return Ok(());
166    }
167
168    let request = CostRequest {
169        providers: args.providers.into_iter().map(Into::into).collect(),
170    };
171    let outputs = collect_cost_outputs(&request, &config, registry).await?;
172
173    let prefs = OutputPreferences {
174        format,
175        pretty: args.pretty,
176        json_only: global.json_only,
177        no_color: global.no_color,
178    };
179    print_outputs(&outputs, &prefs)
180}
181
182pub async fn run_config(cmd: ConfigCommandArgs, global: &GlobalArgs) -> Result<()> {
183    let mut command = cmd.command;
184    if global.json_only {
185        match &mut command {
186            ConfigCommand::Validate(args) => args.format = Some(crate::args::OutputFormatArg::Json),
187            ConfigCommand::Dump(args) => args.format = Some(crate::args::OutputFormatArg::Json),
188        }
189    }
190
191    match command {
192        ConfigCommand::Validate(args) => validate_config(args),
193        ConfigCommand::Dump(args) => dump_config(args),
194    }
195}
196
197pub async fn run_setup(args: SetupArgs) -> Result<()> {
198    let config_path = Config::path(args.config.as_ref())?;
199    if config_path.exists() && !args.force {
200        return Err(anyhow!(
201            "Config already exists at {}. Use --force to overwrite.",
202            config_path.display()
203        ));
204    }
205
206    let detected = DetectResult::detect();
207    let config = build_setup_config(
208        &SetupRequest {
209            enable_all: args.enable_all,
210            claude_cookie: args.claude_cookie.clone(),
211            cursor_cookie: args.cursor_cookie.clone(),
212            factory_cookie: args.factory_cookie.clone(),
213        },
214        &detected,
215    );
216    config.save(args.config.as_ref())?;
217
218    println!(
219        "Setup complete. Config written to {}",
220        config_path.display()
221    );
222    if !detected.codex_auth {
223        println!("Codex: run `codex` to authenticate (creates ~/.codex/auth.json).");
224    }
225    if !detected.claude_oauth && args.claude_cookie.is_none() {
226        println!("Claude: run `claude` to authenticate (creates ~/.claude/.credentials.json).");
227        println!(
228            "Claude: or provide a session cookie via `fuelcheck-cli setup --claude-cookie \"sessionKey=...\"`."
229        );
230    }
231    if !detected.gemini_oauth {
232        println!("Gemini: run `gemini` to authenticate (creates ~/.gemini/oauth_creds.json).");
233    }
234    if args.cursor_cookie.is_none() {
235        println!("Cursor: add cookie header via `fuelcheck-cli setup --cursor-cookie \"...\"`.");
236    }
237    if args.factory_cookie.is_none() {
238        println!(
239            "Factory (Droid): add cookie header via `fuelcheck-cli setup --factory-cookie \"...\"`."
240        );
241    }
242
243    Ok(())
244}
245
246fn validate_config(args: ConfigArgs) -> Result<()> {
247    let path = Config::path(args.config.as_ref())?;
248    let missing = !path.exists();
249    let _config = Config::load(args.config.as_ref())?;
250    match args.format.map(Into::into).unwrap_or(OutputFormat::Text) {
251        OutputFormat::Json => {
252            let output = if missing {
253                serde_json::json!({
254                    "status": "ok",
255                    "missing": true,
256                    "path": path.display().to_string()
257                })
258            } else {
259                serde_json::json!({"status": "ok"})
260            };
261            if args.pretty {
262                println!("{}", serde_json::to_string_pretty(&output)?);
263            } else {
264                println!("{}", serde_json::to_string(&output)?);
265            }
266        }
267        OutputFormat::Text => {
268            if missing {
269                println!("config ok (missing; using defaults): {}", path.display());
270            } else {
271                println!("config ok: {}", path.display());
272            }
273        }
274    }
275
276    Ok(())
277}
278
279fn dump_config(args: ConfigArgs) -> Result<()> {
280    let config = Config::load(args.config.as_ref())?;
281    match args.format.map(Into::into).unwrap_or(OutputFormat::Json) {
282        OutputFormat::Json => {
283            if args.pretty {
284                println!("{}", serde_json::to_string_pretty(&config)?);
285            } else {
286                println!("{}", serde_json::to_string(&config)?);
287            }
288        }
289        OutputFormat::Text => {
290            println!("{}", serde_json::to_string_pretty(&config)?);
291        }
292    }
293
294    Ok(())
295}
296
297fn print_outputs(outputs: &[ProviderPayload], prefs: &OutputPreferences) -> Result<()> {
298    let rendered = render_outputs(
299        outputs,
300        &TextRenderOptions {
301            format: prefs.format,
302            pretty: prefs.pretty,
303            json_only: prefs.json_only,
304            use_color: prefs.use_color(),
305        },
306    )?;
307
308    if let Some(text) = rendered {
309        println!("{}", text);
310    }
311
312    Ok(())
313}
314
315pub fn cli_error_payload(
316    code: i32,
317    message: String,
318    kind: fuelcheck_core::model::ErrorKind,
319) -> ProviderPayload {
320    ProviderPayload::error(
321        "cli".to_string(),
322        "cli".to_string(),
323        ProviderErrorPayload {
324            code,
325            message,
326            kind: Some(kind),
327        },
328    )
329}