mod render;
mod setup;
mod tui;
use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use costroid_core::{GroupBy, NowOptions, Period, TrendsOptions};
use costroid_providers::HostEnv;
use render::{
detect_render_options, render_frontier, render_now, render_statusline, render_trends,
RenderMode,
};
#[derive(Debug, Parser)]
#[command(name = "costroid", version, about = "Local AI coding cost visibility")]
struct Cli {
#[arg(long, global = true, help = "Render plain ASCII output without color")]
plain: bool,
#[arg(long, global = true, help = "Refresh the selected view in place")]
live: bool,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
enum Command {
Trends(TrendsArgs),
Frontier,
Statusline(StatuslineArgs),
SetupStatusline(SetupStatuslineArgs),
Export(ExportArgs),
}
#[derive(Debug, Parser)]
struct StatuslineArgs {
#[arg(long, conflicts_with = "wrap")]
capture_only: bool,
#[arg(long, value_name = "COMMAND")]
wrap: Option<String>,
}
#[derive(Debug, Parser)]
struct SetupStatuslineArgs {
#[arg(long)]
undo: bool,
}
#[derive(Debug, Parser)]
struct TrendsArgs {
#[arg(long, value_enum, default_value_t = PeriodArg::Week)]
period: PeriodArg,
#[arg(long, value_enum, default_value_t = GroupArg::Model)]
group: GroupArg,
}
#[derive(Debug, Parser)]
struct ExportArgs {
#[arg(long, value_enum, default_value_t = ExportFormat::Json)]
format: ExportFormat,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum PeriodArg {
Day,
Week,
Month,
Year,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum GroupArg {
Model,
App,
Total,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum ExportFormat {
Json,
Csv,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let render_options = detect_render_options(cli.plain);
match &cli.command {
Some(Command::Trends(args)) => {
if render_options.mode == RenderMode::Plain {
run_trends(args, render_options)?;
} else {
tui::run(
tui::StartScreen::Trends,
args.period.into(),
args.group.into(),
cli.live,
render_options,
)?;
}
}
Some(Command::Frontier) => {
if render_options.mode == RenderMode::Plain {
run_frontier(render_options)?;
} else {
tui::run(
tui::StartScreen::Frontier,
Period::Week,
GroupBy::Model,
cli.live,
render_options,
)?;
}
}
Some(Command::Statusline(args)) => {
run_statusline(args, render_options)?;
}
Some(Command::SetupStatusline(args)) => {
let env = HostEnv::detect();
setup::run_setup_statusline(&env, args.undo)?;
}
Some(Command::Export(args)) => {
run_export(args.format)?;
}
None => {
if render_options.mode == RenderMode::Plain {
run_now(render_options)?;
} else {
tui::run(
tui::StartScreen::Now,
Period::Week,
GroupBy::Model,
cli.live,
render_options,
)?;
}
}
}
Ok(())
}
fn run_now(render_options: render::RenderOptions) -> Result<()> {
let env = HostEnv::detect();
let snapshot = costroid_core::collect_local_snapshot(&env)?;
let summary = costroid_core::now_summary(&snapshot, NowOptions::default());
print!("{}", render_now(&summary, render_options));
Ok(())
}
fn run_trends(args: &TrendsArgs, render_options: render::RenderOptions) -> Result<()> {
let env = HostEnv::detect();
let snapshot = costroid_core::collect_local_snapshot(&env)?;
let summary = costroid_core::trends_summary(
&snapshot,
TrendsOptions {
period: args.period.into(),
group_by: args.group.into(),
},
);
print!("{}", render_trends(&summary, render_options));
Ok(())
}
fn run_frontier(render_options: render::RenderOptions) -> Result<()> {
let env = HostEnv::detect();
let snapshot = costroid_core::collect_local_snapshot(&env)?;
let view = costroid_core::bench_view(&snapshot)?;
print!("{}", render_frontier(&view, render_options));
Ok(())
}
fn run_statusline(args: &StatuslineArgs, render_options: render::RenderOptions) -> Result<()> {
if args.capture_only {
setup::capture_from_bytes(&setup::read_stdin());
return Ok(());
}
if let Some(command) = &args.wrap {
return setup::run_wrap(command);
}
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
setup::capture_from_bytes(&setup::read_stdin());
}
let env = HostEnv::detect();
let snapshot = costroid_core::collect_local_snapshot(&env)?;
let summary = costroid_core::now_summary(&snapshot, NowOptions::default());
print!("{}", render_statusline(&summary, render_options));
Ok(())
}
fn run_export(format: ExportFormat) -> Result<()> {
let env = HostEnv::detect();
let rows = costroid_core::focus_records_from_local_logs(&env)?;
let output = match format {
ExportFormat::Json => costroid_core::export_focus_json(rows)?,
ExportFormat::Csv => costroid_core::export_focus_csv(&rows)?,
};
print!("{output}");
Ok(())
}
impl From<PeriodArg> for Period {
fn from(value: PeriodArg) -> Self {
match value {
PeriodArg::Day => Self::Day,
PeriodArg::Week => Self::Week,
PeriodArg::Month => Self::Month,
PeriodArg::Year => Self::Year,
}
}
}
impl From<GroupArg> for GroupBy {
fn from(value: GroupArg) -> Self {
match value {
GroupArg::Model => Self::Model,
GroupArg::App => Self::App,
GroupArg::Total => Self::Total,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_shape_is_valid() {
Cli::command().debug_assert();
}
}