1use anyhow::Result;
6use clap::Subcommand;
7
8#[derive(Subcommand)]
9pub enum CostCommands {
10 Summary,
12 User {
14 name: Option<String>,
16 },
17 Job {
19 id: String,
21 },
22 Report {
24 #[arg(long)]
26 month: Option<String>,
27 },
28 Budget {
30 #[arg(long)]
32 set: Option<u64>,
33 },
34}
35
36pub async fn run(cmd: CostCommands) -> Result<()> {
37 let jobs_db = crate::experiments::tracker::zernel_dir()
38 .join("jobs")
39 .join("jobs.db");
40
41 match cmd {
42 CostCommands::Summary => {
43 println!("Zernel GPU Cost Summary");
44 println!("{}", "=".repeat(50));
45
46 if !jobs_db.exists() {
47 println!("No job data. Submit jobs with: zernel job submit <script>");
48 return Ok(());
49 }
50
51 let conn = rusqlite::Connection::open(&jobs_db)?;
52 let mut stmt = conn.prepare(
53 "SELECT COUNT(*), SUM(CASE WHEN status='done' THEN 1 ELSE 0 END), SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END), SUM(gpus_per_node * nodes) FROM jobs",
54 )?;
55
56 let (total, done, failed, total_gpus): (u32, u32, u32, Option<u32>) = stmt
57 .query_row([], |row| {
58 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
59 })?;
60
61 println!(" Total jobs: {total}");
62 println!(" Completed: {done}");
63 println!(" Failed: {failed}");
64 println!(" Total GPU-jobs: {}", total_gpus.unwrap_or(0));
65
66 let exp_db = crate::experiments::tracker::experiments_db_path();
68 if exp_db.exists() {
69 let exp_conn = rusqlite::Connection::open(&exp_db)?;
70 let total_secs: f64 = exp_conn
71 .query_row(
72 "SELECT COALESCE(SUM(duration_secs), 0) FROM experiments",
73 [],
74 |row| row.get(0),
75 )
76 .unwrap_or(0.0);
77 let gpu_hours = total_secs / 3600.0;
78 println!(
79 " Total GPU-hours: {gpu_hours:.1}h (estimated from experiment durations)"
80 );
81 }
82 }
83
84 CostCommands::User { name } => {
85 println!("GPU cost by user");
86 if let Some(n) = name {
87 println!(" Filtering for: {n}");
88 }
89 println!(" (per-user tracking requires auth — coming in enterprise edition)");
90 }
91
92 CostCommands::Job { id } => {
93 if !jobs_db.exists() {
94 println!("No job data.");
95 return Ok(());
96 }
97
98 let conn = rusqlite::Connection::open(&jobs_db)?;
99 let result: Result<(String, u32, u32, String), _> = conn.query_row(
100 "SELECT script, gpus_per_node, nodes, submitted_at FROM jobs WHERE id = ?1",
101 [&id],
102 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
103 );
104
105 match result {
106 Ok((script, gpus, nodes, submitted)) => {
107 println!("Job: {id}");
108 println!(" Script: {script}");
109 println!(" GPUs: {gpus} x {nodes} nodes = {} total", gpus * nodes);
110 println!(" Submitted: {submitted}");
111 }
112 Err(_) => println!("Job not found: {id}"),
113 }
114 }
115
116 CostCommands::Report { month } => {
117 let m = month.unwrap_or_else(|| "all time".into());
118 println!("GPU Cost Report — {m}");
119 println!();
120 println!("Run: zernel cost summary — for current totals");
121 println!("(Detailed reports coming in enterprise edition)");
122 }
123
124 CostCommands::Budget { set } => {
125 if let Some(hours) = set {
126 let budget_file = crate::experiments::tracker::zernel_dir().join("gpu-budget.txt");
127 std::fs::write(&budget_file, hours.to_string())?;
128 println!("GPU-hour budget set to: {hours}h");
129 println!("Alerts will fire when 80% of budget is consumed.");
130 } else {
131 let budget_file = crate::experiments::tracker::zernel_dir().join("gpu-budget.txt");
132 if budget_file.exists() {
133 let budget = std::fs::read_to_string(&budget_file)?;
134 println!("Current budget: {budget}h");
135 } else {
136 println!("No budget set. Set one: zernel cost budget --set 10000");
137 }
138 }
139 }
140 }
141 Ok(())
142}