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