Rust CLI for tangled

Apply auto-refresh to all commands requiring authentication

Extend automatic OAuth token refresh to all command modules:

Updated commands:
- repo: list, create, clone, info, delete, star, unstar (7 functions)
- issue: list, create, show, edit, comment (5 functions)
- pr: list, create, show, review, merge (5 functions)
- knot: migrate (1 function)

Changes per module:
- Remove SessionManager import (no longer needed)
- Replace SessionManager::load() with util::load_session_with_refresh()
- Handles both pattern variations:
- match mgr.load()? { Some(s) => s, None => ... }
- mgr.load()?.ok_or_else(...)

All authenticated commands now automatically refresh expired tokens
without requiring manual re-authentication, providing a seamless
user experience across the entire CLI.

Changed files
+18 -78
crates
tangled-cli
src
+5 -21
crates/tangled-cli/src/commands/issue.rs
··· 4 4 }; 5 5 use anyhow::{anyhow, Result}; 6 6 use tangled_api::Issue; 7 - use tangled_config::session::SessionManager; 8 7 9 8 pub async fn run(_cli: &Cli, cmd: IssueCommand) -> Result<()> { 10 9 match cmd { ··· 17 16 } 18 17 19 18 async fn list(args: IssueListArgs) -> Result<()> { 20 - let mgr = SessionManager::default(); 21 - let session = mgr 22 - .load()? 23 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 19 + let session = crate::util::load_session_with_refresh().await?; 24 20 let pds = session 25 21 .pds 26 22 .clone() ··· 57 53 } 58 54 59 55 async fn create(args: IssueCreateArgs) -> Result<()> { 60 - let mgr = SessionManager::default(); 61 - let session = mgr 62 - .load()? 63 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 56 + let session = crate::util::load_session_with_refresh().await?; 64 57 let pds = session 65 58 .pds 66 59 .clone() ··· 97 90 98 91 async fn show(args: IssueShowArgs) -> Result<()> { 99 92 // For now, show only accepts at-uri or did:rkey or rkey (for your DID) 100 - let mgr = SessionManager::default(); 101 - let session = mgr 102 - .load()? 103 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 93 + let session = crate::util::load_session_with_refresh().await?; 104 94 let id = args.id; 105 95 let (did, rkey) = parse_record_id(&id, &session.did)?; 106 96 let pds = session ··· 129 119 130 120 async fn edit(args: IssueEditArgs) -> Result<()> { 131 121 // Simple edit: fetch existing record and putRecord with new title/body 132 - let mgr = SessionManager::default(); 133 - let session = mgr 134 - .load()? 135 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 122 + let session = crate::util::load_session_with_refresh().await?; 136 123 let (did, rkey) = parse_record_id(&args.id, &session.did)?; 137 124 let pds = session 138 125 .pds ··· 183 170 } 184 171 185 172 async fn comment(args: IssueCommentArgs) -> Result<()> { 186 - let mgr = SessionManager::default(); 187 - let session = mgr 188 - .load()? 189 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 173 + let session = crate::util::load_session_with_refresh().await?; 190 174 let (did, rkey) = parse_record_id(&args.id, &session.did)?; 191 175 let pds = session 192 176 .pds
+1 -5
crates/tangled-cli/src/commands/knot.rs
··· 3 3 use anyhow::Result; 4 4 use git2::{Direction, Repository as GitRepository, StatusOptions}; 5 5 use std::path::Path; 6 - use tangled_config::session::SessionManager; 7 6 8 7 pub async fn run(_cli: &Cli, cmd: KnotCommand) -> Result<()> { 9 8 match cmd { ··· 12 11 } 13 12 14 13 async fn migrate(args: KnotMigrateArgs) -> Result<()> { 15 - let mgr = SessionManager::default(); 16 - let session = mgr 17 - .load()? 18 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 14 + let session = crate::util::load_session_with_refresh().await?; 19 15 // 1) Ensure we're inside a git repository and working tree is clean 20 16 let repo = GitRepository::discover(Path::new("."))?; 21 17 let mut status_opts = StatusOptions::new();
+5 -21
crates/tangled-cli/src/commands/pr.rs
··· 2 2 use anyhow::{anyhow, Result}; 3 3 use std::path::Path; 4 4 use std::process::Command; 5 - use tangled_config::session::SessionManager; 6 5 7 6 pub async fn run(_cli: &Cli, cmd: PrCommand) -> Result<()> { 8 7 match cmd { ··· 15 14 } 16 15 17 16 async fn list(args: PrListArgs) -> Result<()> { 18 - let mgr = SessionManager::default(); 19 - let session = mgr 20 - .load()? 21 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 17 + let session = crate::util::load_session_with_refresh().await?; 22 18 let pds = session 23 19 .pds 24 20 .clone() ··· 54 50 55 51 async fn create(args: PrCreateArgs) -> Result<()> { 56 52 // Must be run inside the repo checkout; we will use git format-patch to build the patch 57 - let mgr = SessionManager::default(); 58 - let session = mgr 59 - .load()? 60 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 53 + let session = crate::util::load_session_with_refresh().await?; 61 54 let pds = session 62 55 .pds 63 56 .clone() ··· 126 119 } 127 120 128 121 async fn show(args: PrShowArgs) -> Result<()> { 129 - let mgr = SessionManager::default(); 130 - let session = mgr 131 - .load()? 132 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 122 + let session = crate::util::load_session_with_refresh().await?; 133 123 let (did, rkey) = parse_record_id(&args.id, &session.did)?; 134 124 let pds = session 135 125 .pds ··· 152 142 } 153 143 154 144 async fn review(args: PrReviewArgs) -> Result<()> { 155 - let mgr = SessionManager::default(); 156 - let session = mgr 157 - .load()? 158 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 145 + let session = crate::util::load_session_with_refresh().await?; 159 146 let (did, rkey) = parse_record_id(&args.id, &session.did)?; 160 147 let pds = session 161 148 .pds ··· 184 171 } 185 172 186 173 async fn merge(args: PrMergeArgs) -> Result<()> { 187 - let mgr = SessionManager::default(); 188 - let session = mgr 189 - .load()? 190 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 174 + let session = crate::util::load_session_with_refresh().await?; 191 175 let (did, rkey) = parse_record_id(&args.id, &session.did)?; 192 176 let pds = session 193 177 .pds
+7 -31
crates/tangled-cli/src/commands/repo.rs
··· 2 2 use git2::{build::RepoBuilder, Cred, FetchOptions, RemoteCallbacks}; 3 3 use serde_json; 4 4 use std::path::PathBuf; 5 - use tangled_config::session::SessionManager; 6 5 7 6 use crate::cli::{ 8 7 Cli, OutputFormat, RepoCloneArgs, RepoCommand, RepoCreateArgs, RepoDeleteArgs, RepoInfoArgs, ··· 22 21 } 23 22 24 23 async fn list(cli: &Cli, args: RepoListArgs) -> Result<()> { 25 - let mgr = SessionManager::default(); 26 - let session = match mgr.load()? { 27 - Some(s) => s, 28 - None => return Err(anyhow!("Please login first: tangled auth login")), 29 - }; 24 + let session = crate::util::load_session_with_refresh().await?; 30 25 31 26 // Use the PDS to list repo records for the user 32 27 let pds = session ··· 63 58 } 64 59 65 60 async fn create(args: RepoCreateArgs) -> Result<()> { 66 - let mgr = SessionManager::default(); 67 - let session = match mgr.load()? { 68 - Some(s) => s, 69 - None => return Err(anyhow!("Please login first: tangled auth login")), 70 - }; 61 + let session = crate::util::load_session_with_refresh().await?; 71 62 72 63 let base = std::env::var("TANGLED_API_BASE").unwrap_or_else(|_| "https://tngl.sh".into()); 73 64 let client = tangled_api::TangledClient::new(base); ··· 97 88 } 98 89 99 90 async fn clone(args: RepoCloneArgs) -> Result<()> { 100 - let mgr = SessionManager::default(); 101 - let session = mgr 102 - .load()? 103 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 91 + let session = crate::util::load_session_with_refresh().await?; 104 92 105 93 let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 106 94 let pds = session ··· 164 152 } 165 153 166 154 async fn info(args: RepoInfoArgs) -> Result<()> { 167 - let mgr = SessionManager::default(); 168 - let session = mgr 169 - .load()? 170 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 155 + let session = crate::util::load_session_with_refresh().await?; 171 156 let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 172 157 let pds = session 173 158 .pds ··· 235 220 } 236 221 237 222 async fn delete(args: RepoDeleteArgs) -> Result<()> { 238 - let mgr = SessionManager::default(); 239 - let session = mgr 240 - .load()? 241 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 223 + let session = crate::util::load_session_with_refresh().await?; 242 224 let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 243 225 let pds = session 244 226 .pds ··· 258 240 } 259 241 260 242 async fn star(args: RepoRefArgs) -> Result<()> { 261 - let mgr = SessionManager::default(); 262 - let session = mgr 263 - .load()? 264 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 243 + let session = crate::util::load_session_with_refresh().await?; 265 244 let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 266 245 let pds = session 267 246 .pds ··· 281 260 } 282 261 283 262 async fn unstar(args: RepoRefArgs) -> Result<()> { 284 - let mgr = SessionManager::default(); 285 - let session = mgr 286 - .load()? 287 - .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 263 + let session = crate::util::load_session_with_refresh().await?; 288 264 let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 289 265 let pds = session 290 266 .pds