use std::fs;
use std::path::Path;
use std::process::Command;
use chrono::{DateTime, Utc};
use owo_colors::OwoColorize;
use serde::Deserialize;
use netsky_core::consts::{AGENT0_NAME, AGENTINFINITY_NAME, TICKER_LOG_PATH, TICKER_SESSION};
use netsky_core::paths::home;
use netsky_sh::tmux;
const GITHUB_REPO: &str = "lostmygithubaccount/netsky";
const MAIN_BRANCH: &str = "main";
#[derive(Debug, Deserialize)]
struct GhRun {
conclusion: Option<String>,
#[serde(rename = "createdAt")]
created_at: String,
#[serde(rename = "headSha")]
head_sha: String,
status: String,
url: String,
}
pub fn run() -> netsky_core::Result<()> {
println!(
"version: {}",
format!("netsky {}", env!("CARGO_PKG_VERSION")).bold()
);
println!("tmux: {}", render_tmux_sessions());
println!("last tick: {}", render_last_tick());
println!("main CI: {}", render_ci_status());
println!("meta.db: {}", render_db_size());
Ok(())
}
fn render_tmux_sessions() -> String {
let mut sessions: Vec<String> = tmux::list_sessions()
.into_iter()
.filter(|s| is_status_session(s))
.collect();
sessions.sort();
if sessions.is_empty() {
return "none".yellow().to_string();
}
let total = sessions.len();
if sessions.len() > 8 {
let tail = sessions.len() - 8;
sessions.truncate(8);
sessions.push(format!("+{tail} more"));
}
format!(
"{} active: {}",
total.to_string().green(),
sessions.join(", ")
)
}
fn is_status_session(name: &str) -> bool {
name == AGENT0_NAME
|| name == AGENTINFINITY_NAME
|| name == TICKER_SESSION
|| (name.starts_with("agent")
&& name.strip_prefix("agent").is_some_and(|suffix| {
!suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit())
}))
}
fn render_last_tick() -> String {
let log = Path::new(TICKER_LOG_PATH);
let modified = match fs::metadata(log).and_then(|m| m.modified()) {
Ok(ts) => ts,
Err(_) => return "none".yellow().to_string(),
};
let dt: DateTime<Utc> = modified.into();
dt.format("%Y-%m-%dT%H:%M:%SZ")
.to_string()
.green()
.to_string()
}
fn render_ci_status() -> String {
match latest_main_run() {
Ok(Some(run)) => render_ci_run(run),
Ok(None) => "pending".yellow().to_string(),
Err(_) => "pending (offline)".yellow().to_string(),
}
}
fn render_ci_run(run: GhRun) -> String {
let created = DateTime::parse_from_rfc3339(&run.created_at)
.map(|dt| dt.with_timezone(&Utc))
.ok();
let age = created
.map(|dt| Utc::now().signed_duration_since(dt))
.unwrap_or_else(|| chrono::Duration::hours(25));
let sha = short_sha(&run.head_sha);
match (run.status.as_str(), run.conclusion.as_deref()) {
("completed", Some("success")) => format!("green {sha} {}", run.url).green().to_string(),
("completed", Some("failure" | "cancelled" | "timed_out")) => {
format!("red {sha} {}", run.url).red().to_string()
}
("completed", _) => format!("pending {sha} {}", run.url).yellow().to_string(),
_ => {
let stale = if age.num_hours() >= 24 { " stale" } else { "" };
format!("pending {sha}{stale} {}", run.url)
.yellow()
.to_string()
}
}
}
fn latest_main_run() -> netsky_core::Result<Option<GhRun>> {
let output = match Command::new("gh")
.args([
"run",
"list",
"--repo",
GITHUB_REPO,
"--branch",
MAIN_BRANCH,
"--limit",
"1",
"--json",
"conclusion,headSha,createdAt,status,url",
])
.output()
{
Ok(o) if o.status.success() => o,
Ok(_) => return Ok(None),
Err(_) => return Ok(None),
};
let runs: Vec<GhRun> = serde_json::from_slice(&output.stdout)?;
Ok(runs.into_iter().next())
}
fn render_db_size() -> String {
let path = home().join(".netsky/meta.db");
let Ok(meta) = fs::metadata(&path) else {
return "missing".yellow().to_string();
};
human_bytes(meta.len()).green().to_string()
}
fn human_bytes(bytes: u64) -> String {
const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
let mut value = bytes as f64;
let mut unit = 0usize;
while value >= 1024.0 && unit + 1 < UNITS.len() {
value /= 1024.0;
unit += 1;
}
format!("{value:.1} {}", UNITS[unit])
}
fn short_sha(sha: &str) -> String {
sha.chars().take(7).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognises_status_sessions() {
assert!(is_status_session("agent0"));
assert!(is_status_session("agent13"));
assert!(is_status_session("agentinfinity"));
assert!(is_status_session("netsky-ticker"));
assert!(!is_status_session("agentx"));
}
#[test]
fn human_bytes_scales() {
assert_eq!(human_bytes(999), "999.0 B");
assert_eq!(human_bytes(1024), "1.0 KB");
assert_eq!(human_bytes(1024 * 1024), "1.0 MB");
}
}