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}