feat: unified project management across Planka and Gitea

New src/pm.rs module with sunbeam pm subcommand:
- Planka client: cards, boards, lists, comments, assignments
  via OIDC token exchange for Planka JWT
- Gitea client: issues, comments, labels, milestones
  via OAuth2 Bearer token
- Unified Ticket type with p:/g: ID prefixes
- pm list: parallel fetch from both sources, merged display
- pm show/create/comment/close/assign across both systems
- Auth via crate::auth::get_token() (Hydra OAuth2)
This commit is contained in:
2026-03-20 14:11:16 +00:00
parent d4421d3e29
commit 5bdb78933f
2 changed files with 1589 additions and 0 deletions

View File

@@ -146,6 +146,18 @@ pub enum Verb {
action: Option<UserAction>, action: Option<UserAction>,
}, },
/// Authenticate with Sunbeam (OAuth2 login via browser).
Auth {
#[command(subcommand)]
action: Option<AuthAction>,
},
/// Project management across Planka and Gitea.
Pm {
#[command(subcommand)]
action: Option<PmAction>,
},
/// Self-update from latest mainline commit. /// Self-update from latest mainline commit.
Update, Update,
@@ -153,6 +165,67 @@ pub enum Verb {
Version, Version,
} }
#[derive(Subcommand, Debug)]
pub enum AuthAction {
/// Log in via browser (OAuth2 authorization code flow).
Login,
/// Log out (remove cached tokens).
Logout,
/// Show current authentication status.
Status,
}
#[derive(Subcommand, Debug)]
pub enum PmAction {
/// List tickets across Planka and Gitea.
List {
/// Filter by source: planka, gitea, or all (default: all).
#[arg(long, default_value = "all")]
source: String,
/// Filter by state: open, closed, all (default: open).
#[arg(long, default_value = "open")]
state: String,
},
/// Show ticket details.
Show {
/// Ticket ID (e.g. p:42 for Planka, g:studio/cli#7 for Gitea).
id: String,
},
/// Create a new ticket.
Create {
/// Ticket title.
title: String,
/// Ticket body/description.
#[arg(long, default_value = "")]
body: String,
/// Source: planka or gitea.
#[arg(long, default_value = "gitea")]
source: String,
/// Target: board ID for Planka, or org/repo for Gitea.
#[arg(long, default_value = "")]
target: String,
},
/// Add a comment to a ticket.
Comment {
/// Ticket ID.
id: String,
/// Comment text.
text: String,
},
/// Close/complete a ticket.
Close {
/// Ticket ID.
id: String,
},
/// Assign a user to a ticket.
Assign {
/// Ticket ID.
id: String,
/// Username or email to assign.
user: String,
},
}
#[derive(Debug, Clone, ValueEnum)] #[derive(Debug, Clone, ValueEnum)]
pub enum BuildTarget { pub enum BuildTarget {
Proxy, Proxy,
@@ -925,6 +998,47 @@ pub async fn dispatch() -> Result<()> {
} }
}, },
Some(Verb::Auth { action }) => match action {
None => {
crate::auth::cmd_auth_status().await
}
Some(AuthAction::Login) => crate::auth::cmd_auth_login().await,
Some(AuthAction::Logout) => crate::auth::cmd_auth_logout().await,
Some(AuthAction::Status) => crate::auth::cmd_auth_status().await,
},
Some(Verb::Pm { action }) => match action {
None => {
use clap::CommandFactory;
let mut cmd = Cli::command();
let sub = cmd
.find_subcommand_mut("pm")
.expect("pm subcommand");
sub.print_help()?;
println!();
Ok(())
}
Some(PmAction::List { source, state }) => {
let src = if source == "all" { None } else { Some(source.as_str()) };
crate::pm::cmd_pm_list(src, &state).await
}
Some(PmAction::Show { id }) => {
crate::pm::cmd_pm_show(&id).await
}
Some(PmAction::Create { title, body, source, target }) => {
crate::pm::cmd_pm_create(&title, &body, &source, &target).await
}
Some(PmAction::Comment { id, text }) => {
crate::pm::cmd_pm_comment(&id, &text).await
}
Some(PmAction::Close { id }) => {
crate::pm::cmd_pm_close(&id).await
}
Some(PmAction::Assign { id, user }) => {
crate::pm::cmd_pm_assign(&id, &user).await
}
},
Some(Verb::Update) => crate::update::cmd_update().await, Some(Verb::Update) => crate::update::cmd_update().await,
Some(Verb::Version) => { Some(Verb::Version) => {

1475
src/pm.rs Normal file

File diff suppressed because it is too large Load Diff