From df70e110cc15a3dacd08d4756731474381b6bbf7 Mon Sep 17 00:00:00 2001 From: 23prime <23.prime.37@gmail.com> Date: Thu, 12 Mar 2026 19:27:00 +0900 Subject: [PATCH 1/7] fix: add serde(default) to optional User fields to handle missing keys --- src/api/user.rs | 111 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 16 deletions(-) diff --git a/src/api/user.rs b/src/api/user.rs index 593a824..5dee8f5 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,5 +1,6 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use super::BacklogClient; @@ -7,10 +8,16 @@ use super::BacklogClient; #[serde(rename_all = "camelCase")] pub struct User { pub id: u64, - pub user_id: String, + pub user_id: Option, pub name: String, - pub mail_address: String, + pub mail_address: Option, pub role_type: u8, + #[serde(default)] + pub lang: Option, + #[serde(default)] + pub last_login_time: Option, + #[serde(flatten)] + pub extra: BTreeMap, } impl BacklogClient { @@ -19,6 +26,18 @@ impl BacklogClient { serde_json::from_value(value) .map_err(|e| anyhow::anyhow!("Failed to deserialize user response: {}", e)) } + + pub fn get_users(&self) -> Result> { + let value = self.get("/users")?; + serde_json::from_value(value) + .map_err(|e| anyhow::anyhow!("Failed to deserialize users response: {}", e)) + } + + pub fn get_user(&self, user_id: u64) -> Result { + let value = self.get(&format!("/users/{user_id}"))?; + serde_json::from_value(value) + .map_err(|e| anyhow::anyhow!("Failed to deserialize user response: {}", e)) + } } #[cfg(test)] @@ -33,7 +52,9 @@ mod tests { "userId": "john", "name": "John Doe", "mailAddress": "john@example.com", - "roleType": 1 + "roleType": 1, + "lang": "ja", + "lastLoginTime": "2024-01-01T00:00:00Z" }) } @@ -48,7 +69,7 @@ mod tests { let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap(); let user = client.get_myself().unwrap(); assert_eq!(user.id, 123); - assert_eq!(user.user_id, "john"); + assert_eq!(user.user_id.as_deref(), Some("john")); assert_eq!(user.name, "John Doe"); } @@ -67,25 +88,83 @@ mod tests { } #[test] - fn deserialize_user() { - let v = json!({ - "id": 123, - "userId": "john", - "name": "John Doe", - "mailAddress": "john@example.com", - "roleType": 1 + fn get_users_returns_list() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/users"); + then.status(200).json_body(json!([user_json()])); + }); + + let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap(); + let users = client.get_users().unwrap(); + assert_eq!(users.len(), 1); + assert_eq!(users[0].id, 123); + } + + #[test] + fn get_users_returns_error_on_api_failure() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/users"); + then.status(403) + .json_body(json!({"errors": [{"message": "Forbidden"}]})); + }); + + let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap(); + let err = client.get_users().unwrap_err(); + assert!(err.to_string().contains("Forbidden")); + } + + #[test] + fn get_user_returns_parsed_struct() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/users/123"); + then.status(200).json_body(user_json()); }); + + let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap(); + let user = client.get_user(123).unwrap(); + assert_eq!(user.id, 123); + assert_eq!(user.name, "John Doe"); + } + + #[test] + fn get_user_returns_error_on_not_found() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/users/999"); + then.status(404) + .json_body(json!({"errors": [{"message": "No user"}]})); + }); + + let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap(); + let err = client.get_user(999).unwrap_err(); + assert!(err.to_string().contains("No user")); + } + + #[test] + fn deserialize_user() { + let v = user_json(); let user: User = serde_json::from_value(v).unwrap(); assert_eq!(user.id, 123); - assert_eq!(user.user_id, "john"); + assert_eq!(user.user_id.as_deref(), Some("john")); assert_eq!(user.name, "John Doe"); - assert_eq!(user.mail_address, "john@example.com"); + assert_eq!(user.mail_address.as_deref(), Some("john@example.com")); assert_eq!(user.role_type, 1); } #[test] - fn deserialize_user_fails_on_missing_required_field() { - let v = json!({"id": 123, "userId": "john"}); - assert!(serde_json::from_value::(v).is_err()); + fn deserialize_user_with_null_user_id() { + let v = json!({ + "id": 1, + "userId": null, + "name": "Bot", + "mailAddress": null, + "roleType": 2 + }); + let user: User = serde_json::from_value(v).unwrap(); + assert_eq!(user.user_id, None); + assert_eq!(user.mail_address, None); } } From 5e379122657b96c1f8ed8ecde0186f1ef7faa54c Mon Sep 17 00:00:00 2001 From: 23prime <23.prime.37@gmail.com> Date: Thu, 12 Mar 2026 19:29:13 +0900 Subject: [PATCH 2/7] feat: add bl user list and bl user show commands Add GET /api/v2/users (list all space users) and GET /api/v2/users/:userId (show a single user). Update User struct to handle nullable userId/mailAddress and optional lang/lastLoginTime fields. --- src/api/mod.rs | 10 + src/cmd/auth.rs | 21 ++- src/cmd/issue/attachment/list.rs | 6 + src/cmd/issue/comment/add.rs | 6 + src/cmd/issue/comment/delete.rs | 6 + src/cmd/issue/comment/list.rs | 6 + src/cmd/issue/comment/update.rs | 6 + src/cmd/issue/count.rs | 6 + src/cmd/issue/create.rs | 6 + src/cmd/issue/delete.rs | 6 + src/cmd/issue/list.rs | 6 + src/cmd/issue/show.rs | 6 + src/cmd/issue/update.rs | 6 + src/cmd/mod.rs | 1 + src/cmd/project/activities.rs | 6 + src/cmd/project/category.rs | 6 + src/cmd/project/disk_usage.rs | 6 + src/cmd/project/issue_type.rs | 6 + src/cmd/project/list.rs | 6 + src/cmd/project/show.rs | 6 + src/cmd/project/status.rs | 6 + src/cmd/project/user.rs | 6 + src/cmd/project/version.rs | 6 + src/cmd/space/activities.rs | 6 + src/cmd/space/disk_usage.rs | 6 + src/cmd/space/notification.rs | 6 + src/cmd/space/show.rs | 6 + src/cmd/user/list.rs | 284 +++++++++++++++++++++++++++++ src/cmd/user/mod.rs | 5 + src/cmd/user/show.rs | 304 +++++++++++++++++++++++++++++++ src/cmd/wiki/attachment/list.rs | 6 + src/cmd/wiki/create.rs | 6 + src/cmd/wiki/delete.rs | 6 + src/cmd/wiki/history.rs | 6 + src/cmd/wiki/list.rs | 6 + src/cmd/wiki/show.rs | 6 + src/cmd/wiki/update.rs | 6 + src/main.rs | 28 +++ 38 files changed, 836 insertions(+), 3 deletions(-) create mode 100644 src/cmd/user/list.rs create mode 100644 src/cmd/user/mod.rs create mode 100644 src/cmd/user/show.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index a9f4690..bc401d7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -30,6 +30,8 @@ use wiki::{Wiki, WikiAttachment, WikiHistory, WikiListItem}; pub trait BacklogApi { fn get_space(&self) -> Result; fn get_myself(&self) -> Result; + fn get_users(&self) -> Result>; + fn get_user(&self, user_id: u64) -> Result; fn get_space_activities(&self) -> Result>; fn get_space_disk_usage(&self) -> Result; fn get_space_notification(&self) -> Result; @@ -76,6 +78,14 @@ impl BacklogApi for BacklogClient { self.get_myself() } + fn get_users(&self) -> Result> { + self.get_users() + } + + fn get_user(&self, user_id: u64) -> Result { + self.get_user(user_id) + } + fn get_space_activities(&self) -> Result> { self.get_space_activities() } diff --git a/src/cmd/auth.rs b/src/cmd/auth.rs index 9700cf8..5fe6aad 100644 --- a/src/cmd/auth.rs +++ b/src/cmd/auth.rs @@ -276,7 +276,11 @@ pub fn status_with( } match api.get_myself() { - Ok(user) => println!(" - Logged in as {} ({})", user.name.green(), user.user_id), + Ok(user) => println!( + " - Logged in as {} ({})", + user.name.green(), + user.user_id.as_deref().unwrap_or("-") + ), Err(e) => println!(" {} Token invalid: {}", "!".red(), e), } @@ -376,6 +380,14 @@ mod tests { .ok_or_else(|| anyhow!("invalid credentials")) } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } + fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } @@ -550,10 +562,13 @@ mod tests { fn sample_user() -> User { User { id: 1, - user_id: "john".to_string(), + user_id: Some("john".to_string()), name: "John Doe".to_string(), - mail_address: "john@example.com".to_string(), + mail_address: Some("john@example.com".to_string()), role_type: 1, + lang: None, + last_login_time: None, + extra: std::collections::BTreeMap::new(), } } diff --git a/src/cmd/issue/attachment/list.rs b/src/cmd/issue/attachment/list.rs index 44a6514..b58103a 100644 --- a/src/cmd/issue/attachment/list.rs +++ b/src/cmd/issue/attachment/list.rs @@ -80,6 +80,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/comment/add.rs b/src/cmd/issue/comment/add.rs index d792dd3..1c9c73f 100644 --- a/src/cmd/issue/comment/add.rs +++ b/src/cmd/issue/comment/add.rs @@ -53,6 +53,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/comment/delete.rs b/src/cmd/issue/comment/delete.rs index 68fc1f5..c2fd87e 100644 --- a/src/cmd/issue/comment/delete.rs +++ b/src/cmd/issue/comment/delete.rs @@ -55,6 +55,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/comment/list.rs b/src/cmd/issue/comment/list.rs index 9fce469..11506ee 100644 --- a/src/cmd/issue/comment/list.rs +++ b/src/cmd/issue/comment/list.rs @@ -94,6 +94,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/comment/update.rs b/src/cmd/issue/comment/update.rs index d1aeafa..6cd2b7a 100644 --- a/src/cmd/issue/comment/update.rs +++ b/src/cmd/issue/comment/update.rs @@ -59,6 +59,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/count.rs b/src/cmd/issue/count.rs index 746942f..bad17bb 100644 --- a/src/cmd/issue/count.rs +++ b/src/cmd/issue/count.rs @@ -108,6 +108,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/create.rs b/src/cmd/issue/create.rs index 32547b4..39b49b2 100644 --- a/src/cmd/issue/create.rs +++ b/src/cmd/issue/create.rs @@ -92,6 +92,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/delete.rs b/src/cmd/issue/delete.rs index 65f2e2b..32dff56 100644 --- a/src/cmd/issue/delete.rs +++ b/src/cmd/issue/delete.rs @@ -50,6 +50,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/list.rs b/src/cmd/issue/list.rs index 72f590e..ca98be2 100644 --- a/src/cmd/issue/list.rs +++ b/src/cmd/issue/list.rs @@ -201,6 +201,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/show.rs b/src/cmd/issue/show.rs index 3fff658..70d8cd8 100644 --- a/src/cmd/issue/show.rs +++ b/src/cmd/issue/show.rs @@ -74,6 +74,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/issue/update.rs b/src/cmd/issue/update.rs index ff3141c..e7f1ab0 100644 --- a/src/cmd/issue/update.rs +++ b/src/cmd/issue/update.rs @@ -115,6 +115,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 07b6a1a..b0bbdb0 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -3,4 +3,5 @@ pub mod banner; pub mod issue; pub mod project; pub mod space; +pub mod user; pub mod wiki; diff --git a/src/cmd/project/activities.rs b/src/cmd/project/activities.rs index e1763b4..fed6f19 100644 --- a/src/cmd/project/activities.rs +++ b/src/cmd/project/activities.rs @@ -63,6 +63,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/project/category.rs b/src/cmd/project/category.rs index de0bb4f..5924ca7 100644 --- a/src/cmd/project/category.rs +++ b/src/cmd/project/category.rs @@ -54,6 +54,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/project/disk_usage.rs b/src/cmd/project/disk_usage.rs index 0d157c5..f30909f 100644 --- a/src/cmd/project/disk_usage.rs +++ b/src/cmd/project/disk_usage.rs @@ -61,6 +61,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/project/issue_type.rs b/src/cmd/project/issue_type.rs index acc7d27..9d94b32 100644 --- a/src/cmd/project/issue_type.rs +++ b/src/cmd/project/issue_type.rs @@ -54,6 +54,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/project/list.rs b/src/cmd/project/list.rs index 30f47ac..eb30233 100644 --- a/src/cmd/project/list.rs +++ b/src/cmd/project/list.rs @@ -62,6 +62,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/project/show.rs b/src/cmd/project/show.rs index ea6bf5c..db3ee8e 100644 --- a/src/cmd/project/show.rs +++ b/src/cmd/project/show.rs @@ -56,6 +56,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/project/status.rs b/src/cmd/project/status.rs index 7c66761..8d6bf37 100644 --- a/src/cmd/project/status.rs +++ b/src/cmd/project/status.rs @@ -54,6 +54,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/project/user.rs b/src/cmd/project/user.rs index 36b3d1b..12a30f9 100644 --- a/src/cmd/project/user.rs +++ b/src/cmd/project/user.rs @@ -58,6 +58,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/project/version.rs b/src/cmd/project/version.rs index 87a3e20..8be27cd 100644 --- a/src/cmd/project/version.rs +++ b/src/cmd/project/version.rs @@ -61,6 +61,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/space/activities.rs b/src/cmd/space/activities.rs index 73cda6d..4b562cf 100644 --- a/src/cmd/space/activities.rs +++ b/src/cmd/space/activities.rs @@ -62,6 +62,12 @@ mod tests { fn get_myself(&self) -> Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> Result> { self.activities .clone() diff --git a/src/cmd/space/disk_usage.rs b/src/cmd/space/disk_usage.rs index 86bb534..ec9b2c3 100644 --- a/src/cmd/space/disk_usage.rs +++ b/src/cmd/space/disk_usage.rs @@ -61,6 +61,12 @@ mod tests { fn get_myself(&self) -> Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> Result> { unimplemented!() } diff --git a/src/cmd/space/notification.rs b/src/cmd/space/notification.rs index 0b6afc3..3b8e179 100644 --- a/src/cmd/space/notification.rs +++ b/src/cmd/space/notification.rs @@ -57,6 +57,12 @@ mod tests { fn get_myself(&self) -> Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> Result> { unimplemented!() } diff --git a/src/cmd/space/show.rs b/src/cmd/space/show.rs index 12489e2..477acfb 100644 --- a/src/cmd/space/show.rs +++ b/src/cmd/space/show.rs @@ -60,6 +60,12 @@ mod tests { fn get_myself(&self) -> Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> Result> { unimplemented!() } diff --git a/src/cmd/user/list.rs b/src/cmd/user/list.rs new file mode 100644 index 0000000..682aa23 --- /dev/null +++ b/src/cmd/user/list.rs @@ -0,0 +1,284 @@ +use anstream::println; +use anyhow::{Context, Result}; + +use crate::api::{BacklogApi, BacklogClient, user::User}; + +pub struct UserListArgs { + json: bool, +} + +impl UserListArgs { + pub fn new(json: bool) -> Self { + Self { json } + } +} + +pub fn list(args: &UserListArgs) -> Result<()> { + let client = BacklogClient::from_config()?; + list_with(args, &client) +} + +pub fn list_with(args: &UserListArgs, api: &dyn BacklogApi) -> Result<()> { + let users = api.get_users()?; + if args.json { + println!( + "{}", + serde_json::to_string_pretty(&users).context("Failed to serialize JSON")? + ); + } else { + for u in &users { + println!("{}", format_user_row(u)); + } + } + Ok(()) +} + +fn format_user_row(u: &User) -> String { + match u.user_id.as_deref() { + Some(user_id) if !user_id.is_empty() => format!("[{}] {}", user_id, u.name), + _ => format!("[{}] {}", u.id, u.name), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::anyhow; + use std::collections::BTreeMap; + + struct MockApi { + users: Option>, + } + + impl crate::api::BacklogApi for MockApi { + fn get_space(&self) -> anyhow::Result { + unimplemented!() + } + fn get_myself(&self) -> anyhow::Result { + unimplemented!() + } + fn get_users(&self) -> anyhow::Result> { + self.users.clone().ok_or_else(|| anyhow!("no users")) + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } + fn get_space_activities(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_space_disk_usage(&self) -> anyhow::Result { + unimplemented!() + } + fn get_space_notification( + &self, + ) -> anyhow::Result { + unimplemented!() + } + fn get_projects(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_project(&self, _key: &str) -> anyhow::Result { + unimplemented!() + } + fn get_project_activities( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_disk_usage( + &self, + _key: &str, + ) -> anyhow::Result { + unimplemented!() + } + fn get_project_users( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_statuses( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_issue_types( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_categories( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_versions( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_issues( + &self, + _params: &[(String, String)], + ) -> anyhow::Result> { + unimplemented!() + } + fn count_issues( + &self, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn get_issue(&self, _key: &str) -> anyhow::Result { + unimplemented!() + } + fn create_issue( + &self, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn update_issue( + &self, + _key: &str, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn delete_issue(&self, _key: &str) -> anyhow::Result { + unimplemented!() + } + fn get_issue_comments( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn add_issue_comment( + &self, + _key: &str, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn update_issue_comment( + &self, + _key: &str, + _comment_id: u64, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn delete_issue_comment( + &self, + _key: &str, + _comment_id: u64, + ) -> anyhow::Result { + unimplemented!() + } + fn get_issue_attachments( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_wikis( + &self, + _params: &[(String, String)], + ) -> anyhow::Result> { + unimplemented!() + } + fn get_wiki(&self, _wiki_id: u64) -> anyhow::Result { + unimplemented!() + } + fn create_wiki( + &self, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn update_wiki( + &self, + _wiki_id: u64, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn delete_wiki( + &self, + _wiki_id: u64, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn get_wiki_history( + &self, + _wiki_id: u64, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_wiki_attachments( + &self, + _wiki_id: u64, + ) -> anyhow::Result> { + unimplemented!() + } + } + + fn sample_user() -> User { + User { + id: 1, + user_id: Some("john".to_string()), + name: "John Doe".to_string(), + mail_address: Some("john@example.com".to_string()), + role_type: 1, + lang: None, + last_login_time: None, + extra: BTreeMap::new(), + } + } + + #[test] + fn format_user_row_with_user_id() { + let text = format_user_row(&sample_user()); + assert!(text.contains("[john]")); + assert!(text.contains("John Doe")); + } + + #[test] + fn format_user_row_without_user_id() { + let mut u = sample_user(); + u.user_id = None; + let text = format_user_row(&u); + assert!(text.contains("[1]")); + assert!(text.contains("John Doe")); + } + + #[test] + fn list_with_text_output_succeeds() { + let api = MockApi { + users: Some(vec![sample_user()]), + }; + assert!(list_with(&UserListArgs::new(false), &api).is_ok()); + } + + #[test] + fn list_with_json_output_succeeds() { + let api = MockApi { + users: Some(vec![sample_user()]), + }; + assert!(list_with(&UserListArgs::new(true), &api).is_ok()); + } + + #[test] + fn list_with_propagates_api_error() { + let api = MockApi { users: None }; + let err = list_with(&UserListArgs::new(false), &api).unwrap_err(); + assert!(err.to_string().contains("no users")); + } +} diff --git a/src/cmd/user/mod.rs b/src/cmd/user/mod.rs new file mode 100644 index 0000000..2249afb --- /dev/null +++ b/src/cmd/user/mod.rs @@ -0,0 +1,5 @@ +mod list; +mod show; + +pub use list::{UserListArgs, list}; +pub use show::{UserShowArgs, show}; diff --git a/src/cmd/user/show.rs b/src/cmd/user/show.rs new file mode 100644 index 0000000..32da112 --- /dev/null +++ b/src/cmd/user/show.rs @@ -0,0 +1,304 @@ +use anstream::println; +use anyhow::{Context, Result}; +use owo_colors::OwoColorize; + +use crate::api::{BacklogApi, BacklogClient, user::User}; + +pub struct UserShowArgs { + id: u64, + json: bool, +} + +impl UserShowArgs { + pub fn new(id: u64, json: bool) -> Self { + Self { id, json } + } +} + +pub fn show(args: &UserShowArgs) -> Result<()> { + let client = BacklogClient::from_config()?; + show_with(args, &client) +} + +pub fn show_with(args: &UserShowArgs, api: &dyn BacklogApi) -> Result<()> { + let user = api.get_user(args.id)?; + if args.json { + println!( + "{}", + serde_json::to_string_pretty(&user).context("Failed to serialize JSON")? + ); + } else { + println!("{}", format_user_text(&user)); + } + Ok(()) +} + +fn format_user_text(u: &User) -> String { + let user_id = u.user_id.as_deref().unwrap_or("-"); + let mail = u.mail_address.as_deref().unwrap_or("-"); + let lang = u.lang.as_deref().unwrap_or("-"); + let last_login = u.last_login_time.as_deref().unwrap_or("-"); + format!( + "ID: {}\nUser ID: {}\nName: {}\nMail: {}\nRole: {}\nLang: {}\nLast login: {}", + u.id.to_string().bold(), + user_id, + u.name, + mail, + u.role_type, + lang, + last_login, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::anyhow; + use std::collections::BTreeMap; + + struct MockApi { + user: Option, + } + + impl crate::api::BacklogApi for MockApi { + fn get_space(&self) -> anyhow::Result { + unimplemented!() + } + fn get_myself(&self) -> anyhow::Result { + unimplemented!() + } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + self.user.clone().ok_or_else(|| anyhow!("no user")) + } + fn get_space_activities(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_space_disk_usage(&self) -> anyhow::Result { + unimplemented!() + } + fn get_space_notification( + &self, + ) -> anyhow::Result { + unimplemented!() + } + fn get_projects(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_project(&self, _key: &str) -> anyhow::Result { + unimplemented!() + } + fn get_project_activities( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_disk_usage( + &self, + _key: &str, + ) -> anyhow::Result { + unimplemented!() + } + fn get_project_users( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_statuses( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_issue_types( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_categories( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_project_versions( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_issues( + &self, + _params: &[(String, String)], + ) -> anyhow::Result> { + unimplemented!() + } + fn count_issues( + &self, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn get_issue(&self, _key: &str) -> anyhow::Result { + unimplemented!() + } + fn create_issue( + &self, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn update_issue( + &self, + _key: &str, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn delete_issue(&self, _key: &str) -> anyhow::Result { + unimplemented!() + } + fn get_issue_comments( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn add_issue_comment( + &self, + _key: &str, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn update_issue_comment( + &self, + _key: &str, + _comment_id: u64, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn delete_issue_comment( + &self, + _key: &str, + _comment_id: u64, + ) -> anyhow::Result { + unimplemented!() + } + fn get_issue_attachments( + &self, + _key: &str, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_wikis( + &self, + _params: &[(String, String)], + ) -> anyhow::Result> { + unimplemented!() + } + fn get_wiki(&self, _wiki_id: u64) -> anyhow::Result { + unimplemented!() + } + fn create_wiki( + &self, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn update_wiki( + &self, + _wiki_id: u64, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn delete_wiki( + &self, + _wiki_id: u64, + _params: &[(String, String)], + ) -> anyhow::Result { + unimplemented!() + } + fn get_wiki_history( + &self, + _wiki_id: u64, + ) -> anyhow::Result> { + unimplemented!() + } + fn get_wiki_attachments( + &self, + _wiki_id: u64, + ) -> anyhow::Result> { + unimplemented!() + } + } + + fn sample_user() -> User { + User { + id: 123, + user_id: Some("john".to_string()), + name: "John Doe".to_string(), + mail_address: Some("john@example.com".to_string()), + role_type: 1, + lang: Some("ja".to_string()), + last_login_time: Some("2024-01-01T00:00:00Z".to_string()), + extra: BTreeMap::new(), + } + } + + #[test] + fn show_with_text_output_succeeds() { + let api = MockApi { + user: Some(sample_user()), + }; + assert!(show_with(&UserShowArgs::new(123, false), &api).is_ok()); + } + + #[test] + fn show_with_json_output_succeeds() { + let api = MockApi { + user: Some(sample_user()), + }; + assert!(show_with(&UserShowArgs::new(123, true), &api).is_ok()); + } + + #[test] + fn show_with_propagates_api_error() { + let api = MockApi { user: None }; + let err = show_with(&UserShowArgs::new(999, false), &api).unwrap_err(); + assert!(err.to_string().contains("no user")); + } + + #[test] + fn format_user_text_contains_fields() { + let text = format_user_text(&sample_user()); + assert!(text.contains("123")); + assert!(text.contains("john")); + assert!(text.contains("John Doe")); + assert!(text.contains("john@example.com")); + } + + #[test] + fn format_user_text_handles_nulls() { + let user = User { + id: 1, + user_id: None, + name: "Bot".to_string(), + mail_address: None, + role_type: 2, + lang: None, + last_login_time: None, + extra: BTreeMap::new(), + }; + let text = format_user_text(&user); + assert!(text.contains("Bot")); + assert!(text.contains('-')); + } +} diff --git a/src/cmd/wiki/attachment/list.rs b/src/cmd/wiki/attachment/list.rs index fe281a2..f92a0ae 100644 --- a/src/cmd/wiki/attachment/list.rs +++ b/src/cmd/wiki/attachment/list.rs @@ -62,6 +62,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/wiki/create.rs b/src/cmd/wiki/create.rs index d2bf7ba..4e2c0c9 100644 --- a/src/cmd/wiki/create.rs +++ b/src/cmd/wiki/create.rs @@ -75,6 +75,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/wiki/delete.rs b/src/cmd/wiki/delete.rs index 0b9e95f..2f18513 100644 --- a/src/cmd/wiki/delete.rs +++ b/src/cmd/wiki/delete.rs @@ -61,6 +61,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/wiki/history.rs b/src/cmd/wiki/history.rs index 6a64dcf..b1999ec 100644 --- a/src/cmd/wiki/history.rs +++ b/src/cmd/wiki/history.rs @@ -62,6 +62,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/wiki/list.rs b/src/cmd/wiki/list.rs index ae80150..68edb0a 100644 --- a/src/cmd/wiki/list.rs +++ b/src/cmd/wiki/list.rs @@ -115,6 +115,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/wiki/show.rs b/src/cmd/wiki/show.rs index 3747496..4de7a35 100644 --- a/src/cmd/wiki/show.rs +++ b/src/cmd/wiki/show.rs @@ -65,6 +65,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/cmd/wiki/update.rs b/src/cmd/wiki/update.rs index 427c9dd..2d55f4e 100644 --- a/src/cmd/wiki/update.rs +++ b/src/cmd/wiki/update.rs @@ -83,6 +83,12 @@ mod tests { fn get_myself(&self) -> anyhow::Result { unimplemented!() } + fn get_users(&self) -> anyhow::Result> { + unimplemented!() + } + fn get_user(&self, _user_id: u64) -> anyhow::Result { + unimplemented!() + } fn get_space_activities(&self) -> anyhow::Result> { unimplemented!() } diff --git a/src/main.rs b/src/main.rs index ff6a36e..5c8794e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ use cmd::project::user::ProjectUserListArgs; use cmd::project::version::ProjectVersionListArgs; use cmd::project::{ProjectActivitiesArgs, ProjectDiskUsageArgs, ProjectListArgs, ProjectShowArgs}; use cmd::space::{SpaceActivitiesArgs, SpaceDiskUsageArgs, SpaceNotificationArgs, SpaceShowArgs}; +use cmd::user::{UserListArgs, UserShowArgs}; use cmd::wiki::attachment::WikiAttachmentListArgs; use cmd::wiki::{ WikiCreateArgs, WikiDeleteArgs, WikiHistoryArgs, WikiListArgs, WikiShowArgs, WikiUpdateArgs, @@ -78,6 +79,11 @@ enum Commands { #[command(subcommand)] action: WikiCommands, }, + /// Manage users + User { + #[command(subcommand)] + action: UserCommands, + }, } #[derive(Subcommand)] @@ -525,6 +531,24 @@ enum WikiAttachmentCommands { }, } +#[derive(Subcommand)] +enum UserCommands { + /// List users in the space + List { + /// Output as JSON + #[arg(long)] + json: bool, + }, + /// Show a user + Show { + /// User numeric ID + id: u64, + /// Output as JSON + #[arg(long)] + json: bool, + }, +} + #[derive(Subcommand)] enum AuthCommands { /// Login with your API key @@ -826,6 +850,10 @@ fn run() -> Result<()> { } }, }, + Commands::User { action } => match action { + UserCommands::List { json } => cmd::user::list(&UserListArgs::new(json)), + UserCommands::Show { id, json } => cmd::user::show(&UserShowArgs::new(id, json)), + }, Commands::Space { action, json } => match action { None => cmd::space::show(&SpaceShowArgs::new(json)), Some(SpaceCommands::Activities { json: sub_json }) => { From c4afa883d9ff6639641e3d1ae4d752e3ca516eb8 Mon Sep 17 00:00:00 2001 From: 23prime <23.prime.37@gmail.com> Date: Thu, 12 Mar 2026 19:35:53 +0900 Subject: [PATCH 3/7] docs: add bl user list and bl user show to commands reference --- website/docs/commands.md | 43 ++++++++++++++++++- .../current/commands.md | 43 ++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/website/docs/commands.md b/website/docs/commands.md index debdd9e..c6ad0d5 100644 --- a/website/docs/commands.md +++ b/website/docs/commands.md @@ -522,6 +522,45 @@ Example output: [2] notes.txt (1024 bytes) ``` +## `bl user list` + +List all users in the space. +Requires Space Administrator privileges. Non-admin users will receive `403 Forbidden`. + +```bash +bl user list +bl user list --json +``` + +Example output: + +```text +[john] John Doe +[jane] Jane Smith +[12345] Bot User +``` + +## `bl user show` + +Show details of a specific user by numeric ID. + +```bash +bl user show +bl user show --json +``` + +Example output: + +```text +ID: 123 +User ID: john +Name: John Doe +Mail: john@example.com +Role: 1 +Lang: ja +Last login: 2024-06-01T00:00:00Z +``` + ## Command coverage The table below maps Backlog API v2 endpoints to `bl` commands. @@ -601,8 +640,8 @@ The table below maps Backlog API v2 endpoints to `bl` commands. | Command | API endpoint | Status | | --- | --- | --- | | `bl auth status` | `GET /api/v2/users/myself` | ✅ Implemented (internal) | -| `bl user list` | `GET /api/v2/users` | Planned | -| `bl user show ` | `GET /api/v2/users/{userId}` | Planned | +| `bl user list` | `GET /api/v2/users` | ✅ Implemented | +| `bl user show ` | `GET /api/v2/users/{userId}` | ✅ Implemented | | `bl user activities ` | `GET /api/v2/users/{userId}/activities` | Planned | | `bl user recently-viewed ` | `GET /api/v2/users/{userId}/recentlyViewedIssues` | Planned | diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md b/website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md index d8483a0..09a4f01 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md @@ -522,6 +522,45 @@ bl wiki attachment list 12345 --json [2] notes.txt (1024 bytes) ``` +## `bl user list` + +スペース内のユーザーを一覧表示します。 +Space Administrator 権限が必要です。権限がない場合は `403 Forbidden` が返ります。 + +```bash +bl user list +bl user list --json +``` + +出力例: + +```text +[john] John Doe +[jane] Jane Smith +[12345] Bot User +``` + +## `bl user show` + +数値 ID でユーザーの詳細を表示します。 + +```bash +bl user show +bl user show --json +``` + +出力例: + +```text +ID: 123 +User ID: john +Name: John Doe +Mail: john@example.com +Role: 1 +Lang: ja +Last login: 2024-06-01T00:00:00Z +``` + ## コマンドカバレッジ Backlog API v2 エンドポイントと `bl` コマンドの対応表です。 @@ -601,8 +640,8 @@ Backlog API v2 エンドポイントと `bl` コマンドの対応表です。 | コマンド | API エンドポイント | 状態 | | --- | --- | --- | | `bl auth status` | `GET /api/v2/users/myself` | ✅ 実装済み(内部) | -| `bl user list` | `GET /api/v2/users` | 計画中 | -| `bl user show ` | `GET /api/v2/users/{userId}` | 計画中 | +| `bl user list` | `GET /api/v2/users` | ✅ 実装済み | +| `bl user show ` | `GET /api/v2/users/{userId}` | ✅ 実装済み | | `bl user activities ` | `GET /api/v2/users/{userId}/activities` | 計画中 | | `bl user recently-viewed ` | `GET /api/v2/users/{userId}/recentlyViewedIssues` | 計画中 | From 55e46c5c5faa517aba178ef317ed1b49a803df40 Mon Sep 17 00:00:00 2001 From: 23prime <23.prime.37@gmail.com> Date: Thu, 12 Mar 2026 19:36:37 +0900 Subject: [PATCH 4/7] ai: fix outdated file paths in developing skill --- .claude/skills/developing/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/developing/SKILL.md b/.claude/skills/developing/SKILL.md index 7cbfe75..f662383 100644 --- a/.claude/skills/developing/SKILL.md +++ b/.claude/skills/developing/SKILL.md @@ -30,10 +30,10 @@ git switch -c feature/ ## Step 3 — Pick a feature (feature tasks only) -Read `docs/user-guide.md` and pick the first "Planned" entry from the command coverage table. +Read `website/docs/commands.md` and pick the first "Planned" entry from the command coverage table. Confirm the selection with the user before proceeding. -For feature tasks, also read `references/patterns.md` for code patterns and known gotchas, and check the official API docs before writing structs: +For feature tasks, check the official API docs before writing structs: - **API docs**: - **Official SDK**: (ground truth for field names and types) @@ -47,7 +47,7 @@ Follow `AGENTS.md` conventions. For feature tasks, the typical file order is: 3. `src/cmd//.rs` — `()` + `_with()` + tests 4. `src/cmd//mod.rs` — re-export 5. `src/main.rs` — clap wiring -6. `docs/user-guide.md` — mark as implemented +6. `website/docs/commands.md` and `website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md` — add command docs and mark as implemented in the coverage table ## Step 5 — Auto-fix and check From bb1d58e8d51d7e63db724255afb3930f49a1e514 Mon Sep 17 00:00:00 2001 From: 23prime <23.prime.37@gmail.com> Date: Thu, 12 Mar 2026 19:48:19 +0900 Subject: [PATCH 5/7] fix: always show numeric ID in user list rows Addresses review comment: numeric ID hidden when userId exists, breaking bl user show workflow --- src/cmd/user/list.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cmd/user/list.rs b/src/cmd/user/list.rs index 682aa23..aaba654 100644 --- a/src/cmd/user/list.rs +++ b/src/cmd/user/list.rs @@ -35,7 +35,7 @@ pub fn list_with(args: &UserListArgs, api: &dyn BacklogApi) -> Result<()> { fn format_user_row(u: &User) -> String { match u.user_id.as_deref() { - Some(user_id) if !user_id.is_empty() => format!("[{}] {}", user_id, u.name), + Some(user_id) if !user_id.is_empty() => format!("[{}] {} ({})", u.id, u.name, user_id), _ => format!("[{}] {}", u.id, u.name), } } @@ -246,8 +246,9 @@ mod tests { #[test] fn format_user_row_with_user_id() { let text = format_user_row(&sample_user()); - assert!(text.contains("[john]")); + assert!(text.contains("[1]")); assert!(text.contains("John Doe")); + assert!(text.contains("(john)")); } #[test] From ea2515eb2471c5d49639fc02ff33b9f3acb01d4a Mon Sep 17 00:00:00 2001 From: 23prime <23.prime.37@gmail.com> Date: Thu, 12 Mar 2026 20:22:49 +0900 Subject: [PATCH 6/7] docs: add comments explaining nullable fields for bot accounts in User struct --- src/api/user.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/user.rs b/src/api/user.rs index 5dee8f5..962cbb1 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -8,8 +8,10 @@ use super::BacklogClient; #[serde(rename_all = "camelCase")] pub struct User { pub id: u64, + /// `null` for bot accounts (e.g. automation bots have no userId in Backlog API). pub user_id: Option, pub name: String, + /// `null` for bot accounts. pub mail_address: Option, pub role_type: u8, #[serde(default)] From b527edbe721d2f7949458ea3ca3aaa2967cfbc6d Mon Sep 17 00:00:00 2001 From: 23prime <23.prime.37@gmail.com> Date: Thu, 12 Mar 2026 20:25:27 +0900 Subject: [PATCH 7/7] ai: restore references/patterns.md reference in developing skill --- .claude/skills/developing/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/developing/SKILL.md b/.claude/skills/developing/SKILL.md index f662383..2cf7e18 100644 --- a/.claude/skills/developing/SKILL.md +++ b/.claude/skills/developing/SKILL.md @@ -33,7 +33,7 @@ git switch -c feature/ Read `website/docs/commands.md` and pick the first "Planned" entry from the command coverage table. Confirm the selection with the user before proceeding. -For feature tasks, check the official API docs before writing structs: +For feature tasks, also read `references/patterns.md` for code patterns and known gotchas, and check the official API docs before writing structs: - **API docs**: - **Official SDK**: (ground truth for field names and types)