use anyhow::Result;
use clap::Subcommand;
use colored::Colorize;
use serde_json::json;
use tabled::{Table, Tabled};
use crate::api::{resolve_team_id, LinearClient};
#[derive(Subcommand)]
pub enum CycleCommands {
#[command(alias = "ls")]
List {
#[arg(short, long)]
team: String,
#[arg(short, long)]
all: bool,
},
Current {
#[arg(short, long)]
team: String,
},
}
#[derive(Tabled)]
struct CycleRow {
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Number")]
number: String,
#[tabled(rename = "Status")]
status: String,
#[tabled(rename = "Start Date")]
start_date: String,
#[tabled(rename = "End Date")]
end_date: String,
#[tabled(rename = "Progress")]
progress: String,
#[tabled(rename = "ID")]
id: String,
}
pub async fn handle(cmd: CycleCommands) -> Result<()> {
match cmd {
CycleCommands::List { team, all } => list_cycles(&team, all).await,
CycleCommands::Current { team } => current_cycle(&team).await,
}
}
async fn list_cycles(team: &str, include_all: bool) -> Result<()> {
let client = LinearClient::new()?;
let team_id = resolve_team_id(&client, team).await?;
let team_query = r#"
query($teamId: String!) {
team(id: $teamId) {
id
name
cycles(first: 50) {
nodes {
id
name
number
startsAt
endsAt
completedAt
progress
}
}
}
}
"#;
let result = client.query(team_query, Some(json!({ "teamId": team_id }))).await?;
let team_data = &result["data"]["team"];
if team_data.is_null() {
anyhow::bail!("Team not found: {}", team);
}
let team_name = team_data["name"].as_str().unwrap_or("");
let empty = vec![];
let cycles = team_data["cycles"]["nodes"]
.as_array()
.unwrap_or(&empty);
if cycles.is_empty() {
println!("No cycles found for team '{}'.", team_name);
return Ok(());
}
let rows: Vec<CycleRow> = cycles
.iter()
.filter(|c| {
if include_all {
true
} else {
c["completedAt"].is_null()
}
})
.map(|c| {
let progress = c["progress"].as_f64().unwrap_or(0.0);
let status = if !c["completedAt"].is_null() {
"Completed".to_string()
} else {
"Active".to_string()
};
CycleRow {
name: c["name"].as_str().unwrap_or("-").to_string(),
number: c["number"].as_i64().map(|n| n.to_string()).unwrap_or("-".to_string()),
status,
start_date: c["startsAt"]
.as_str()
.map(|s| s.chars().take(10).collect())
.unwrap_or("-".to_string()),
end_date: c["endsAt"]
.as_str()
.map(|s| s.chars().take(10).collect())
.unwrap_or("-".to_string()),
progress: format!("{:.0}%", progress * 100.0),
id: c["id"].as_str().unwrap_or("").to_string(),
}
})
.collect();
if rows.is_empty() {
println!("No active cycles found for team '{}'. Use --all to see completed cycles.", team_name);
return Ok(());
}
println!("{}", format!("Cycles for team '{}'", team_name).bold());
println!("{}", "-".repeat(40));
let table = Table::new(rows).to_string();
println!("{}", table);
println!("\n{} cycles shown", cycles.len());
Ok(())
}
async fn current_cycle(team: &str) -> Result<()> {
let client = LinearClient::new()?;
let team_id = resolve_team_id(&client, team).await?;
let query = r#"
query($teamId: String!) {
team(id: $teamId) {
id
name
activeCycle {
id
name
number
startsAt
endsAt
progress
issues(first: 50) {
nodes {
id
identifier
title
state { name type }
}
}
}
}
}
"#;
let result = client.query(query, Some(json!({ "teamId": team_id }))).await?;
let team_data = &result["data"]["team"];
if team_data.is_null() {
anyhow::bail!("Team not found: {}", team);
}
let team_name = team_data["name"].as_str().unwrap_or("");
let cycle = &team_data["activeCycle"];
if cycle.is_null() {
println!("No active cycle for team '{}'.", team_name);
return Ok(());
}
let progress = cycle["progress"].as_f64().unwrap_or(0.0);
let cycle_number = cycle["number"].as_i64().unwrap_or(0);
let default_name = format!("Cycle {}", cycle_number);
let cycle_name = cycle["name"].as_str().unwrap_or(&default_name);
println!("{}", format!("Current Cycle: {}", cycle_name).bold());
println!("{}", "-".repeat(40));
println!("Team: {}", team_name);
println!("Cycle Number: {}", cycle_number);
println!("Start Date: {}", cycle["startsAt"].as_str().map(|s| &s[..10]).unwrap_or("-"));
println!("End Date: {}", cycle["endsAt"].as_str().map(|s| &s[..10]).unwrap_or("-"));
println!("Progress: {:.0}%", progress * 100.0);
println!("ID: {}", cycle["id"].as_str().unwrap_or("-"));
let issues = cycle["issues"]["nodes"].as_array();
if let Some(issues) = issues {
if !issues.is_empty() {
println!("\n{}", "Issues in this cycle:".bold());
for issue in issues {
let identifier = issue["identifier"].as_str().unwrap_or("");
let title = issue["title"].as_str().unwrap_or("");
let state = issue["state"]["name"].as_str().unwrap_or("");
let state_type = issue["state"]["type"].as_str().unwrap_or("");
let state_colored = match state_type {
"completed" => state.green().to_string(),
"started" => state.yellow().to_string(),
"canceled" | "cancelled" => state.red().to_string(),
_ => state.dimmed().to_string(),
};
println!(" {} {} [{}]", identifier.cyan(), title, state_colored);
}
}
}
Ok(())
}