Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/cockpit-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ edition = "2021"
authors = ["jlcodes"]
license = "CC-BY-NC-SA-4.0"

# `cockpit quota --json` is the public command. Keep `cockpit-cli` as a
# compatibility alias for older consumers.
[[bin]]
name = "cockpit"
path = "src/main.rs"

[[bin]]
name = "cockpit-cli"
path = "src/main.rs"

[dependencies]
cockpit-core = { path = "../cockpit-core" }
clap = { version = "4.5", features = ["derive", "env"] }
Expand Down
107 changes: 96 additions & 11 deletions crates/cockpit-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
use clap::{Parser, Subcommand};
use cockpit_core::modules::capacity_snapshot::{self, RouteCapacity};
use cockpit_core::modules::{cursor_account, github_copilot_account};
use colored::*;
use tabled::{Table, Tabled};

#[derive(Parser)]
#[command(author, version, about = "Cockpit Tools CLI", long_about = None)]
#[command(
name = "cockpit",
bin_name = "cockpit",
author,
version,
about = "Cockpit Tools CLI",
long_about = None
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
Expand All @@ -24,10 +32,13 @@ enum Commands {
/// The account ID or email to switch to
account: String,
},
/// Show current quota for a platform
/// Show current quota as a sanitized capacity snapshot
Quota {
/// The platform (cursor, copilot)
platform: String,
/// Optional platform filter (antigravity, codex). Omit for all.
platform: Option<String>,
/// Output machine-readable sanitized capacity snapshot JSON
#[arg(long)]
json: bool,
},
}

Expand Down Expand Up @@ -96,13 +107,39 @@ async fn main() -> anyhow::Result<()> {
}
_ => println!("{} Unknown platform: {}", "Error:".red(), platform),
},
Some(Commands::Quota { platform }) => match platform.to_lowercase().as_str() {
_ => println!(
"{} Quota command not yet implemented for {}",
"Info:".yellow(),
platform
),
},
Some(Commands::Quota { platform, json }) => {
let mut snapshot = capacity_snapshot::build_capacity_snapshot();
match platform.as_deref().map(str::to_lowercase).as_deref() {
None | Some("all") => {}
Some(filter) => {
let supported = ["antigravity", "codex"];
if !supported.contains(&filter) {
if json {
snapshot.routes.clear();
snapshot.availability =
capacity_snapshot::SnapshotAvailability::Unavailable;
snapshot.sources.clear();
println!("{}", serde_json::to_string_pretty(&snapshot)?);
} else {
println!(
"{} Unknown or unsupported platform: {} (supported: {})",
"Error:".red(),
filter,
supported.join(", ")
);
}
return Ok(());
}
snapshot.routes.retain(|r| r.provider == filter);
}
};

if json {
println!("{}", serde_json::to_string_pretty(&snapshot)?);
} else {
display_routes(&snapshot.routes);
}
}
None => {
println!("Welcome to Cockpit CLI! Use --help for commands.");
}
Expand All @@ -118,3 +155,51 @@ fn display_accounts(accounts: Vec<AccountDisplay>) {
println!("{}", Table::new(accounts).to_string());
}
}

#[derive(Tabled)]
struct RouteDisplay {
#[tabled(rename = "Route")]
route_id: String,
#[tabled(rename = "Provider")]
provider: String,
#[tabled(rename = "Plan")]
plan: String,
#[tabled(rename = "Health")]
health: String,
#[tabled(rename = "Min remaining")]
min_remaining: String,
#[tabled(rename = "Updated")]
updated_at: String,
}

fn display_routes(routes: &[RouteCapacity]) {
if routes.is_empty() {
println!("No capacity routes found.");
return;
}
let rows: Vec<RouteDisplay> = routes
.iter()
.map(|r| RouteDisplay {
route_id: r.route_id.clone(),
provider: r.provider.clone(),
plan: r.plan.clone().unwrap_or_else(|| "-".to_string()),
health: format!("{:?}", r.health.status).to_lowercase(),
min_remaining: r
.quota_windows
.iter()
.map(|w| w.remaining_ratio)
.fold(None::<f64>, |acc, v| match acc {
Some(cur) => Some(cur.min(v)),
None => Some(v),
})
.map(|v| format!("{}%", (v * 100.0).round() as i64))
.unwrap_or_else(|| "-".to_string()),
updated_at: r
.updated_at
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
.map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
.unwrap_or_else(|| "-".to_string()),
})
.collect();
println!("{}", Table::new(rows).to_string());
}
Loading
Loading