diff --git a/crates/lineark/src/commands/issues.rs b/crates/lineark/src/commands/issues.rs index 55c9ae2..966a083 100644 --- a/crates/lineark/src/commands/issues.rs +++ b/crates/lineark/src/commands/issues.rs @@ -90,6 +90,9 @@ pub enum IssuesAction { /// Priority: 0=none, 1=urgent, 2=high, 3=medium, 4=low. #[arg(short = 'p', long, value_parser = clap::value_parser!(i64).range(0..=4))] priority: Option, + /// Estimate points (valid values depend on the team's estimation scale). + #[arg(short = 'e', long)] + estimate: Option, /// Issue description (markdown). #[arg(short = 'd', long)] description: Option, @@ -149,6 +152,9 @@ pub enum IssuesAction { /// Priority: 0=none, 1=urgent, 2=high, 3=medium, 4=low. #[arg(short = 'p', long, value_parser = clap::value_parser!(i64).range(0..=4))] priority: Option, + /// Estimate points (valid values depend on the team's estimation scale). + #[arg(short = 'e', long)] + estimate: Option, /// Comma-separated label names or UUIDs. Behavior depends on --label-by. #[arg(long, value_delimiter = ',')] labels: Option>, @@ -316,6 +322,7 @@ pub struct IssueDetail { pub description: Option, pub priority: Option, pub priority_label: Option, + pub estimate: Option, pub url: Option, pub created_at: Option, pub updated_at: Option, @@ -551,6 +558,7 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res assignee, labels, priority, + estimate, description, parent, status, @@ -595,6 +603,7 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res assignee_id, label_ids, priority, + estimate, description, parent_id, state_id, @@ -648,6 +657,7 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res identifier, status, priority, + estimate, labels, label_by, clear_labels, @@ -661,6 +671,7 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res } => { if status.is_none() && priority.is_none() + && estimate.is_none() && labels.is_none() && !clear_labels && assignee.is_none() @@ -672,7 +683,7 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res && cycle.is_none() { return Err(anyhow::anyhow!( - "No update fields provided. Use --status, --priority, --assignee, --labels, --title, --description, --parent, --project, or --cycle to specify changes." + "No update fields provided. Use --status, --priority, --estimate, --assignee, --labels, --title, --description, --parent, --project, or --cycle to specify changes." )); } @@ -744,6 +755,7 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res description, assignee_id, priority, + estimate, state_id, parent_id, label_ids, diff --git a/crates/lineark/src/commands/usage.rs b/crates/lineark/src/commands/usage.rs index 40109d5..047b4e8 100644 --- a/crates/lineark/src/commands/usage.rs +++ b/crates/lineark/src/commands/usage.rs @@ -62,12 +62,13 @@ COMMANDS: [--team KEY] [--assignee NAME-OR-ID|me] Filter by team, assignee, status [--status NAME,...] [--show-done] Comma-separated status names lineark issues create --team KEY Create an issue - [-p 0-4] [--assignee NAME-OR-ID|me] 0=none 1=urgent 2=high 3=medium 4=low + [-p 0-4] [-e N] [--assignee NAME-OR-ID|me] 0=none 1=urgent 2=high 3=medium 4=low [--labels NAME,...] [-d TEXT] [-s NAME] Label names (team-scoped), status name [--parent ID] [--project NAME-OR-ID] Parent issue, project, cycle [--cycle NAME-OR-ID] lineark issues update <IDENTIFIER> Update an issue - [-s NAME] [-p 0-4] [--assignee NAME-OR-ID|me] Status, priority, assignee + [-s NAME] [-p 0-4] [-e N] Status, priority, estimate + [--assignee NAME-OR-ID|me] Assignee [--labels NAME,...] [--label-by adding|replacing|removing] [--clear-labels] [-t TEXT] [-d TEXT] Title, description [--parent ID] [--clear-parent] Set or remove parent diff --git a/crates/lineark/tests/offline.rs b/crates/lineark/tests/offline.rs index d916b73..4fe0253 100644 --- a/crates/lineark/tests/offline.rs +++ b/crates/lineark/tests/offline.rs @@ -907,3 +907,34 @@ fn usage_includes_issues_list_project_filter() { .success() .stdout(predicate::str::contains("--project")); } + +// ── Estimate flag ─────────────────────────────────────────────────────────── + +#[test] +fn issues_create_help_shows_estimate() { + lineark() + .args(["issues", "create", "--help"]) + .assert() + .success() + .stdout(predicate::str::contains("--estimate")) + .stdout(predicate::str::contains("-e")); +} + +#[test] +fn issues_update_help_shows_estimate() { + lineark() + .args(["issues", "update", "--help"]) + .assert() + .success() + .stdout(predicate::str::contains("--estimate")) + .stdout(predicate::str::contains("-e")); +} + +#[test] +fn issues_update_no_flags_error_mentions_estimate() { + lineark() + .args(["--api-token", "fake-token", "issues", "update", "ENG-123"]) + .assert() + .failure() + .stderr(predicate::str::contains("--estimate")); +} diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index f06f897..3f38d75 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -3592,4 +3592,86 @@ mod online { }) .expect("issues list --project should return at least one issue"); } + + // ── Issues create with --estimate ─────────────────────────────────────── + + #[test_with::runtime_ignore_if(no_online_test_token)] + fn issues_create_with_estimate() { + let token = api_token(); + let unique_name = format!( + "[test] CLI estimate flag {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); + + // Get a team key. + let output = lineark() + .args(["--api-token", &token, "--format", "json", "teams", "list"]) + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "teams list should succeed.\nstdout: {stdout}\nstderr: {stderr}" + ); + let teams: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let team_key = teams[0]["key"].as_str().unwrap().to_string(); + + // Create an issue with --estimate. + let output = lineark() + .args([ + "--api-token", + &token, + "--format", + "json", + "issues", + "create", + &unique_name, + "--team", + &team_key, + "--priority", + "4", + "--estimate", + "3", + ]) + .output() + .expect("failed to execute lineark"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "issues create --estimate should succeed.\nstdout: {stdout}\nstderr: {stderr}" + ); + let created: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let issue_id = created["id"] + .as_str() + .expect("created issue should have id") + .to_string(); + let _issue_guard = IssueGuard { + token: token.clone(), + id: issue_id.clone(), + }; + + // Update the issue with a different estimate. + let output = lineark() + .args([ + "--api-token", + &token, + "--format", + "json", + "issues", + "update", + &issue_id, + "--estimate", + "5", + ]) + .output() + .expect("failed to execute lineark"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "issues update --estimate should succeed.\nstdout: {stdout}\nstderr: {stderr}" + ); + } }