Compare changes

Choose any two refs to compare.

+1
.gitignore
··· 1 + target/
+18
.pre-commit-config.yaml
··· 1 + # Pre-commit configuration for Tangled workspace 2 + # Uses local hooks to avoid network fetches and to run with system toolchain 3 + 4 + repos: 5 + - repo: local 6 + hooks: 7 + - id: rustfmt 8 + name: rustfmt (cargo fmt --check) 9 + entry: cargo fmt --all -- --check 10 + language: system 11 + types: [rust] 12 + pass_filenames: false 13 + - id: clippy 14 + name: clippy (cargo clippy -D warnings) 15 + entry: bash -lc 'cargo clippy --all-targets -- -D warnings' 16 + language: system 17 + types: [rust] 18 + pass_filenames: false
+441
AGENTS.md
··· 1 + # Tangled CLI โ€“ Agent Handoff (Massive Context) 2 + 3 + This document is a complete handoff for the next Codex instance working on the Tangled CLI (Rust). It explains what exists, what to build next, where to edit, how to call the APIs, how to persist sessions, how to print output, and how to validate success. 4 + 5 + Primary focus for this session: implement authentication (auth login/status/logout) and repository listing (repo list). 6 + 7 + -------------------------------------------------------------------------------- 8 + 9 + ## 0) TL;DR โ€“ Immediate Actions 10 + 11 + - Implement `auth login` using AT Protocol `com.atproto.server.createSession`. 12 + - Prompt for handle/password if flags arenโ€™t provided. 13 + - POST to `/xrpc/com.atproto.server.createSession` at the configured PDS (default `https://bsky.social`). 14 + - Persist `{accessJwt, refreshJwt, did, handle}` via `SessionManager` (keyring-backed). 15 + - `auth status` reads keyring and prints handle + did; `auth logout` clears keyring. 16 + 17 + - Implement `repo list` using Tangledโ€™s repo list method (tentative `sh.tangled.repo.list`). 18 + - GET `/xrpc/sh.tangled.repo.list` with optional params: `user`, `knot`, `starred`. 19 + - Include `Authorization: Bearer <accessJwt>` if required. 20 + - Print results as table (default) or JSON (`--format json`). 21 + 22 + Keep edits minimal and scoped to these features. 23 + 24 + -------------------------------------------------------------------------------- 25 + 26 + ## 1) Repository Map (Paths You Will Touch) 27 + 28 + - CLI (binary): 29 + - `tangled/crates/tangled-cli/src/commands/auth.rs` โ†’ implement login/status/logout. 30 + - `tangled/crates/tangled-cli/src/commands/repo.rs` โ†’ implement list. 31 + - `tangled/crates/tangled-cli/src/cli.rs` โ†’ already contains arguments and subcommands; no structural changes needed. 32 + - `tangled/crates/tangled-cli/src/main.rs` โ†’ no change. 33 + 34 + - Config + session: 35 + - `tangled/crates/tangled-config/src/session.rs` โ†’ already provides `Session` + `SessionManager` (keyring). 36 + - `tangled/crates/tangled-config/src/config.rs` โ†’ optional use for PDS/base URL (MVP can use CLI flags/env vars). 37 + 38 + - API client: 39 + - `tangled/crates/tangled-api/src/client.rs` โ†’ add XRPC helpers and implement `login_with_password` and `list_repos`. 40 + 41 + -------------------------------------------------------------------------------- 42 + 43 + ## 2) Current State Snapshot 44 + 45 + - Workspace is scaffolded and compiles after wiring dependencies (network needed to fetch crates): 46 + - `tangled-cli`: clap CLI with subcommands; commands currently log stubs. 47 + - `tangled-config`: TOML config loader/saver; keyring-backed session store. 48 + - `tangled-api`: client struct with placeholder methods. 49 + - `tangled-git`: stubs for future. 50 + - Placeholder lexicons in `tangled/lexicons/sh.tangled/*` are not authoritative; use AT Protocol docs and inspect real endpoints later. 51 + 52 + Goal: replace CLI stubs with real API calls for auth + repo list. 53 + 54 + -------------------------------------------------------------------------------- 55 + 56 + ## 3) Endpoints & Data Shapes 57 + 58 + ### 3.1 AT Protocol โ€“ Create Session 59 + 60 + - Method: `com.atproto.server.createSession` 61 + - HTTP: `POST /xrpc/com.atproto.server.createSession` 62 + - Request JSON: 63 + - `identifier: string` โ†’ user handle or email (e.g., `alice.bsky.social`). 64 + - `password: string` โ†’ password or app password. 65 + - Response JSON (subset used): 66 + - `accessJwt: string` 67 + - `refreshJwt: string` 68 + - `did: string` (e.g., `did:plc:...`) 69 + - `handle: string` 70 + 71 + Persist to keyring using `SessionManager`. 72 + 73 + ### 3.2 Tangled โ€“ Repo List (tentative) 74 + 75 + - Method: `sh.tangled.repo.list` (subject to change; wire in a constant to adjust easily). 76 + - HTTP: `GET /xrpc/sh.tangled.repo.list?user=<..>&knot=<..>&starred=<true|false>` 77 + - Auth: likely required; include `Authorization: Bearer <accessJwt>`. 78 + - Response JSON (envelope): 79 + - `{ "repos": [{ "name": string, "knot": string, "private": bool, ... }] }` 80 + 81 + If method name or response shape differs, adapt the client code; keep CLI interface stable. 82 + 83 + -------------------------------------------------------------------------------- 84 + 85 + ## 4) Implementation Plan 86 + 87 + ### 4.1 Add XRPC helpers and methods in `tangled-api` 88 + 89 + File: `tangled/crates/tangled-api/src/client.rs` 90 + 91 + - Extend `TangledClient` with: 92 + - `fn xrpc_url(&self, method: &str) -> String` โ†’ combines `base_url` + `/xrpc/` + `method`. 93 + - `async fn post_json<TReq: Serialize, TRes: DeserializeOwned>(&self, method, req, bearer) -> Result<TRes>`. 94 + - `async fn get_json<TRes: DeserializeOwned>(&self, method, params, bearer) -> Result<TRes>`. 95 + - Include `Authorization: Bearer <token>` when `bearer` is provided. 96 + 97 + - Implement: 98 + - `pub async fn login_with_password(&self, handle: &str, password: &str, pds: &str) -> Result<Session>` 99 + - POST to `com.atproto.server.createSession` at `self.base_url` (which should be the PDS base). 100 + - Map response to `tangled_config::session::Session` and return it (caller will persist). 101 + - `pub async fn list_repos(&self, user: Option<&str>, knot: Option<&str>, starred: bool, bearer: Option<&str>) -> Result<Vec<Repository>>` 102 + - GET `sh.tangled.repo.list` with params present only if set. 103 + - Return parsed `Vec<Repository>` from an envelope `{ repos: [...] }`. 104 + 105 + Error handling: For non-2xx, read the response body, return `anyhow!("{status}: {body}")`. 106 + 107 + ### 4.2 Wire CLI auth commands 108 + 109 + File: `tangled/crates/tangled-cli/src/commands/auth.rs` 110 + 111 + - `login`: 112 + - Determine PDS: use `--pds` arg if provided, else default `https://bsky.social` (later from config/env). 113 + - Prompt for missing handle/password. 114 + - `let client = tangled_api::TangledClient::new(&pds);` 115 + - `let session = client.login_with_password(&handle, &password, &pds).await?;` 116 + - `tangled_config::session::SessionManager::default().save(&session)?;` 117 + - Print: `Logged in as '{handle}' ({did})`. 118 + 119 + - `status`: 120 + - Load `SessionManager::default().load()?`. 121 + - If Some: print `Logged in as '{handle}' ({did})`. 122 + - Else: print `Not logged in. Run: tangled auth login`. 123 + 124 + - `logout`: 125 + - `SessionManager::default().clear()?`. 126 + - Print `Logged out` if something was cleared; otherwise `No session found` is acceptable. 127 + 128 + ### 4.3 Wire CLI repo list 129 + 130 + File: `tangled/crates/tangled-cli/src/commands/repo.rs` 131 + 132 + - Load session; if absent, print `Please login first: tangled auth login` and exit 1 (or 0 with friendly message; choose one and be consistent). 133 + - Build a client for Tangled API base (for now, default to `https://tangled.org` or allow `TANGLED_API_BASE` env var to override): 134 + - `let base = std::env::var("TANGLED_API_BASE").unwrap_or_else(|_| "https://tangled.org".into());` 135 + - `let client = tangled_api::TangledClient::new(base);` 136 + - Call `client.list_repos(args.user.as_deref(), args.knot.as_deref(), args.starred, Some(session.access_jwt.as_str())).await?`. 137 + - Print: 138 + - If `Cli.format == OutputFormat::Json`: `serde_json::to_string_pretty(&repos)`. 139 + - Else: simple columns `NAME KNOT PRIVATE` using `println!` formatting for now. 140 + 141 + -------------------------------------------------------------------------------- 142 + 143 + ## 5) Code Snippets (Copy/Paste Friendly) 144 + 145 + ### 5.1 In `tangled-api/src/client.rs` 146 + 147 + ```rust 148 + use anyhow::{anyhow, bail, Result}; 149 + use serde::{de::DeserializeOwned, Deserialize, Serialize}; 150 + use tangled_config::session::Session; 151 + 152 + #[derive(Clone, Debug)] 153 + pub struct TangledClient { pub(crate) base_url: String } 154 + 155 + impl TangledClient { 156 + pub fn new(base_url: impl Into<String>) -> Self { Self { base_url: base_url.into() } } 157 + pub fn default() -> Self { Self::new("https://tangled.org") } 158 + 159 + fn xrpc_url(&self, method: &str) -> String { 160 + format!("{}/xrpc/{}", self.base_url.trim_end_matches('/'), method) 161 + } 162 + 163 + async fn post_json<TReq: Serialize, TRes: DeserializeOwned>( 164 + &self, 165 + method: &str, 166 + req: &TReq, 167 + bearer: Option<&str>, 168 + ) -> Result<TRes> { 169 + let url = self.xrpc_url(method); 170 + let client = reqwest::Client::new(); 171 + let mut reqb = client.post(url).header(reqwest::header::CONTENT_TYPE, "application/json"); 172 + if let Some(token) = bearer { reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token)); } 173 + let res = reqb.json(req).send().await?; 174 + let status = res.status(); 175 + if !status.is_success() { 176 + let body = res.text().await.unwrap_or_default(); 177 + return Err(anyhow!("{}: {}", status, body)); 178 + } 179 + Ok(res.json::<TRes>().await?) 180 + } 181 + 182 + async fn get_json<TRes: DeserializeOwned>( 183 + &self, 184 + method: &str, 185 + params: &[(&str, String)], 186 + bearer: Option<&str>, 187 + ) -> Result<TRes> { 188 + let url = self.xrpc_url(method); 189 + let client = reqwest::Client::new(); 190 + let mut reqb = client.get(url).query(&params); 191 + if let Some(token) = bearer { reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token)); } 192 + let res = reqb.send().await?; 193 + let status = res.status(); 194 + if !status.is_success() { 195 + let body = res.text().await.unwrap_or_default(); 196 + return Err(anyhow!("{}: {}", status, body)); 197 + } 198 + Ok(res.json::<TRes>().await?) 199 + } 200 + 201 + pub async fn login_with_password(&self, handle: &str, password: &str, _pds: &str) -> Result<Session> { 202 + #[derive(Serialize)] 203 + struct Req<'a> { #[serde(rename = "identifier")] identifier: &'a str, #[serde(rename = "password")] password: &'a str } 204 + #[derive(Deserialize)] 205 + struct Res { #[serde(rename = "accessJwt")] access_jwt: String, #[serde(rename = "refreshJwt")] refresh_jwt: String, did: String, handle: String } 206 + let body = Req { identifier: handle, password }; 207 + let res: Res = self.post_json("com.atproto.server.createSession", &body, None).await?; 208 + Ok(Session { access_jwt: res.access_jwt, refresh_jwt: res.refresh_jwt, did: res.did, handle: res.handle, ..Default::default() }) 209 + } 210 + 211 + pub async fn list_repos(&self, user: Option<&str>, knot: Option<&str>, starred: bool, bearer: Option<&str>) -> Result<Vec<Repository>> { 212 + #[derive(Deserialize)] 213 + struct Envelope { repos: Vec<Repository> } 214 + let mut q = vec![]; 215 + if let Some(u) = user { q.push(("user", u.to_string())); } 216 + if let Some(k) = knot { q.push(("knot", k.to_string())); } 217 + if starred { q.push(("starred", true.to_string())); } 218 + let env: Envelope = self.get_json("sh.tangled.repo.list", &q, bearer).await?; 219 + Ok(env.repos) 220 + } 221 + } 222 + 223 + #[derive(Debug, Clone, Serialize, Deserialize, Default)] 224 + pub struct Repository { pub did: Option<String>, pub rkey: Option<String>, pub name: String, pub knot: Option<String>, pub description: Option<String>, pub private: bool } 225 + ``` 226 + 227 + ### 5.2 In `tangled-cli/src/commands/auth.rs` 228 + 229 + ```rust 230 + use anyhow::Result; 231 + use dialoguer::{Input, Password}; 232 + use tangled_config::session::SessionManager; 233 + use crate::cli::{AuthCommand, AuthLoginArgs, Cli}; 234 + 235 + pub async fn run(_cli: &Cli, cmd: AuthCommand) -> Result<()> { 236 + match cmd { 237 + AuthCommand::Login(args) => login(args).await, 238 + AuthCommand::Status => status().await, 239 + AuthCommand::Logout => logout().await, 240 + } 241 + } 242 + 243 + async fn login(mut args: AuthLoginArgs) -> Result<()> { 244 + let handle: String = match args.handle.take() { Some(h) => h, None => Input::new().with_prompt("Handle").interact_text()? }; 245 + let password: String = match args.password.take() { Some(p) => p, None => Password::new().with_prompt("Password").interact()? }; 246 + let pds = args.pds.unwrap_or_else(|| "https://bsky.social".to_string()); 247 + let client = tangled_api::TangledClient::new(&pds); 248 + let session = client.login_with_password(&handle, &password, &pds).await?; 249 + SessionManager::default().save(&session)?; 250 + println!("Logged in as '{}' ({})", session.handle, session.did); 251 + Ok(()) 252 + } 253 + 254 + async fn status() -> Result<()> { 255 + let mgr = SessionManager::default(); 256 + match mgr.load()? { 257 + Some(s) => println!("Logged in as '{}' ({})", s.handle, s.did), 258 + None => println!("Not logged in. Run: tangled auth login"), 259 + } 260 + Ok(()) 261 + } 262 + 263 + async fn logout() -> Result<()> { 264 + let mgr = SessionManager::default(); 265 + if mgr.load()?.is_some() { mgr.clear()?; println!("Logged out"); } else { println!("No session found"); } 266 + Ok(()) 267 + } 268 + ``` 269 + 270 + ### 5.3 In `tangled-cli/src/commands/repo.rs` 271 + 272 + ```rust 273 + use anyhow::{anyhow, Result}; 274 + use tangled_config::session::SessionManager; 275 + use crate::cli::{Cli, RepoCommand, RepoListArgs}; 276 + 277 + pub async fn run(_cli: &Cli, cmd: RepoCommand) -> Result<()> { 278 + match cmd { RepoCommand::List(args) => list(args).await, _ => Ok(println!("not implemented")) } 279 + } 280 + 281 + async fn list(args: RepoListArgs) -> Result<()> { 282 + let mgr = SessionManager::default(); 283 + let session = mgr.load()?.ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 284 + let base = std::env::var("TANGLED_API_BASE").unwrap_or_else(|_| "https://tangled.org".into()); 285 + let client = tangled_api::TangledClient::new(base); 286 + let repos = client.list_repos(args.user.as_deref(), args.knot.as_deref(), args.starred, Some(session.access_jwt.as_str())).await?; 287 + // Simple output: table or JSON to be improved later 288 + println!("NAME\tKNOT\tPRIVATE"); 289 + for r in repos { println!("{}\t{}\t{}", r.name, r.knot.unwrap_or_default(), r.private); } 290 + Ok(()) 291 + } 292 + ``` 293 + 294 + -------------------------------------------------------------------------------- 295 + 296 + ## 6) Configuration, Env Vars, and Security 297 + 298 + - PDS base (auth): default `https://bsky.social`. Accept CLI flag `--pds`; later read from config. 299 + - Tangled API base (repo list): default `https://tangled.org`; allow override via `TANGLED_API_BASE` env var. 300 + - Do not log passwords or tokens. 301 + - Store tokens only in keyring (already implemented). 302 + 303 + -------------------------------------------------------------------------------- 304 + 305 + ## 7) Testing Plan (MVP) 306 + 307 + - Client unit tests with `mockito` for `createSession` and `repo list` endpoints; simulate expected JSON. 308 + - CLI smoke tests optional for this pass. If added, use `assert_cmd` to check printed output strings. 309 + - Avoid live network calls in tests. 310 + 311 + -------------------------------------------------------------------------------- 312 + 313 + ## 8) Acceptance Criteria 314 + 315 + - `tangled auth login`: 316 + - Prompts or uses flags; successful call saves session and prints `Logged in as ...`. 317 + - On failure, shows HTTP status and short message. 318 + - `tangled auth status`: 319 + - Shows handle + did if session exists; otherwise says not logged in. 320 + - `tangled auth logout`: 321 + - Clears keyring; prints confirmation. 322 + - `tangled repo list`: 323 + - Performs authenticated GET and prints a list (even if empty) without panicking. 324 + - JSON output possible later; table output acceptable for now. 325 + 326 + -------------------------------------------------------------------------------- 327 + 328 + ## 9) Troubleshooting Notes 329 + 330 + - Keyring errors on Linux may indicate no secret service running; suggest enabling GNOME Keyring or KWallet. 331 + - If `repo list` returns 404, the method name or base URL may be wrong; adjust `sh.tangled.repo.list` or `TANGLED_API_BASE`. 332 + - If 401, session may be missing/expired; run `auth login` again. 333 + 334 + -------------------------------------------------------------------------------- 335 + 336 + ## 10) Nonโ€‘Goals for This Pass 337 + 338 + - Refresh token flow, device code, OAuth. 339 + - PRs, issues, knots, spindle implementation. 340 + - Advanced formatting, paging, completions. 341 + 342 + -------------------------------------------------------------------------------- 343 + 344 + ## 11) Future Followโ€‘ups 345 + 346 + - Refresh flow (`com.atproto.server.refreshSession`) and retry once on 401. 347 + - Persist base URLs and profiles in config; add `tangled config` commands. 348 + - Proper table/json formatting and shell completions. 349 + 350 + -------------------------------------------------------------------------------- 351 + 352 + ## 12) Quick Operator Commands 353 + 354 + - Build CLI: `cargo build -p tangled-cli` 355 + - Help: `cargo run -p tangled-cli -- --help` 356 + - Login: `cargo run -p tangled-cli -- auth login --handle <handle>` 357 + - Status: `cargo run -p tangled-cli -- auth status` 358 + - Repo list: `TANGLED_API_BASE=https://tangled.org cargo run -p tangled-cli -- repo list --user <handle>` 359 + 360 + -------------------------------------------------------------------------------- 361 + 362 + End of handoff. Implement auth login and repo list as described, keeping changes focused and testable. 363 + 364 + 365 + -------------------------------------------------------------------------------- 366 + 367 + ## 13) Tangled Core (../tangled-core) โ€“ Practical Notes 368 + 369 + This workspace often needs to peek at the Tangled monorepo to confirm XRPC endpoints and shapes. Here are concise tips and findings that informed this CLI implementation. 370 + 371 + ### Where To Look 372 + 373 + - Lexicons (authoritative NSIDs and shapes): `../tangled-core/lexicons/**` 374 + - Repo create: `../tangled-core/lexicons/repo/create.json` โ†’ `sh.tangled.repo.create` 375 + - Repo record schema: `../tangled-core/lexicons/repo/repo.json` โ†’ `sh.tangled.repo` 376 + - Misc repo queries (tree, log, tags, etc.) under `../tangled-core/lexicons/repo/` 377 + - Note: there is no `sh.tangled.repo.list` lexicon in the core right now; listing is done via ATproto records. 378 + - Knotserver XRPC routes (what requires auth vs open): `../tangled-core/knotserver/xrpc/xrpc.go` 379 + - Mutating repo ops (e.g., `sh.tangled.repo.create`) are behind ServiceAuth middleware. 380 + - Read-only repo queries (tree, log, etc.) are open. 381 + - Create repo handler (server-side flow): `../tangled-core/knotserver/xrpc/create_repo.go` 382 + - Validates ServiceAuth; expects rkey for the `sh.tangled.repo` record that already exists on the user's PDS. 383 + - ServiceAuth middleware (how Bearer is validated): `../tangled-core/xrpc/serviceauth/service_auth.go` 384 + - Validates a ServiceAuth token with Audience = `did:web:<knot-or-service-host>`. 385 + - Appview client for ServiceAuth: `../tangled-core/appview/xrpcclient/xrpc.go` (method: `ServerGetServiceAuth`). 386 + 387 + ### How To Search Quickly (rg examples) 388 + 389 + - Find a specific NSID across the repo: 390 + - `rg -n "sh\.tangled\.repo\.create" ../tangled-core` 391 + - See which endpoints are routed and whether theyโ€™re behind ServiceAuth: 392 + - `rg -n "chi\..*Get\(|chi\..*Post\(" ../tangled-core/knotserver/xrpc` 393 + - Then open `xrpc.go` and respective handlers. 394 + - Discover ServiceAuth usage and audience DID: 395 + - `rg -n "ServerGetServiceAuth|VerifyServiceAuth|serviceauth" ../tangled-core` 396 + - List lexicons by area: 397 + - `ls ../tangled-core/lexicons/repo` or `rg -n "\bid\": \"sh\.tangled\..*\"" ../tangled-core/lexicons` 398 + 399 + ### Repo Listing (client-side pattern) 400 + 401 + - There is no `sh.tangled.repo.list` in core. To list a userโ€™s repos: 402 + 1) Resolve handle โ†’ DID if needed via PDS: `GET com.atproto.identity.resolveHandle`. 403 + 2) List records from the userโ€™s PDS: `GET com.atproto.repo.listRecords` with `collection=sh.tangled.repo`. 404 + 3) Filter client-side (e.g., by `knot`). โ€œStarredโ€ filtering is not currently defined in core. 405 + 406 + ### Repo Creation (two-step flow) 407 + 408 + - Step 1 (PDS): create the `sh.tangled.repo` record in the userโ€™s repo: 409 + - `POST com.atproto.repo.createRecord` with `{ repo: <did>, collection: "sh.tangled.repo", record: { name, knot, description?, createdAt } }`. 410 + - Extract `rkey` from the returned `uri` (`at://<did>/<collection>/<rkey>`). 411 + - Step 2 (Tangled API base): call the server to initialize the bare repo on the knot: 412 + - Obtain ServiceAuth: `GET com.atproto.server.getServiceAuth` from PDS with `aud=did:web:<tngl.sh or target-host>`. 413 + - `POST sh.tangled.repo.create` on the Tangled API base with `{ rkey, defaultBranch?, source? }` and `Authorization: Bearer <serviceAuth>`. 414 + - Server validates token via `xrpc/serviceauth`, confirms actor permissions, and creates the git repo. 415 + 416 + ### Base URLs, DIDs, and Defaults 417 + 418 + - Tangled API base (server): default is `https://tngl.sh`. Do not use the marketing/landing site. 419 + - PDS base (auth + record ops): default `https://bsky.social` unless a different PDS was chosen on login. 420 + - ServiceAuth audience DID is `did:web:<host>` where `<host>` is the Tangled API base hostname. 421 + - CLI stores the PDS URL in the session to keep the CLI stateful. 422 + 423 + ### Common Errors and Fixes 424 + 425 + - `InvalidToken` when listing repos: listing should use the PDS (`com.atproto.repo.listRecords`), not the Tangled API base. 426 + - 404 on `repo.create`: verify ServiceAuth audience matches the target host and that the rkey exists on the PDS. 427 + - Keychain issues on Linux: ensure a Secret Service (e.g., GNOME Keyring or KWallet) is running. 428 + 429 + ### Implementation Pointers (CLI) 430 + 431 + - Auth 432 + - `com.atproto.server.createSession` against the PDS, save `{accessJwt, refreshJwt, did, handle, pds}` in keyring. 433 + - List repos 434 + - Use session.handle by default; resolve to DID, then `com.atproto.repo.listRecords` on PDS. 435 + - Create repo 436 + - Build the PDS record first; then ServiceAuth โ†’ `sh.tangled.repo.create` on `tngl.sh`. 437 + 438 + ### Testing Hints 439 + 440 + - Avoid live calls; use `mockito` to stub both PDS and Tangled API base endpoints. 441 + - Unit test decoding with minimal JSON envelopes: record lists, createRecord `uri`, and repo.create (empty body or simple ack).
+2975
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "addr2line" 7 + version = "0.25.1" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 15 + name = "adler2" 16 + version = "2.0.1" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 + 20 + [[package]] 21 + name = "aho-corasick" 22 + version = "1.1.3" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 + dependencies = [ 26 + "memchr", 27 + ] 28 + 29 + [[package]] 30 + name = "android_system_properties" 31 + version = "0.1.5" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 34 + dependencies = [ 35 + "libc", 36 + ] 37 + 38 + [[package]] 39 + name = "anstream" 40 + version = "0.6.20" 41 + source = "registry+https://github.com/rust-lang/crates.io-index" 42 + checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 43 + dependencies = [ 44 + "anstyle", 45 + "anstyle-parse", 46 + "anstyle-query", 47 + "anstyle-wincon", 48 + "colorchoice", 49 + "is_terminal_polyfill", 50 + "utf8parse", 51 + ] 52 + 53 + [[package]] 54 + name = "anstyle" 55 + version = "1.0.13" 56 + source = "registry+https://github.com/rust-lang/crates.io-index" 57 + checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 58 + 59 + [[package]] 60 + name = "anstyle-parse" 61 + version = "0.2.7" 62 + source = "registry+https://github.com/rust-lang/crates.io-index" 63 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 64 + dependencies = [ 65 + "utf8parse", 66 + ] 67 + 68 + [[package]] 69 + name = "anstyle-query" 70 + version = "1.1.4" 71 + source = "registry+https://github.com/rust-lang/crates.io-index" 72 + checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 73 + dependencies = [ 74 + "windows-sys 0.60.2", 75 + ] 76 + 77 + [[package]] 78 + name = "anstyle-wincon" 79 + version = "3.0.10" 80 + source = "registry+https://github.com/rust-lang/crates.io-index" 81 + checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 82 + dependencies = [ 83 + "anstyle", 84 + "once_cell_polyfill", 85 + "windows-sys 0.60.2", 86 + ] 87 + 88 + [[package]] 89 + name = "anyhow" 90 + version = "1.0.100" 91 + source = "registry+https://github.com/rust-lang/crates.io-index" 92 + checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 93 + 94 + [[package]] 95 + name = "async-compression" 96 + version = "0.4.32" 97 + source = "registry+https://github.com/rust-lang/crates.io-index" 98 + checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" 99 + dependencies = [ 100 + "compression-codecs", 101 + "compression-core", 102 + "futures-core", 103 + "pin-project-lite", 104 + "tokio", 105 + ] 106 + 107 + [[package]] 108 + name = "atomic-waker" 109 + version = "1.1.2" 110 + source = "registry+https://github.com/rust-lang/crates.io-index" 111 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 112 + 113 + [[package]] 114 + name = "atrium-api" 115 + version = "0.24.10" 116 + source = "registry+https://github.com/rust-lang/crates.io-index" 117 + checksum = "9c5d74937642f6b21814e82d80f54d55ebd985b681bffbe27c8a76e726c3c4db" 118 + dependencies = [ 119 + "atrium-xrpc", 120 + "chrono", 121 + "http", 122 + "ipld-core", 123 + "langtag", 124 + "regex", 125 + "serde", 126 + "serde_bytes", 127 + "serde_json", 128 + "thiserror 1.0.69", 129 + "tokio", 130 + "trait-variant", 131 + ] 132 + 133 + [[package]] 134 + name = "atrium-xrpc" 135 + version = "0.12.3" 136 + source = "registry+https://github.com/rust-lang/crates.io-index" 137 + checksum = "0216ad50ce34e9ff982e171c3659e65dedaa2ed5ac2994524debdc9a9647ffa8" 138 + dependencies = [ 139 + "http", 140 + "serde", 141 + "serde_html_form", 142 + "serde_json", 143 + "thiserror 1.0.69", 144 + "trait-variant", 145 + ] 146 + 147 + [[package]] 148 + name = "atrium-xrpc-client" 149 + version = "0.5.14" 150 + source = "registry+https://github.com/rust-lang/crates.io-index" 151 + checksum = "e099e5171f79faef52364ef0657a4cab086a71b384a779a29597a91b780de0d5" 152 + dependencies = [ 153 + "atrium-xrpc", 154 + "reqwest", 155 + ] 156 + 157 + [[package]] 158 + name = "autocfg" 159 + version = "1.5.0" 160 + source = "registry+https://github.com/rust-lang/crates.io-index" 161 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 162 + 163 + [[package]] 164 + name = "backtrace" 165 + version = "0.3.76" 166 + source = "registry+https://github.com/rust-lang/crates.io-index" 167 + checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" 168 + dependencies = [ 169 + "addr2line", 170 + "cfg-if", 171 + "libc", 172 + "miniz_oxide", 173 + "object", 174 + "rustc-demangle", 175 + "windows-link 0.2.0", 176 + ] 177 + 178 + [[package]] 179 + name = "base-x" 180 + version = "0.2.11" 181 + source = "registry+https://github.com/rust-lang/crates.io-index" 182 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 183 + 184 + [[package]] 185 + name = "base256emoji" 186 + version = "1.0.2" 187 + source = "registry+https://github.com/rust-lang/crates.io-index" 188 + checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" 189 + dependencies = [ 190 + "const-str", 191 + "match-lookup", 192 + ] 193 + 194 + [[package]] 195 + name = "base64" 196 + version = "0.22.1" 197 + source = "registry+https://github.com/rust-lang/crates.io-index" 198 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 199 + 200 + [[package]] 201 + name = "bitflags" 202 + version = "2.9.4" 203 + source = "registry+https://github.com/rust-lang/crates.io-index" 204 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 205 + 206 + [[package]] 207 + name = "bumpalo" 208 + version = "3.19.0" 209 + source = "registry+https://github.com/rust-lang/crates.io-index" 210 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 211 + 212 + [[package]] 213 + name = "bytes" 214 + version = "1.10.1" 215 + source = "registry+https://github.com/rust-lang/crates.io-index" 216 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 217 + 218 + [[package]] 219 + name = "cc" 220 + version = "1.2.39" 221 + source = "registry+https://github.com/rust-lang/crates.io-index" 222 + checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" 223 + dependencies = [ 224 + "find-msvc-tools", 225 + "jobserver", 226 + "libc", 227 + "shlex", 228 + ] 229 + 230 + [[package]] 231 + name = "cfg-if" 232 + version = "1.0.3" 233 + source = "registry+https://github.com/rust-lang/crates.io-index" 234 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 235 + 236 + [[package]] 237 + name = "cfg_aliases" 238 + version = "0.2.1" 239 + source = "registry+https://github.com/rust-lang/crates.io-index" 240 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 241 + 242 + [[package]] 243 + name = "chrono" 244 + version = "0.4.42" 245 + source = "registry+https://github.com/rust-lang/crates.io-index" 246 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 247 + dependencies = [ 248 + "iana-time-zone", 249 + "js-sys", 250 + "num-traits", 251 + "serde", 252 + "wasm-bindgen", 253 + "windows-link 0.2.0", 254 + ] 255 + 256 + [[package]] 257 + name = "cid" 258 + version = "0.11.1" 259 + source = "registry+https://github.com/rust-lang/crates.io-index" 260 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 261 + dependencies = [ 262 + "core2", 263 + "multibase", 264 + "multihash", 265 + "serde", 266 + "serde_bytes", 267 + "unsigned-varint", 268 + ] 269 + 270 + [[package]] 271 + name = "clap" 272 + version = "4.5.48" 273 + source = "registry+https://github.com/rust-lang/crates.io-index" 274 + checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" 275 + dependencies = [ 276 + "clap_builder", 277 + "clap_derive", 278 + ] 279 + 280 + [[package]] 281 + name = "clap_builder" 282 + version = "4.5.48" 283 + source = "registry+https://github.com/rust-lang/crates.io-index" 284 + checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" 285 + dependencies = [ 286 + "anstream", 287 + "anstyle", 288 + "clap_lex", 289 + "strsim", 290 + "terminal_size", 291 + "unicase", 292 + "unicode-width", 293 + ] 294 + 295 + [[package]] 296 + name = "clap_derive" 297 + version = "4.5.47" 298 + source = "registry+https://github.com/rust-lang/crates.io-index" 299 + checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 300 + dependencies = [ 301 + "heck", 302 + "proc-macro2", 303 + "quote", 304 + "syn 2.0.106", 305 + ] 306 + 307 + [[package]] 308 + name = "clap_lex" 309 + version = "0.7.5" 310 + source = "registry+https://github.com/rust-lang/crates.io-index" 311 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 312 + 313 + [[package]] 314 + name = "colorchoice" 315 + version = "1.0.4" 316 + source = "registry+https://github.com/rust-lang/crates.io-index" 317 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 318 + 319 + [[package]] 320 + name = "colored" 321 + version = "2.2.0" 322 + source = "registry+https://github.com/rust-lang/crates.io-index" 323 + checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 324 + dependencies = [ 325 + "lazy_static", 326 + "windows-sys 0.59.0", 327 + ] 328 + 329 + [[package]] 330 + name = "compression-codecs" 331 + version = "0.4.31" 332 + source = "registry+https://github.com/rust-lang/crates.io-index" 333 + checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" 334 + dependencies = [ 335 + "compression-core", 336 + "flate2", 337 + "memchr", 338 + ] 339 + 340 + [[package]] 341 + name = "compression-core" 342 + version = "0.4.29" 343 + source = "registry+https://github.com/rust-lang/crates.io-index" 344 + checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 345 + 346 + [[package]] 347 + name = "console" 348 + version = "0.15.11" 349 + source = "registry+https://github.com/rust-lang/crates.io-index" 350 + checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 351 + dependencies = [ 352 + "encode_unicode", 353 + "libc", 354 + "once_cell", 355 + "unicode-width", 356 + "windows-sys 0.59.0", 357 + ] 358 + 359 + [[package]] 360 + name = "const-str" 361 + version = "0.4.3" 362 + source = "registry+https://github.com/rust-lang/crates.io-index" 363 + checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 364 + 365 + [[package]] 366 + name = "core-foundation" 367 + version = "0.9.4" 368 + source = "registry+https://github.com/rust-lang/crates.io-index" 369 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 370 + dependencies = [ 371 + "core-foundation-sys", 372 + "libc", 373 + ] 374 + 375 + [[package]] 376 + name = "core-foundation-sys" 377 + version = "0.8.7" 378 + source = "registry+https://github.com/rust-lang/crates.io-index" 379 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 380 + 381 + [[package]] 382 + name = "core2" 383 + version = "0.4.0" 384 + source = "registry+https://github.com/rust-lang/crates.io-index" 385 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 386 + dependencies = [ 387 + "memchr", 388 + ] 389 + 390 + [[package]] 391 + name = "crc32fast" 392 + version = "1.5.0" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 395 + dependencies = [ 396 + "cfg-if", 397 + ] 398 + 399 + [[package]] 400 + name = "data-encoding" 401 + version = "2.9.0" 402 + source = "registry+https://github.com/rust-lang/crates.io-index" 403 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 404 + 405 + [[package]] 406 + name = "data-encoding-macro" 407 + version = "0.1.18" 408 + source = "registry+https://github.com/rust-lang/crates.io-index" 409 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 410 + dependencies = [ 411 + "data-encoding", 412 + "data-encoding-macro-internal", 413 + ] 414 + 415 + [[package]] 416 + name = "data-encoding-macro-internal" 417 + version = "0.1.16" 418 + source = "registry+https://github.com/rust-lang/crates.io-index" 419 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 420 + dependencies = [ 421 + "data-encoding", 422 + "syn 2.0.106", 423 + ] 424 + 425 + [[package]] 426 + name = "dbus" 427 + version = "0.9.9" 428 + source = "registry+https://github.com/rust-lang/crates.io-index" 429 + checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" 430 + dependencies = [ 431 + "libc", 432 + "libdbus-sys", 433 + "windows-sys 0.59.0", 434 + ] 435 + 436 + [[package]] 437 + name = "dbus-secret-service" 438 + version = "4.1.0" 439 + source = "registry+https://github.com/rust-lang/crates.io-index" 440 + checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" 441 + dependencies = [ 442 + "dbus", 443 + "openssl", 444 + "zeroize", 445 + ] 446 + 447 + [[package]] 448 + name = "dialoguer" 449 + version = "0.11.0" 450 + source = "registry+https://github.com/rust-lang/crates.io-index" 451 + checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" 452 + dependencies = [ 453 + "console", 454 + "shell-words", 455 + "tempfile", 456 + "thiserror 1.0.69", 457 + "zeroize", 458 + ] 459 + 460 + [[package]] 461 + name = "dirs" 462 + version = "5.0.1" 463 + source = "registry+https://github.com/rust-lang/crates.io-index" 464 + checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 465 + dependencies = [ 466 + "dirs-sys", 467 + ] 468 + 469 + [[package]] 470 + name = "dirs-sys" 471 + version = "0.4.1" 472 + source = "registry+https://github.com/rust-lang/crates.io-index" 473 + checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 474 + dependencies = [ 475 + "libc", 476 + "option-ext", 477 + "redox_users", 478 + "windows-sys 0.48.0", 479 + ] 480 + 481 + [[package]] 482 + name = "displaydoc" 483 + version = "0.2.5" 484 + source = "registry+https://github.com/rust-lang/crates.io-index" 485 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 486 + dependencies = [ 487 + "proc-macro2", 488 + "quote", 489 + "syn 2.0.106", 490 + ] 491 + 492 + [[package]] 493 + name = "encode_unicode" 494 + version = "1.0.0" 495 + source = "registry+https://github.com/rust-lang/crates.io-index" 496 + checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 497 + 498 + [[package]] 499 + name = "encoding_rs" 500 + version = "0.8.35" 501 + source = "registry+https://github.com/rust-lang/crates.io-index" 502 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 503 + dependencies = [ 504 + "cfg-if", 505 + ] 506 + 507 + [[package]] 508 + name = "equivalent" 509 + version = "1.0.2" 510 + source = "registry+https://github.com/rust-lang/crates.io-index" 511 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 512 + 513 + [[package]] 514 + name = "errno" 515 + version = "0.3.14" 516 + source = "registry+https://github.com/rust-lang/crates.io-index" 517 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 518 + dependencies = [ 519 + "libc", 520 + "windows-sys 0.61.1", 521 + ] 522 + 523 + [[package]] 524 + name = "fastrand" 525 + version = "2.3.0" 526 + source = "registry+https://github.com/rust-lang/crates.io-index" 527 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 528 + 529 + [[package]] 530 + name = "find-msvc-tools" 531 + version = "0.1.2" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" 534 + 535 + [[package]] 536 + name = "flate2" 537 + version = "1.1.2" 538 + source = "registry+https://github.com/rust-lang/crates.io-index" 539 + checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 540 + dependencies = [ 541 + "crc32fast", 542 + "miniz_oxide", 543 + ] 544 + 545 + [[package]] 546 + name = "fnv" 547 + version = "1.0.7" 548 + source = "registry+https://github.com/rust-lang/crates.io-index" 549 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 550 + 551 + [[package]] 552 + name = "foreign-types" 553 + version = "0.3.2" 554 + source = "registry+https://github.com/rust-lang/crates.io-index" 555 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 556 + dependencies = [ 557 + "foreign-types-shared", 558 + ] 559 + 560 + [[package]] 561 + name = "foreign-types-shared" 562 + version = "0.1.1" 563 + source = "registry+https://github.com/rust-lang/crates.io-index" 564 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 565 + 566 + [[package]] 567 + name = "form_urlencoded" 568 + version = "1.2.2" 569 + source = "registry+https://github.com/rust-lang/crates.io-index" 570 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 571 + dependencies = [ 572 + "percent-encoding", 573 + ] 574 + 575 + [[package]] 576 + name = "futures-channel" 577 + version = "0.3.31" 578 + source = "registry+https://github.com/rust-lang/crates.io-index" 579 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 580 + dependencies = [ 581 + "futures-core", 582 + ] 583 + 584 + [[package]] 585 + name = "futures-core" 586 + version = "0.3.31" 587 + source = "registry+https://github.com/rust-lang/crates.io-index" 588 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 589 + 590 + [[package]] 591 + name = "futures-io" 592 + version = "0.3.31" 593 + source = "registry+https://github.com/rust-lang/crates.io-index" 594 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 595 + 596 + [[package]] 597 + name = "futures-macro" 598 + version = "0.3.31" 599 + source = "registry+https://github.com/rust-lang/crates.io-index" 600 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 601 + dependencies = [ 602 + "proc-macro2", 603 + "quote", 604 + "syn 2.0.106", 605 + ] 606 + 607 + [[package]] 608 + name = "futures-sink" 609 + version = "0.3.31" 610 + source = "registry+https://github.com/rust-lang/crates.io-index" 611 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 612 + 613 + [[package]] 614 + name = "futures-task" 615 + version = "0.3.31" 616 + source = "registry+https://github.com/rust-lang/crates.io-index" 617 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 618 + 619 + [[package]] 620 + name = "futures-util" 621 + version = "0.3.31" 622 + source = "registry+https://github.com/rust-lang/crates.io-index" 623 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 624 + dependencies = [ 625 + "futures-core", 626 + "futures-io", 627 + "futures-macro", 628 + "futures-sink", 629 + "futures-task", 630 + "memchr", 631 + "pin-project-lite", 632 + "pin-utils", 633 + "slab", 634 + ] 635 + 636 + [[package]] 637 + name = "getrandom" 638 + version = "0.2.16" 639 + source = "registry+https://github.com/rust-lang/crates.io-index" 640 + checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 641 + dependencies = [ 642 + "cfg-if", 643 + "js-sys", 644 + "libc", 645 + "wasi 0.11.1+wasi-snapshot-preview1", 646 + "wasm-bindgen", 647 + ] 648 + 649 + [[package]] 650 + name = "getrandom" 651 + version = "0.3.3" 652 + source = "registry+https://github.com/rust-lang/crates.io-index" 653 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 654 + dependencies = [ 655 + "cfg-if", 656 + "js-sys", 657 + "libc", 658 + "r-efi", 659 + "wasi 0.14.7+wasi-0.2.4", 660 + "wasm-bindgen", 661 + ] 662 + 663 + [[package]] 664 + name = "gimli" 665 + version = "0.32.3" 666 + source = "registry+https://github.com/rust-lang/crates.io-index" 667 + checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 668 + 669 + [[package]] 670 + name = "git2" 671 + version = "0.19.0" 672 + source = "registry+https://github.com/rust-lang/crates.io-index" 673 + checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" 674 + dependencies = [ 675 + "bitflags", 676 + "libc", 677 + "libgit2-sys", 678 + "log", 679 + "openssl-probe", 680 + "openssl-sys", 681 + "url", 682 + ] 683 + 684 + [[package]] 685 + name = "h2" 686 + version = "0.4.12" 687 + source = "registry+https://github.com/rust-lang/crates.io-index" 688 + checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 689 + dependencies = [ 690 + "atomic-waker", 691 + "bytes", 692 + "fnv", 693 + "futures-core", 694 + "futures-sink", 695 + "http", 696 + "indexmap", 697 + "slab", 698 + "tokio", 699 + "tokio-util", 700 + "tracing", 701 + ] 702 + 703 + [[package]] 704 + name = "hashbrown" 705 + version = "0.16.0" 706 + source = "registry+https://github.com/rust-lang/crates.io-index" 707 + checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 708 + 709 + [[package]] 710 + name = "heck" 711 + version = "0.5.0" 712 + source = "registry+https://github.com/rust-lang/crates.io-index" 713 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 714 + 715 + [[package]] 716 + name = "http" 717 + version = "1.3.1" 718 + source = "registry+https://github.com/rust-lang/crates.io-index" 719 + checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 720 + dependencies = [ 721 + "bytes", 722 + "fnv", 723 + "itoa", 724 + ] 725 + 726 + [[package]] 727 + name = "http-body" 728 + version = "1.0.1" 729 + source = "registry+https://github.com/rust-lang/crates.io-index" 730 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 731 + dependencies = [ 732 + "bytes", 733 + "http", 734 + ] 735 + 736 + [[package]] 737 + name = "http-body-util" 738 + version = "0.1.3" 739 + source = "registry+https://github.com/rust-lang/crates.io-index" 740 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 741 + dependencies = [ 742 + "bytes", 743 + "futures-core", 744 + "http", 745 + "http-body", 746 + "pin-project-lite", 747 + ] 748 + 749 + [[package]] 750 + name = "httparse" 751 + version = "1.10.1" 752 + source = "registry+https://github.com/rust-lang/crates.io-index" 753 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 754 + 755 + [[package]] 756 + name = "hyper" 757 + version = "1.7.0" 758 + source = "registry+https://github.com/rust-lang/crates.io-index" 759 + checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 760 + dependencies = [ 761 + "atomic-waker", 762 + "bytes", 763 + "futures-channel", 764 + "futures-core", 765 + "h2", 766 + "http", 767 + "http-body", 768 + "httparse", 769 + "itoa", 770 + "pin-project-lite", 771 + "pin-utils", 772 + "smallvec", 773 + "tokio", 774 + "want", 775 + ] 776 + 777 + [[package]] 778 + name = "hyper-rustls" 779 + version = "0.27.7" 780 + source = "registry+https://github.com/rust-lang/crates.io-index" 781 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 782 + dependencies = [ 783 + "http", 784 + "hyper", 785 + "hyper-util", 786 + "rustls", 787 + "rustls-pki-types", 788 + "tokio", 789 + "tokio-rustls", 790 + "tower-service", 791 + "webpki-roots", 792 + ] 793 + 794 + [[package]] 795 + name = "hyper-tls" 796 + version = "0.6.0" 797 + source = "registry+https://github.com/rust-lang/crates.io-index" 798 + checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 799 + dependencies = [ 800 + "bytes", 801 + "http-body-util", 802 + "hyper", 803 + "hyper-util", 804 + "native-tls", 805 + "tokio", 806 + "tokio-native-tls", 807 + "tower-service", 808 + ] 809 + 810 + [[package]] 811 + name = "hyper-util" 812 + version = "0.1.17" 813 + source = "registry+https://github.com/rust-lang/crates.io-index" 814 + checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" 815 + dependencies = [ 816 + "base64", 817 + "bytes", 818 + "futures-channel", 819 + "futures-core", 820 + "futures-util", 821 + "http", 822 + "http-body", 823 + "hyper", 824 + "ipnet", 825 + "libc", 826 + "percent-encoding", 827 + "pin-project-lite", 828 + "socket2", 829 + "system-configuration", 830 + "tokio", 831 + "tower-service", 832 + "tracing", 833 + "windows-registry", 834 + ] 835 + 836 + [[package]] 837 + name = "iana-time-zone" 838 + version = "0.1.64" 839 + source = "registry+https://github.com/rust-lang/crates.io-index" 840 + checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 841 + dependencies = [ 842 + "android_system_properties", 843 + "core-foundation-sys", 844 + "iana-time-zone-haiku", 845 + "js-sys", 846 + "log", 847 + "wasm-bindgen", 848 + "windows-core", 849 + ] 850 + 851 + [[package]] 852 + name = "iana-time-zone-haiku" 853 + version = "0.1.2" 854 + source = "registry+https://github.com/rust-lang/crates.io-index" 855 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 856 + dependencies = [ 857 + "cc", 858 + ] 859 + 860 + [[package]] 861 + name = "icu_collections" 862 + version = "2.0.0" 863 + source = "registry+https://github.com/rust-lang/crates.io-index" 864 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 865 + dependencies = [ 866 + "displaydoc", 867 + "potential_utf", 868 + "yoke", 869 + "zerofrom", 870 + "zerovec", 871 + ] 872 + 873 + [[package]] 874 + name = "icu_locale_core" 875 + version = "2.0.0" 876 + source = "registry+https://github.com/rust-lang/crates.io-index" 877 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 878 + dependencies = [ 879 + "displaydoc", 880 + "litemap", 881 + "tinystr", 882 + "writeable", 883 + "zerovec", 884 + ] 885 + 886 + [[package]] 887 + name = "icu_normalizer" 888 + version = "2.0.0" 889 + source = "registry+https://github.com/rust-lang/crates.io-index" 890 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 891 + dependencies = [ 892 + "displaydoc", 893 + "icu_collections", 894 + "icu_normalizer_data", 895 + "icu_properties", 896 + "icu_provider", 897 + "smallvec", 898 + "zerovec", 899 + ] 900 + 901 + [[package]] 902 + name = "icu_normalizer_data" 903 + version = "2.0.0" 904 + source = "registry+https://github.com/rust-lang/crates.io-index" 905 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 906 + 907 + [[package]] 908 + name = "icu_properties" 909 + version = "2.0.1" 910 + source = "registry+https://github.com/rust-lang/crates.io-index" 911 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 912 + dependencies = [ 913 + "displaydoc", 914 + "icu_collections", 915 + "icu_locale_core", 916 + "icu_properties_data", 917 + "icu_provider", 918 + "potential_utf", 919 + "zerotrie", 920 + "zerovec", 921 + ] 922 + 923 + [[package]] 924 + name = "icu_properties_data" 925 + version = "2.0.1" 926 + source = "registry+https://github.com/rust-lang/crates.io-index" 927 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 928 + 929 + [[package]] 930 + name = "icu_provider" 931 + version = "2.0.0" 932 + source = "registry+https://github.com/rust-lang/crates.io-index" 933 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 934 + dependencies = [ 935 + "displaydoc", 936 + "icu_locale_core", 937 + "stable_deref_trait", 938 + "tinystr", 939 + "writeable", 940 + "yoke", 941 + "zerofrom", 942 + "zerotrie", 943 + "zerovec", 944 + ] 945 + 946 + [[package]] 947 + name = "idna" 948 + version = "1.1.0" 949 + source = "registry+https://github.com/rust-lang/crates.io-index" 950 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 951 + dependencies = [ 952 + "idna_adapter", 953 + "smallvec", 954 + "utf8_iter", 955 + ] 956 + 957 + [[package]] 958 + name = "idna_adapter" 959 + version = "1.2.1" 960 + source = "registry+https://github.com/rust-lang/crates.io-index" 961 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 962 + dependencies = [ 963 + "icu_normalizer", 964 + "icu_properties", 965 + ] 966 + 967 + [[package]] 968 + name = "indexmap" 969 + version = "2.11.4" 970 + source = "registry+https://github.com/rust-lang/crates.io-index" 971 + checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 972 + dependencies = [ 973 + "equivalent", 974 + "hashbrown", 975 + ] 976 + 977 + [[package]] 978 + name = "indicatif" 979 + version = "0.17.11" 980 + source = "registry+https://github.com/rust-lang/crates.io-index" 981 + checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 982 + dependencies = [ 983 + "console", 984 + "number_prefix", 985 + "portable-atomic", 986 + "unicode-width", 987 + "web-time", 988 + ] 989 + 990 + [[package]] 991 + name = "io-uring" 992 + version = "0.7.10" 993 + source = "registry+https://github.com/rust-lang/crates.io-index" 994 + checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" 995 + dependencies = [ 996 + "bitflags", 997 + "cfg-if", 998 + "libc", 999 + ] 1000 + 1001 + [[package]] 1002 + name = "ipld-core" 1003 + version = "0.4.2" 1004 + source = "registry+https://github.com/rust-lang/crates.io-index" 1005 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1006 + dependencies = [ 1007 + "cid", 1008 + "serde", 1009 + "serde_bytes", 1010 + ] 1011 + 1012 + [[package]] 1013 + name = "ipnet" 1014 + version = "2.11.0" 1015 + source = "registry+https://github.com/rust-lang/crates.io-index" 1016 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1017 + 1018 + [[package]] 1019 + name = "iri-string" 1020 + version = "0.7.8" 1021 + source = "registry+https://github.com/rust-lang/crates.io-index" 1022 + checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 1023 + dependencies = [ 1024 + "memchr", 1025 + "serde", 1026 + ] 1027 + 1028 + [[package]] 1029 + name = "is_terminal_polyfill" 1030 + version = "1.70.1" 1031 + source = "registry+https://github.com/rust-lang/crates.io-index" 1032 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1033 + 1034 + [[package]] 1035 + name = "itoa" 1036 + version = "1.0.15" 1037 + source = "registry+https://github.com/rust-lang/crates.io-index" 1038 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1039 + 1040 + [[package]] 1041 + name = "jobserver" 1042 + version = "0.1.34" 1043 + source = "registry+https://github.com/rust-lang/crates.io-index" 1044 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 1045 + dependencies = [ 1046 + "getrandom 0.3.3", 1047 + "libc", 1048 + ] 1049 + 1050 + [[package]] 1051 + name = "js-sys" 1052 + version = "0.3.81" 1053 + source = "registry+https://github.com/rust-lang/crates.io-index" 1054 + checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" 1055 + dependencies = [ 1056 + "once_cell", 1057 + "wasm-bindgen", 1058 + ] 1059 + 1060 + [[package]] 1061 + name = "keyring" 1062 + version = "3.6.3" 1063 + source = "registry+https://github.com/rust-lang/crates.io-index" 1064 + checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" 1065 + dependencies = [ 1066 + "dbus-secret-service", 1067 + "log", 1068 + "openssl", 1069 + "zeroize", 1070 + ] 1071 + 1072 + [[package]] 1073 + name = "langtag" 1074 + version = "0.3.4" 1075 + source = "registry+https://github.com/rust-lang/crates.io-index" 1076 + checksum = "ed60c85f254d6ae8450cec15eedd921efbc4d1bdf6fcf6202b9a58b403f6f805" 1077 + dependencies = [ 1078 + "serde", 1079 + ] 1080 + 1081 + [[package]] 1082 + name = "lazy_static" 1083 + version = "1.5.0" 1084 + source = "registry+https://github.com/rust-lang/crates.io-index" 1085 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1086 + 1087 + [[package]] 1088 + name = "libc" 1089 + version = "0.2.176" 1090 + source = "registry+https://github.com/rust-lang/crates.io-index" 1091 + checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 1092 + 1093 + [[package]] 1094 + name = "libdbus-sys" 1095 + version = "0.2.6" 1096 + source = "registry+https://github.com/rust-lang/crates.io-index" 1097 + checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" 1098 + dependencies = [ 1099 + "cc", 1100 + "pkg-config", 1101 + ] 1102 + 1103 + [[package]] 1104 + name = "libgit2-sys" 1105 + version = "0.17.0+1.8.1" 1106 + source = "registry+https://github.com/rust-lang/crates.io-index" 1107 + checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" 1108 + dependencies = [ 1109 + "cc", 1110 + "libc", 1111 + "libssh2-sys", 1112 + "libz-sys", 1113 + "openssl-sys", 1114 + "pkg-config", 1115 + ] 1116 + 1117 + [[package]] 1118 + name = "libredox" 1119 + version = "0.1.10" 1120 + source = "registry+https://github.com/rust-lang/crates.io-index" 1121 + checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" 1122 + dependencies = [ 1123 + "bitflags", 1124 + "libc", 1125 + ] 1126 + 1127 + [[package]] 1128 + name = "libssh2-sys" 1129 + version = "0.3.1" 1130 + source = "registry+https://github.com/rust-lang/crates.io-index" 1131 + checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" 1132 + dependencies = [ 1133 + "cc", 1134 + "libc", 1135 + "libz-sys", 1136 + "openssl-sys", 1137 + "pkg-config", 1138 + "vcpkg", 1139 + ] 1140 + 1141 + [[package]] 1142 + name = "libz-sys" 1143 + version = "1.1.22" 1144 + source = "registry+https://github.com/rust-lang/crates.io-index" 1145 + checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" 1146 + dependencies = [ 1147 + "cc", 1148 + "libc", 1149 + "pkg-config", 1150 + "vcpkg", 1151 + ] 1152 + 1153 + [[package]] 1154 + name = "linux-raw-sys" 1155 + version = "0.11.0" 1156 + source = "registry+https://github.com/rust-lang/crates.io-index" 1157 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 1158 + 1159 + [[package]] 1160 + name = "litemap" 1161 + version = "0.8.0" 1162 + source = "registry+https://github.com/rust-lang/crates.io-index" 1163 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1164 + 1165 + [[package]] 1166 + name = "lock_api" 1167 + version = "0.4.13" 1168 + source = "registry+https://github.com/rust-lang/crates.io-index" 1169 + checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 1170 + dependencies = [ 1171 + "autocfg", 1172 + "scopeguard", 1173 + ] 1174 + 1175 + [[package]] 1176 + name = "log" 1177 + version = "0.4.28" 1178 + source = "registry+https://github.com/rust-lang/crates.io-index" 1179 + checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 1180 + 1181 + [[package]] 1182 + name = "lru-slab" 1183 + version = "0.1.2" 1184 + source = "registry+https://github.com/rust-lang/crates.io-index" 1185 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 1186 + 1187 + [[package]] 1188 + name = "match-lookup" 1189 + version = "0.1.1" 1190 + source = "registry+https://github.com/rust-lang/crates.io-index" 1191 + checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" 1192 + dependencies = [ 1193 + "proc-macro2", 1194 + "quote", 1195 + "syn 1.0.109", 1196 + ] 1197 + 1198 + [[package]] 1199 + name = "memchr" 1200 + version = "2.7.6" 1201 + source = "registry+https://github.com/rust-lang/crates.io-index" 1202 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 1203 + 1204 + [[package]] 1205 + name = "mime" 1206 + version = "0.3.17" 1207 + source = "registry+https://github.com/rust-lang/crates.io-index" 1208 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1209 + 1210 + [[package]] 1211 + name = "miniz_oxide" 1212 + version = "0.8.9" 1213 + source = "registry+https://github.com/rust-lang/crates.io-index" 1214 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1215 + dependencies = [ 1216 + "adler2", 1217 + ] 1218 + 1219 + [[package]] 1220 + name = "mio" 1221 + version = "1.0.4" 1222 + source = "registry+https://github.com/rust-lang/crates.io-index" 1223 + checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 1224 + dependencies = [ 1225 + "libc", 1226 + "wasi 0.11.1+wasi-snapshot-preview1", 1227 + "windows-sys 0.59.0", 1228 + ] 1229 + 1230 + [[package]] 1231 + name = "multibase" 1232 + version = "0.9.2" 1233 + source = "registry+https://github.com/rust-lang/crates.io-index" 1234 + checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" 1235 + dependencies = [ 1236 + "base-x", 1237 + "base256emoji", 1238 + "data-encoding", 1239 + "data-encoding-macro", 1240 + ] 1241 + 1242 + [[package]] 1243 + name = "multihash" 1244 + version = "0.19.3" 1245 + source = "registry+https://github.com/rust-lang/crates.io-index" 1246 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1247 + dependencies = [ 1248 + "core2", 1249 + "serde", 1250 + "unsigned-varint", 1251 + ] 1252 + 1253 + [[package]] 1254 + name = "native-tls" 1255 + version = "0.2.14" 1256 + source = "registry+https://github.com/rust-lang/crates.io-index" 1257 + checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1258 + dependencies = [ 1259 + "libc", 1260 + "log", 1261 + "openssl", 1262 + "openssl-probe", 1263 + "openssl-sys", 1264 + "schannel", 1265 + "security-framework", 1266 + "security-framework-sys", 1267 + "tempfile", 1268 + ] 1269 + 1270 + [[package]] 1271 + name = "num-traits" 1272 + version = "0.2.19" 1273 + source = "registry+https://github.com/rust-lang/crates.io-index" 1274 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1275 + dependencies = [ 1276 + "autocfg", 1277 + ] 1278 + 1279 + [[package]] 1280 + name = "number_prefix" 1281 + version = "0.4.0" 1282 + source = "registry+https://github.com/rust-lang/crates.io-index" 1283 + checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 1284 + 1285 + [[package]] 1286 + name = "object" 1287 + version = "0.37.3" 1288 + source = "registry+https://github.com/rust-lang/crates.io-index" 1289 + checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" 1290 + dependencies = [ 1291 + "memchr", 1292 + ] 1293 + 1294 + [[package]] 1295 + name = "once_cell" 1296 + version = "1.21.3" 1297 + source = "registry+https://github.com/rust-lang/crates.io-index" 1298 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1299 + 1300 + [[package]] 1301 + name = "once_cell_polyfill" 1302 + version = "1.70.1" 1303 + source = "registry+https://github.com/rust-lang/crates.io-index" 1304 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 1305 + 1306 + [[package]] 1307 + name = "openssl" 1308 + version = "0.10.73" 1309 + source = "registry+https://github.com/rust-lang/crates.io-index" 1310 + checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 1311 + dependencies = [ 1312 + "bitflags", 1313 + "cfg-if", 1314 + "foreign-types", 1315 + "libc", 1316 + "once_cell", 1317 + "openssl-macros", 1318 + "openssl-sys", 1319 + ] 1320 + 1321 + [[package]] 1322 + name = "openssl-macros" 1323 + version = "0.1.1" 1324 + source = "registry+https://github.com/rust-lang/crates.io-index" 1325 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1326 + dependencies = [ 1327 + "proc-macro2", 1328 + "quote", 1329 + "syn 2.0.106", 1330 + ] 1331 + 1332 + [[package]] 1333 + name = "openssl-probe" 1334 + version = "0.1.6" 1335 + source = "registry+https://github.com/rust-lang/crates.io-index" 1336 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1337 + 1338 + [[package]] 1339 + name = "openssl-src" 1340 + version = "300.5.2+3.5.2" 1341 + source = "registry+https://github.com/rust-lang/crates.io-index" 1342 + checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" 1343 + dependencies = [ 1344 + "cc", 1345 + ] 1346 + 1347 + [[package]] 1348 + name = "openssl-sys" 1349 + version = "0.9.109" 1350 + source = "registry+https://github.com/rust-lang/crates.io-index" 1351 + checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 1352 + dependencies = [ 1353 + "cc", 1354 + "libc", 1355 + "openssl-src", 1356 + "pkg-config", 1357 + "vcpkg", 1358 + ] 1359 + 1360 + [[package]] 1361 + name = "option-ext" 1362 + version = "0.2.0" 1363 + source = "registry+https://github.com/rust-lang/crates.io-index" 1364 + checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1365 + 1366 + [[package]] 1367 + name = "parking_lot" 1368 + version = "0.12.4" 1369 + source = "registry+https://github.com/rust-lang/crates.io-index" 1370 + checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 1371 + dependencies = [ 1372 + "lock_api", 1373 + "parking_lot_core", 1374 + ] 1375 + 1376 + [[package]] 1377 + name = "parking_lot_core" 1378 + version = "0.9.11" 1379 + source = "registry+https://github.com/rust-lang/crates.io-index" 1380 + checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 1381 + dependencies = [ 1382 + "cfg-if", 1383 + "libc", 1384 + "redox_syscall", 1385 + "smallvec", 1386 + "windows-targets 0.52.6", 1387 + ] 1388 + 1389 + [[package]] 1390 + name = "percent-encoding" 1391 + version = "2.3.2" 1392 + source = "registry+https://github.com/rust-lang/crates.io-index" 1393 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1394 + 1395 + [[package]] 1396 + name = "pin-project-lite" 1397 + version = "0.2.16" 1398 + source = "registry+https://github.com/rust-lang/crates.io-index" 1399 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1400 + 1401 + [[package]] 1402 + name = "pin-utils" 1403 + version = "0.1.0" 1404 + source = "registry+https://github.com/rust-lang/crates.io-index" 1405 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1406 + 1407 + [[package]] 1408 + name = "pkg-config" 1409 + version = "0.3.32" 1410 + source = "registry+https://github.com/rust-lang/crates.io-index" 1411 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1412 + 1413 + [[package]] 1414 + name = "portable-atomic" 1415 + version = "1.11.1" 1416 + source = "registry+https://github.com/rust-lang/crates.io-index" 1417 + checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 1418 + 1419 + [[package]] 1420 + name = "potential_utf" 1421 + version = "0.1.3" 1422 + source = "registry+https://github.com/rust-lang/crates.io-index" 1423 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 1424 + dependencies = [ 1425 + "zerovec", 1426 + ] 1427 + 1428 + [[package]] 1429 + name = "ppv-lite86" 1430 + version = "0.2.21" 1431 + source = "registry+https://github.com/rust-lang/crates.io-index" 1432 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1433 + dependencies = [ 1434 + "zerocopy", 1435 + ] 1436 + 1437 + [[package]] 1438 + name = "proc-macro2" 1439 + version = "1.0.101" 1440 + source = "registry+https://github.com/rust-lang/crates.io-index" 1441 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 1442 + dependencies = [ 1443 + "unicode-ident", 1444 + ] 1445 + 1446 + [[package]] 1447 + name = "quinn" 1448 + version = "0.11.9" 1449 + source = "registry+https://github.com/rust-lang/crates.io-index" 1450 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 1451 + dependencies = [ 1452 + "bytes", 1453 + "cfg_aliases", 1454 + "pin-project-lite", 1455 + "quinn-proto", 1456 + "quinn-udp", 1457 + "rustc-hash", 1458 + "rustls", 1459 + "socket2", 1460 + "thiserror 2.0.17", 1461 + "tokio", 1462 + "tracing", 1463 + "web-time", 1464 + ] 1465 + 1466 + [[package]] 1467 + name = "quinn-proto" 1468 + version = "0.11.13" 1469 + source = "registry+https://github.com/rust-lang/crates.io-index" 1470 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 1471 + dependencies = [ 1472 + "bytes", 1473 + "getrandom 0.3.3", 1474 + "lru-slab", 1475 + "rand", 1476 + "ring", 1477 + "rustc-hash", 1478 + "rustls", 1479 + "rustls-pki-types", 1480 + "slab", 1481 + "thiserror 2.0.17", 1482 + "tinyvec", 1483 + "tracing", 1484 + "web-time", 1485 + ] 1486 + 1487 + [[package]] 1488 + name = "quinn-udp" 1489 + version = "0.5.14" 1490 + source = "registry+https://github.com/rust-lang/crates.io-index" 1491 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 1492 + dependencies = [ 1493 + "cfg_aliases", 1494 + "libc", 1495 + "once_cell", 1496 + "socket2", 1497 + "tracing", 1498 + "windows-sys 0.60.2", 1499 + ] 1500 + 1501 + [[package]] 1502 + name = "quote" 1503 + version = "1.0.41" 1504 + source = "registry+https://github.com/rust-lang/crates.io-index" 1505 + checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 1506 + dependencies = [ 1507 + "proc-macro2", 1508 + ] 1509 + 1510 + [[package]] 1511 + name = "r-efi" 1512 + version = "5.3.0" 1513 + source = "registry+https://github.com/rust-lang/crates.io-index" 1514 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1515 + 1516 + [[package]] 1517 + name = "rand" 1518 + version = "0.9.2" 1519 + source = "registry+https://github.com/rust-lang/crates.io-index" 1520 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 1521 + dependencies = [ 1522 + "rand_chacha", 1523 + "rand_core", 1524 + ] 1525 + 1526 + [[package]] 1527 + name = "rand_chacha" 1528 + version = "0.9.0" 1529 + source = "registry+https://github.com/rust-lang/crates.io-index" 1530 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1531 + dependencies = [ 1532 + "ppv-lite86", 1533 + "rand_core", 1534 + ] 1535 + 1536 + [[package]] 1537 + name = "rand_core" 1538 + version = "0.9.3" 1539 + source = "registry+https://github.com/rust-lang/crates.io-index" 1540 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1541 + dependencies = [ 1542 + "getrandom 0.3.3", 1543 + ] 1544 + 1545 + [[package]] 1546 + name = "redox_syscall" 1547 + version = "0.5.17" 1548 + source = "registry+https://github.com/rust-lang/crates.io-index" 1549 + checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" 1550 + dependencies = [ 1551 + "bitflags", 1552 + ] 1553 + 1554 + [[package]] 1555 + name = "redox_users" 1556 + version = "0.4.6" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 1559 + dependencies = [ 1560 + "getrandom 0.2.16", 1561 + "libredox", 1562 + "thiserror 1.0.69", 1563 + ] 1564 + 1565 + [[package]] 1566 + name = "regex" 1567 + version = "1.11.3" 1568 + source = "registry+https://github.com/rust-lang/crates.io-index" 1569 + checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" 1570 + dependencies = [ 1571 + "aho-corasick", 1572 + "memchr", 1573 + "regex-automata", 1574 + "regex-syntax", 1575 + ] 1576 + 1577 + [[package]] 1578 + name = "regex-automata" 1579 + version = "0.4.11" 1580 + source = "registry+https://github.com/rust-lang/crates.io-index" 1581 + checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" 1582 + dependencies = [ 1583 + "aho-corasick", 1584 + "memchr", 1585 + "regex-syntax", 1586 + ] 1587 + 1588 + [[package]] 1589 + name = "regex-syntax" 1590 + version = "0.8.6" 1591 + source = "registry+https://github.com/rust-lang/crates.io-index" 1592 + checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" 1593 + 1594 + [[package]] 1595 + name = "reqwest" 1596 + version = "0.12.23" 1597 + source = "registry+https://github.com/rust-lang/crates.io-index" 1598 + checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" 1599 + dependencies = [ 1600 + "async-compression", 1601 + "base64", 1602 + "bytes", 1603 + "encoding_rs", 1604 + "futures-core", 1605 + "futures-util", 1606 + "h2", 1607 + "http", 1608 + "http-body", 1609 + "http-body-util", 1610 + "hyper", 1611 + "hyper-rustls", 1612 + "hyper-tls", 1613 + "hyper-util", 1614 + "js-sys", 1615 + "log", 1616 + "mime", 1617 + "native-tls", 1618 + "percent-encoding", 1619 + "pin-project-lite", 1620 + "quinn", 1621 + "rustls", 1622 + "rustls-pki-types", 1623 + "serde", 1624 + "serde_json", 1625 + "serde_urlencoded", 1626 + "sync_wrapper", 1627 + "tokio", 1628 + "tokio-native-tls", 1629 + "tokio-rustls", 1630 + "tokio-util", 1631 + "tower", 1632 + "tower-http", 1633 + "tower-service", 1634 + "url", 1635 + "wasm-bindgen", 1636 + "wasm-bindgen-futures", 1637 + "wasm-streams", 1638 + "web-sys", 1639 + "webpki-roots", 1640 + ] 1641 + 1642 + [[package]] 1643 + name = "ring" 1644 + version = "0.17.14" 1645 + source = "registry+https://github.com/rust-lang/crates.io-index" 1646 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1647 + dependencies = [ 1648 + "cc", 1649 + "cfg-if", 1650 + "getrandom 0.2.16", 1651 + "libc", 1652 + "untrusted", 1653 + "windows-sys 0.52.0", 1654 + ] 1655 + 1656 + [[package]] 1657 + name = "rustc-demangle" 1658 + version = "0.1.26" 1659 + source = "registry+https://github.com/rust-lang/crates.io-index" 1660 + checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 1661 + 1662 + [[package]] 1663 + name = "rustc-hash" 1664 + version = "2.1.1" 1665 + source = "registry+https://github.com/rust-lang/crates.io-index" 1666 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1667 + 1668 + [[package]] 1669 + name = "rustix" 1670 + version = "1.1.2" 1671 + source = "registry+https://github.com/rust-lang/crates.io-index" 1672 + checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 1673 + dependencies = [ 1674 + "bitflags", 1675 + "errno", 1676 + "libc", 1677 + "linux-raw-sys", 1678 + "windows-sys 0.61.1", 1679 + ] 1680 + 1681 + [[package]] 1682 + name = "rustls" 1683 + version = "0.23.32" 1684 + source = "registry+https://github.com/rust-lang/crates.io-index" 1685 + checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" 1686 + dependencies = [ 1687 + "once_cell", 1688 + "ring", 1689 + "rustls-pki-types", 1690 + "rustls-webpki", 1691 + "subtle", 1692 + "zeroize", 1693 + ] 1694 + 1695 + [[package]] 1696 + name = "rustls-pki-types" 1697 + version = "1.12.0" 1698 + source = "registry+https://github.com/rust-lang/crates.io-index" 1699 + checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 1700 + dependencies = [ 1701 + "web-time", 1702 + "zeroize", 1703 + ] 1704 + 1705 + [[package]] 1706 + name = "rustls-webpki" 1707 + version = "0.103.6" 1708 + source = "registry+https://github.com/rust-lang/crates.io-index" 1709 + checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" 1710 + dependencies = [ 1711 + "ring", 1712 + "rustls-pki-types", 1713 + "untrusted", 1714 + ] 1715 + 1716 + [[package]] 1717 + name = "rustversion" 1718 + version = "1.0.22" 1719 + source = "registry+https://github.com/rust-lang/crates.io-index" 1720 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1721 + 1722 + [[package]] 1723 + name = "ryu" 1724 + version = "1.0.20" 1725 + source = "registry+https://github.com/rust-lang/crates.io-index" 1726 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1727 + 1728 + [[package]] 1729 + name = "schannel" 1730 + version = "0.1.28" 1731 + source = "registry+https://github.com/rust-lang/crates.io-index" 1732 + checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 1733 + dependencies = [ 1734 + "windows-sys 0.61.1", 1735 + ] 1736 + 1737 + [[package]] 1738 + name = "scopeguard" 1739 + version = "1.2.0" 1740 + source = "registry+https://github.com/rust-lang/crates.io-index" 1741 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1742 + 1743 + [[package]] 1744 + name = "security-framework" 1745 + version = "2.11.1" 1746 + source = "registry+https://github.com/rust-lang/crates.io-index" 1747 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1748 + dependencies = [ 1749 + "bitflags", 1750 + "core-foundation", 1751 + "core-foundation-sys", 1752 + "libc", 1753 + "security-framework-sys", 1754 + ] 1755 + 1756 + [[package]] 1757 + name = "security-framework-sys" 1758 + version = "2.15.0" 1759 + source = "registry+https://github.com/rust-lang/crates.io-index" 1760 + checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 1761 + dependencies = [ 1762 + "core-foundation-sys", 1763 + "libc", 1764 + ] 1765 + 1766 + [[package]] 1767 + name = "serde" 1768 + version = "1.0.228" 1769 + source = "registry+https://github.com/rust-lang/crates.io-index" 1770 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 1771 + dependencies = [ 1772 + "serde_core", 1773 + "serde_derive", 1774 + ] 1775 + 1776 + [[package]] 1777 + name = "serde_bytes" 1778 + version = "0.11.19" 1779 + source = "registry+https://github.com/rust-lang/crates.io-index" 1780 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 1781 + dependencies = [ 1782 + "serde", 1783 + "serde_core", 1784 + ] 1785 + 1786 + [[package]] 1787 + name = "serde_core" 1788 + version = "1.0.228" 1789 + source = "registry+https://github.com/rust-lang/crates.io-index" 1790 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 1791 + dependencies = [ 1792 + "serde_derive", 1793 + ] 1794 + 1795 + [[package]] 1796 + name = "serde_derive" 1797 + version = "1.0.228" 1798 + source = "registry+https://github.com/rust-lang/crates.io-index" 1799 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 1800 + dependencies = [ 1801 + "proc-macro2", 1802 + "quote", 1803 + "syn 2.0.106", 1804 + ] 1805 + 1806 + [[package]] 1807 + name = "serde_html_form" 1808 + version = "0.2.8" 1809 + source = "registry+https://github.com/rust-lang/crates.io-index" 1810 + checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 1811 + dependencies = [ 1812 + "form_urlencoded", 1813 + "indexmap", 1814 + "itoa", 1815 + "ryu", 1816 + "serde_core", 1817 + ] 1818 + 1819 + [[package]] 1820 + name = "serde_json" 1821 + version = "1.0.145" 1822 + source = "registry+https://github.com/rust-lang/crates.io-index" 1823 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 1824 + dependencies = [ 1825 + "itoa", 1826 + "memchr", 1827 + "ryu", 1828 + "serde", 1829 + "serde_core", 1830 + ] 1831 + 1832 + [[package]] 1833 + name = "serde_spanned" 1834 + version = "0.6.9" 1835 + source = "registry+https://github.com/rust-lang/crates.io-index" 1836 + checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 1837 + dependencies = [ 1838 + "serde", 1839 + ] 1840 + 1841 + [[package]] 1842 + name = "serde_urlencoded" 1843 + version = "0.7.1" 1844 + source = "registry+https://github.com/rust-lang/crates.io-index" 1845 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1846 + dependencies = [ 1847 + "form_urlencoded", 1848 + "itoa", 1849 + "ryu", 1850 + "serde", 1851 + ] 1852 + 1853 + [[package]] 1854 + name = "shell-words" 1855 + version = "1.1.0" 1856 + source = "registry+https://github.com/rust-lang/crates.io-index" 1857 + checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 1858 + 1859 + [[package]] 1860 + name = "shlex" 1861 + version = "1.3.0" 1862 + source = "registry+https://github.com/rust-lang/crates.io-index" 1863 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1864 + 1865 + [[package]] 1866 + name = "signal-hook-registry" 1867 + version = "1.4.6" 1868 + source = "registry+https://github.com/rust-lang/crates.io-index" 1869 + checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 1870 + dependencies = [ 1871 + "libc", 1872 + ] 1873 + 1874 + [[package]] 1875 + name = "slab" 1876 + version = "0.4.11" 1877 + source = "registry+https://github.com/rust-lang/crates.io-index" 1878 + checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1879 + 1880 + [[package]] 1881 + name = "smallvec" 1882 + version = "1.15.1" 1883 + source = "registry+https://github.com/rust-lang/crates.io-index" 1884 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1885 + 1886 + [[package]] 1887 + name = "socket2" 1888 + version = "0.6.0" 1889 + source = "registry+https://github.com/rust-lang/crates.io-index" 1890 + checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 1891 + dependencies = [ 1892 + "libc", 1893 + "windows-sys 0.59.0", 1894 + ] 1895 + 1896 + [[package]] 1897 + name = "stable_deref_trait" 1898 + version = "1.2.0" 1899 + source = "registry+https://github.com/rust-lang/crates.io-index" 1900 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1901 + 1902 + [[package]] 1903 + name = "strsim" 1904 + version = "0.11.1" 1905 + source = "registry+https://github.com/rust-lang/crates.io-index" 1906 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1907 + 1908 + [[package]] 1909 + name = "subtle" 1910 + version = "2.6.1" 1911 + source = "registry+https://github.com/rust-lang/crates.io-index" 1912 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1913 + 1914 + [[package]] 1915 + name = "syn" 1916 + version = "1.0.109" 1917 + source = "registry+https://github.com/rust-lang/crates.io-index" 1918 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1919 + dependencies = [ 1920 + "proc-macro2", 1921 + "quote", 1922 + "unicode-ident", 1923 + ] 1924 + 1925 + [[package]] 1926 + name = "syn" 1927 + version = "2.0.106" 1928 + source = "registry+https://github.com/rust-lang/crates.io-index" 1929 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 1930 + dependencies = [ 1931 + "proc-macro2", 1932 + "quote", 1933 + "unicode-ident", 1934 + ] 1935 + 1936 + [[package]] 1937 + name = "sync_wrapper" 1938 + version = "1.0.2" 1939 + source = "registry+https://github.com/rust-lang/crates.io-index" 1940 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1941 + dependencies = [ 1942 + "futures-core", 1943 + ] 1944 + 1945 + [[package]] 1946 + name = "synstructure" 1947 + version = "0.13.2" 1948 + source = "registry+https://github.com/rust-lang/crates.io-index" 1949 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1950 + dependencies = [ 1951 + "proc-macro2", 1952 + "quote", 1953 + "syn 2.0.106", 1954 + ] 1955 + 1956 + [[package]] 1957 + name = "system-configuration" 1958 + version = "0.6.1" 1959 + source = "registry+https://github.com/rust-lang/crates.io-index" 1960 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1961 + dependencies = [ 1962 + "bitflags", 1963 + "core-foundation", 1964 + "system-configuration-sys", 1965 + ] 1966 + 1967 + [[package]] 1968 + name = "system-configuration-sys" 1969 + version = "0.6.0" 1970 + source = "registry+https://github.com/rust-lang/crates.io-index" 1971 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1972 + dependencies = [ 1973 + "core-foundation-sys", 1974 + "libc", 1975 + ] 1976 + 1977 + [[package]] 1978 + name = "tangled-api" 1979 + version = "0.1.0" 1980 + dependencies = [ 1981 + "anyhow", 1982 + "atrium-api", 1983 + "atrium-xrpc-client", 1984 + "chrono", 1985 + "reqwest", 1986 + "serde", 1987 + "serde_json", 1988 + "tangled-config", 1989 + "tokio", 1990 + ] 1991 + 1992 + [[package]] 1993 + name = "tangled-cli" 1994 + version = "0.1.0" 1995 + dependencies = [ 1996 + "anyhow", 1997 + "clap", 1998 + "colored", 1999 + "dialoguer", 2000 + "git2", 2001 + "indicatif", 2002 + "serde", 2003 + "serde_json", 2004 + "tangled-api", 2005 + "tangled-config", 2006 + "tangled-git", 2007 + "tokio", 2008 + "url", 2009 + ] 2010 + 2011 + [[package]] 2012 + name = "tangled-config" 2013 + version = "0.1.0" 2014 + dependencies = [ 2015 + "anyhow", 2016 + "chrono", 2017 + "dirs", 2018 + "keyring", 2019 + "serde", 2020 + "serde_json", 2021 + "toml", 2022 + ] 2023 + 2024 + [[package]] 2025 + name = "tangled-git" 2026 + version = "0.1.0" 2027 + dependencies = [ 2028 + "anyhow", 2029 + "git2", 2030 + ] 2031 + 2032 + [[package]] 2033 + name = "tempfile" 2034 + version = "3.23.0" 2035 + source = "registry+https://github.com/rust-lang/crates.io-index" 2036 + checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 2037 + dependencies = [ 2038 + "fastrand", 2039 + "getrandom 0.3.3", 2040 + "once_cell", 2041 + "rustix", 2042 + "windows-sys 0.61.1", 2043 + ] 2044 + 2045 + [[package]] 2046 + name = "terminal_size" 2047 + version = "0.4.3" 2048 + source = "registry+https://github.com/rust-lang/crates.io-index" 2049 + checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 2050 + dependencies = [ 2051 + "rustix", 2052 + "windows-sys 0.60.2", 2053 + ] 2054 + 2055 + [[package]] 2056 + name = "thiserror" 2057 + version = "1.0.69" 2058 + source = "registry+https://github.com/rust-lang/crates.io-index" 2059 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2060 + dependencies = [ 2061 + "thiserror-impl 1.0.69", 2062 + ] 2063 + 2064 + [[package]] 2065 + name = "thiserror" 2066 + version = "2.0.17" 2067 + source = "registry+https://github.com/rust-lang/crates.io-index" 2068 + checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 2069 + dependencies = [ 2070 + "thiserror-impl 2.0.17", 2071 + ] 2072 + 2073 + [[package]] 2074 + name = "thiserror-impl" 2075 + version = "1.0.69" 2076 + source = "registry+https://github.com/rust-lang/crates.io-index" 2077 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2078 + dependencies = [ 2079 + "proc-macro2", 2080 + "quote", 2081 + "syn 2.0.106", 2082 + ] 2083 + 2084 + [[package]] 2085 + name = "thiserror-impl" 2086 + version = "2.0.17" 2087 + source = "registry+https://github.com/rust-lang/crates.io-index" 2088 + checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 2089 + dependencies = [ 2090 + "proc-macro2", 2091 + "quote", 2092 + "syn 2.0.106", 2093 + ] 2094 + 2095 + [[package]] 2096 + name = "tinystr" 2097 + version = "0.8.1" 2098 + source = "registry+https://github.com/rust-lang/crates.io-index" 2099 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 2100 + dependencies = [ 2101 + "displaydoc", 2102 + "zerovec", 2103 + ] 2104 + 2105 + [[package]] 2106 + name = "tinyvec" 2107 + version = "1.10.0" 2108 + source = "registry+https://github.com/rust-lang/crates.io-index" 2109 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 2110 + dependencies = [ 2111 + "tinyvec_macros", 2112 + ] 2113 + 2114 + [[package]] 2115 + name = "tinyvec_macros" 2116 + version = "0.1.1" 2117 + source = "registry+https://github.com/rust-lang/crates.io-index" 2118 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2119 + 2120 + [[package]] 2121 + name = "tokio" 2122 + version = "1.47.1" 2123 + source = "registry+https://github.com/rust-lang/crates.io-index" 2124 + checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" 2125 + dependencies = [ 2126 + "backtrace", 2127 + "bytes", 2128 + "io-uring", 2129 + "libc", 2130 + "mio", 2131 + "parking_lot", 2132 + "pin-project-lite", 2133 + "signal-hook-registry", 2134 + "slab", 2135 + "socket2", 2136 + "tokio-macros", 2137 + "windows-sys 0.59.0", 2138 + ] 2139 + 2140 + [[package]] 2141 + name = "tokio-macros" 2142 + version = "2.5.0" 2143 + source = "registry+https://github.com/rust-lang/crates.io-index" 2144 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2145 + dependencies = [ 2146 + "proc-macro2", 2147 + "quote", 2148 + "syn 2.0.106", 2149 + ] 2150 + 2151 + [[package]] 2152 + name = "tokio-native-tls" 2153 + version = "0.3.1" 2154 + source = "registry+https://github.com/rust-lang/crates.io-index" 2155 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2156 + dependencies = [ 2157 + "native-tls", 2158 + "tokio", 2159 + ] 2160 + 2161 + [[package]] 2162 + name = "tokio-rustls" 2163 + version = "0.26.4" 2164 + source = "registry+https://github.com/rust-lang/crates.io-index" 2165 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 2166 + dependencies = [ 2167 + "rustls", 2168 + "tokio", 2169 + ] 2170 + 2171 + [[package]] 2172 + name = "tokio-util" 2173 + version = "0.7.16" 2174 + source = "registry+https://github.com/rust-lang/crates.io-index" 2175 + checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 2176 + dependencies = [ 2177 + "bytes", 2178 + "futures-core", 2179 + "futures-sink", 2180 + "pin-project-lite", 2181 + "tokio", 2182 + ] 2183 + 2184 + [[package]] 2185 + name = "toml" 2186 + version = "0.8.23" 2187 + source = "registry+https://github.com/rust-lang/crates.io-index" 2188 + checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 2189 + dependencies = [ 2190 + "serde", 2191 + "serde_spanned", 2192 + "toml_datetime", 2193 + "toml_edit", 2194 + ] 2195 + 2196 + [[package]] 2197 + name = "toml_datetime" 2198 + version = "0.6.11" 2199 + source = "registry+https://github.com/rust-lang/crates.io-index" 2200 + checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 2201 + dependencies = [ 2202 + "serde", 2203 + ] 2204 + 2205 + [[package]] 2206 + name = "toml_edit" 2207 + version = "0.22.27" 2208 + source = "registry+https://github.com/rust-lang/crates.io-index" 2209 + checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 2210 + dependencies = [ 2211 + "indexmap", 2212 + "serde", 2213 + "serde_spanned", 2214 + "toml_datetime", 2215 + "toml_write", 2216 + "winnow", 2217 + ] 2218 + 2219 + [[package]] 2220 + name = "toml_write" 2221 + version = "0.1.2" 2222 + source = "registry+https://github.com/rust-lang/crates.io-index" 2223 + checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" 2224 + 2225 + [[package]] 2226 + name = "tower" 2227 + version = "0.5.2" 2228 + source = "registry+https://github.com/rust-lang/crates.io-index" 2229 + checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2230 + dependencies = [ 2231 + "futures-core", 2232 + "futures-util", 2233 + "pin-project-lite", 2234 + "sync_wrapper", 2235 + "tokio", 2236 + "tower-layer", 2237 + "tower-service", 2238 + ] 2239 + 2240 + [[package]] 2241 + name = "tower-http" 2242 + version = "0.6.6" 2243 + source = "registry+https://github.com/rust-lang/crates.io-index" 2244 + checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 2245 + dependencies = [ 2246 + "bitflags", 2247 + "bytes", 2248 + "futures-util", 2249 + "http", 2250 + "http-body", 2251 + "iri-string", 2252 + "pin-project-lite", 2253 + "tower", 2254 + "tower-layer", 2255 + "tower-service", 2256 + ] 2257 + 2258 + [[package]] 2259 + name = "tower-layer" 2260 + version = "0.3.3" 2261 + source = "registry+https://github.com/rust-lang/crates.io-index" 2262 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2263 + 2264 + [[package]] 2265 + name = "tower-service" 2266 + version = "0.3.3" 2267 + source = "registry+https://github.com/rust-lang/crates.io-index" 2268 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2269 + 2270 + [[package]] 2271 + name = "tracing" 2272 + version = "0.1.41" 2273 + source = "registry+https://github.com/rust-lang/crates.io-index" 2274 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2275 + dependencies = [ 2276 + "pin-project-lite", 2277 + "tracing-core", 2278 + ] 2279 + 2280 + [[package]] 2281 + name = "tracing-core" 2282 + version = "0.1.34" 2283 + source = "registry+https://github.com/rust-lang/crates.io-index" 2284 + checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 2285 + dependencies = [ 2286 + "once_cell", 2287 + ] 2288 + 2289 + [[package]] 2290 + name = "trait-variant" 2291 + version = "0.1.2" 2292 + source = "registry+https://github.com/rust-lang/crates.io-index" 2293 + checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" 2294 + dependencies = [ 2295 + "proc-macro2", 2296 + "quote", 2297 + "syn 2.0.106", 2298 + ] 2299 + 2300 + [[package]] 2301 + name = "try-lock" 2302 + version = "0.2.5" 2303 + source = "registry+https://github.com/rust-lang/crates.io-index" 2304 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2305 + 2306 + [[package]] 2307 + name = "unicase" 2308 + version = "2.8.1" 2309 + source = "registry+https://github.com/rust-lang/crates.io-index" 2310 + checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 2311 + 2312 + [[package]] 2313 + name = "unicode-ident" 2314 + version = "1.0.19" 2315 + source = "registry+https://github.com/rust-lang/crates.io-index" 2316 + checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 2317 + 2318 + [[package]] 2319 + name = "unicode-width" 2320 + version = "0.2.1" 2321 + source = "registry+https://github.com/rust-lang/crates.io-index" 2322 + checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 2323 + 2324 + [[package]] 2325 + name = "unsigned-varint" 2326 + version = "0.8.0" 2327 + source = "registry+https://github.com/rust-lang/crates.io-index" 2328 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 2329 + 2330 + [[package]] 2331 + name = "untrusted" 2332 + version = "0.9.0" 2333 + source = "registry+https://github.com/rust-lang/crates.io-index" 2334 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2335 + 2336 + [[package]] 2337 + name = "url" 2338 + version = "2.5.7" 2339 + source = "registry+https://github.com/rust-lang/crates.io-index" 2340 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 2341 + dependencies = [ 2342 + "form_urlencoded", 2343 + "idna", 2344 + "percent-encoding", 2345 + "serde", 2346 + ] 2347 + 2348 + [[package]] 2349 + name = "utf8_iter" 2350 + version = "1.0.4" 2351 + source = "registry+https://github.com/rust-lang/crates.io-index" 2352 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2353 + 2354 + [[package]] 2355 + name = "utf8parse" 2356 + version = "0.2.2" 2357 + source = "registry+https://github.com/rust-lang/crates.io-index" 2358 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2359 + 2360 + [[package]] 2361 + name = "vcpkg" 2362 + version = "0.2.15" 2363 + source = "registry+https://github.com/rust-lang/crates.io-index" 2364 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2365 + 2366 + [[package]] 2367 + name = "want" 2368 + version = "0.3.1" 2369 + source = "registry+https://github.com/rust-lang/crates.io-index" 2370 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2371 + dependencies = [ 2372 + "try-lock", 2373 + ] 2374 + 2375 + [[package]] 2376 + name = "wasi" 2377 + version = "0.11.1+wasi-snapshot-preview1" 2378 + source = "registry+https://github.com/rust-lang/crates.io-index" 2379 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 2380 + 2381 + [[package]] 2382 + name = "wasi" 2383 + version = "0.14.7+wasi-0.2.4" 2384 + source = "registry+https://github.com/rust-lang/crates.io-index" 2385 + checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" 2386 + dependencies = [ 2387 + "wasip2", 2388 + ] 2389 + 2390 + [[package]] 2391 + name = "wasip2" 2392 + version = "1.0.1+wasi-0.2.4" 2393 + source = "registry+https://github.com/rust-lang/crates.io-index" 2394 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 2395 + dependencies = [ 2396 + "wit-bindgen", 2397 + ] 2398 + 2399 + [[package]] 2400 + name = "wasm-bindgen" 2401 + version = "0.2.104" 2402 + source = "registry+https://github.com/rust-lang/crates.io-index" 2403 + checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" 2404 + dependencies = [ 2405 + "cfg-if", 2406 + "once_cell", 2407 + "rustversion", 2408 + "wasm-bindgen-macro", 2409 + "wasm-bindgen-shared", 2410 + ] 2411 + 2412 + [[package]] 2413 + name = "wasm-bindgen-backend" 2414 + version = "0.2.104" 2415 + source = "registry+https://github.com/rust-lang/crates.io-index" 2416 + checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" 2417 + dependencies = [ 2418 + "bumpalo", 2419 + "log", 2420 + "proc-macro2", 2421 + "quote", 2422 + "syn 2.0.106", 2423 + "wasm-bindgen-shared", 2424 + ] 2425 + 2426 + [[package]] 2427 + name = "wasm-bindgen-futures" 2428 + version = "0.4.54" 2429 + source = "registry+https://github.com/rust-lang/crates.io-index" 2430 + checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" 2431 + dependencies = [ 2432 + "cfg-if", 2433 + "js-sys", 2434 + "once_cell", 2435 + "wasm-bindgen", 2436 + "web-sys", 2437 + ] 2438 + 2439 + [[package]] 2440 + name = "wasm-bindgen-macro" 2441 + version = "0.2.104" 2442 + source = "registry+https://github.com/rust-lang/crates.io-index" 2443 + checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" 2444 + dependencies = [ 2445 + "quote", 2446 + "wasm-bindgen-macro-support", 2447 + ] 2448 + 2449 + [[package]] 2450 + name = "wasm-bindgen-macro-support" 2451 + version = "0.2.104" 2452 + source = "registry+https://github.com/rust-lang/crates.io-index" 2453 + checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" 2454 + dependencies = [ 2455 + "proc-macro2", 2456 + "quote", 2457 + "syn 2.0.106", 2458 + "wasm-bindgen-backend", 2459 + "wasm-bindgen-shared", 2460 + ] 2461 + 2462 + [[package]] 2463 + name = "wasm-bindgen-shared" 2464 + version = "0.2.104" 2465 + source = "registry+https://github.com/rust-lang/crates.io-index" 2466 + checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" 2467 + dependencies = [ 2468 + "unicode-ident", 2469 + ] 2470 + 2471 + [[package]] 2472 + name = "wasm-streams" 2473 + version = "0.4.2" 2474 + source = "registry+https://github.com/rust-lang/crates.io-index" 2475 + checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 2476 + dependencies = [ 2477 + "futures-util", 2478 + "js-sys", 2479 + "wasm-bindgen", 2480 + "wasm-bindgen-futures", 2481 + "web-sys", 2482 + ] 2483 + 2484 + [[package]] 2485 + name = "web-sys" 2486 + version = "0.3.81" 2487 + source = "registry+https://github.com/rust-lang/crates.io-index" 2488 + checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" 2489 + dependencies = [ 2490 + "js-sys", 2491 + "wasm-bindgen", 2492 + ] 2493 + 2494 + [[package]] 2495 + name = "web-time" 2496 + version = "1.1.0" 2497 + source = "registry+https://github.com/rust-lang/crates.io-index" 2498 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2499 + dependencies = [ 2500 + "js-sys", 2501 + "wasm-bindgen", 2502 + ] 2503 + 2504 + [[package]] 2505 + name = "webpki-roots" 2506 + version = "1.0.2" 2507 + source = "registry+https://github.com/rust-lang/crates.io-index" 2508 + checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" 2509 + dependencies = [ 2510 + "rustls-pki-types", 2511 + ] 2512 + 2513 + [[package]] 2514 + name = "windows-core" 2515 + version = "0.62.1" 2516 + source = "registry+https://github.com/rust-lang/crates.io-index" 2517 + checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" 2518 + dependencies = [ 2519 + "windows-implement", 2520 + "windows-interface", 2521 + "windows-link 0.2.0", 2522 + "windows-result 0.4.0", 2523 + "windows-strings 0.5.0", 2524 + ] 2525 + 2526 + [[package]] 2527 + name = "windows-implement" 2528 + version = "0.60.1" 2529 + source = "registry+https://github.com/rust-lang/crates.io-index" 2530 + checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" 2531 + dependencies = [ 2532 + "proc-macro2", 2533 + "quote", 2534 + "syn 2.0.106", 2535 + ] 2536 + 2537 + [[package]] 2538 + name = "windows-interface" 2539 + version = "0.59.2" 2540 + source = "registry+https://github.com/rust-lang/crates.io-index" 2541 + checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" 2542 + dependencies = [ 2543 + "proc-macro2", 2544 + "quote", 2545 + "syn 2.0.106", 2546 + ] 2547 + 2548 + [[package]] 2549 + name = "windows-link" 2550 + version = "0.1.3" 2551 + source = "registry+https://github.com/rust-lang/crates.io-index" 2552 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 2553 + 2554 + [[package]] 2555 + name = "windows-link" 2556 + version = "0.2.0" 2557 + source = "registry+https://github.com/rust-lang/crates.io-index" 2558 + checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 2559 + 2560 + [[package]] 2561 + name = "windows-registry" 2562 + version = "0.5.3" 2563 + source = "registry+https://github.com/rust-lang/crates.io-index" 2564 + checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 2565 + dependencies = [ 2566 + "windows-link 0.1.3", 2567 + "windows-result 0.3.4", 2568 + "windows-strings 0.4.2", 2569 + ] 2570 + 2571 + [[package]] 2572 + name = "windows-result" 2573 + version = "0.3.4" 2574 + source = "registry+https://github.com/rust-lang/crates.io-index" 2575 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 2576 + dependencies = [ 2577 + "windows-link 0.1.3", 2578 + ] 2579 + 2580 + [[package]] 2581 + name = "windows-result" 2582 + version = "0.4.0" 2583 + source = "registry+https://github.com/rust-lang/crates.io-index" 2584 + checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" 2585 + dependencies = [ 2586 + "windows-link 0.2.0", 2587 + ] 2588 + 2589 + [[package]] 2590 + name = "windows-strings" 2591 + version = "0.4.2" 2592 + source = "registry+https://github.com/rust-lang/crates.io-index" 2593 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 2594 + dependencies = [ 2595 + "windows-link 0.1.3", 2596 + ] 2597 + 2598 + [[package]] 2599 + name = "windows-strings" 2600 + version = "0.5.0" 2601 + source = "registry+https://github.com/rust-lang/crates.io-index" 2602 + checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" 2603 + dependencies = [ 2604 + "windows-link 0.2.0", 2605 + ] 2606 + 2607 + [[package]] 2608 + name = "windows-sys" 2609 + version = "0.48.0" 2610 + source = "registry+https://github.com/rust-lang/crates.io-index" 2611 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2612 + dependencies = [ 2613 + "windows-targets 0.48.5", 2614 + ] 2615 + 2616 + [[package]] 2617 + name = "windows-sys" 2618 + version = "0.52.0" 2619 + source = "registry+https://github.com/rust-lang/crates.io-index" 2620 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2621 + dependencies = [ 2622 + "windows-targets 0.52.6", 2623 + ] 2624 + 2625 + [[package]] 2626 + name = "windows-sys" 2627 + version = "0.59.0" 2628 + source = "registry+https://github.com/rust-lang/crates.io-index" 2629 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2630 + dependencies = [ 2631 + "windows-targets 0.52.6", 2632 + ] 2633 + 2634 + [[package]] 2635 + name = "windows-sys" 2636 + version = "0.60.2" 2637 + source = "registry+https://github.com/rust-lang/crates.io-index" 2638 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 2639 + dependencies = [ 2640 + "windows-targets 0.53.4", 2641 + ] 2642 + 2643 + [[package]] 2644 + name = "windows-sys" 2645 + version = "0.61.1" 2646 + source = "registry+https://github.com/rust-lang/crates.io-index" 2647 + checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" 2648 + dependencies = [ 2649 + "windows-link 0.2.0", 2650 + ] 2651 + 2652 + [[package]] 2653 + name = "windows-targets" 2654 + version = "0.48.5" 2655 + source = "registry+https://github.com/rust-lang/crates.io-index" 2656 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2657 + dependencies = [ 2658 + "windows_aarch64_gnullvm 0.48.5", 2659 + "windows_aarch64_msvc 0.48.5", 2660 + "windows_i686_gnu 0.48.5", 2661 + "windows_i686_msvc 0.48.5", 2662 + "windows_x86_64_gnu 0.48.5", 2663 + "windows_x86_64_gnullvm 0.48.5", 2664 + "windows_x86_64_msvc 0.48.5", 2665 + ] 2666 + 2667 + [[package]] 2668 + name = "windows-targets" 2669 + version = "0.52.6" 2670 + source = "registry+https://github.com/rust-lang/crates.io-index" 2671 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2672 + dependencies = [ 2673 + "windows_aarch64_gnullvm 0.52.6", 2674 + "windows_aarch64_msvc 0.52.6", 2675 + "windows_i686_gnu 0.52.6", 2676 + "windows_i686_gnullvm 0.52.6", 2677 + "windows_i686_msvc 0.52.6", 2678 + "windows_x86_64_gnu 0.52.6", 2679 + "windows_x86_64_gnullvm 0.52.6", 2680 + "windows_x86_64_msvc 0.52.6", 2681 + ] 2682 + 2683 + [[package]] 2684 + name = "windows-targets" 2685 + version = "0.53.4" 2686 + source = "registry+https://github.com/rust-lang/crates.io-index" 2687 + checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" 2688 + dependencies = [ 2689 + "windows-link 0.2.0", 2690 + "windows_aarch64_gnullvm 0.53.0", 2691 + "windows_aarch64_msvc 0.53.0", 2692 + "windows_i686_gnu 0.53.0", 2693 + "windows_i686_gnullvm 0.53.0", 2694 + "windows_i686_msvc 0.53.0", 2695 + "windows_x86_64_gnu 0.53.0", 2696 + "windows_x86_64_gnullvm 0.53.0", 2697 + "windows_x86_64_msvc 0.53.0", 2698 + ] 2699 + 2700 + [[package]] 2701 + name = "windows_aarch64_gnullvm" 2702 + version = "0.48.5" 2703 + source = "registry+https://github.com/rust-lang/crates.io-index" 2704 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2705 + 2706 + [[package]] 2707 + name = "windows_aarch64_gnullvm" 2708 + version = "0.52.6" 2709 + source = "registry+https://github.com/rust-lang/crates.io-index" 2710 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2711 + 2712 + [[package]] 2713 + name = "windows_aarch64_gnullvm" 2714 + version = "0.53.0" 2715 + source = "registry+https://github.com/rust-lang/crates.io-index" 2716 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 2717 + 2718 + [[package]] 2719 + name = "windows_aarch64_msvc" 2720 + version = "0.48.5" 2721 + source = "registry+https://github.com/rust-lang/crates.io-index" 2722 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2723 + 2724 + [[package]] 2725 + name = "windows_aarch64_msvc" 2726 + version = "0.52.6" 2727 + source = "registry+https://github.com/rust-lang/crates.io-index" 2728 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2729 + 2730 + [[package]] 2731 + name = "windows_aarch64_msvc" 2732 + version = "0.53.0" 2733 + source = "registry+https://github.com/rust-lang/crates.io-index" 2734 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 2735 + 2736 + [[package]] 2737 + name = "windows_i686_gnu" 2738 + version = "0.48.5" 2739 + source = "registry+https://github.com/rust-lang/crates.io-index" 2740 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2741 + 2742 + [[package]] 2743 + name = "windows_i686_gnu" 2744 + version = "0.52.6" 2745 + source = "registry+https://github.com/rust-lang/crates.io-index" 2746 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2747 + 2748 + [[package]] 2749 + name = "windows_i686_gnu" 2750 + version = "0.53.0" 2751 + source = "registry+https://github.com/rust-lang/crates.io-index" 2752 + checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 2753 + 2754 + [[package]] 2755 + name = "windows_i686_gnullvm" 2756 + version = "0.52.6" 2757 + source = "registry+https://github.com/rust-lang/crates.io-index" 2758 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2759 + 2760 + [[package]] 2761 + name = "windows_i686_gnullvm" 2762 + version = "0.53.0" 2763 + source = "registry+https://github.com/rust-lang/crates.io-index" 2764 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 2765 + 2766 + [[package]] 2767 + name = "windows_i686_msvc" 2768 + version = "0.48.5" 2769 + source = "registry+https://github.com/rust-lang/crates.io-index" 2770 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2771 + 2772 + [[package]] 2773 + name = "windows_i686_msvc" 2774 + version = "0.52.6" 2775 + source = "registry+https://github.com/rust-lang/crates.io-index" 2776 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2777 + 2778 + [[package]] 2779 + name = "windows_i686_msvc" 2780 + version = "0.53.0" 2781 + source = "registry+https://github.com/rust-lang/crates.io-index" 2782 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 2783 + 2784 + [[package]] 2785 + name = "windows_x86_64_gnu" 2786 + version = "0.48.5" 2787 + source = "registry+https://github.com/rust-lang/crates.io-index" 2788 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2789 + 2790 + [[package]] 2791 + name = "windows_x86_64_gnu" 2792 + version = "0.52.6" 2793 + source = "registry+https://github.com/rust-lang/crates.io-index" 2794 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2795 + 2796 + [[package]] 2797 + name = "windows_x86_64_gnu" 2798 + version = "0.53.0" 2799 + source = "registry+https://github.com/rust-lang/crates.io-index" 2800 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 2801 + 2802 + [[package]] 2803 + name = "windows_x86_64_gnullvm" 2804 + version = "0.48.5" 2805 + source = "registry+https://github.com/rust-lang/crates.io-index" 2806 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2807 + 2808 + [[package]] 2809 + name = "windows_x86_64_gnullvm" 2810 + version = "0.52.6" 2811 + source = "registry+https://github.com/rust-lang/crates.io-index" 2812 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2813 + 2814 + [[package]] 2815 + name = "windows_x86_64_gnullvm" 2816 + version = "0.53.0" 2817 + source = "registry+https://github.com/rust-lang/crates.io-index" 2818 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 2819 + 2820 + [[package]] 2821 + name = "windows_x86_64_msvc" 2822 + version = "0.48.5" 2823 + source = "registry+https://github.com/rust-lang/crates.io-index" 2824 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2825 + 2826 + [[package]] 2827 + name = "windows_x86_64_msvc" 2828 + version = "0.52.6" 2829 + source = "registry+https://github.com/rust-lang/crates.io-index" 2830 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2831 + 2832 + [[package]] 2833 + name = "windows_x86_64_msvc" 2834 + version = "0.53.0" 2835 + source = "registry+https://github.com/rust-lang/crates.io-index" 2836 + checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 2837 + 2838 + [[package]] 2839 + name = "winnow" 2840 + version = "0.7.13" 2841 + source = "registry+https://github.com/rust-lang/crates.io-index" 2842 + checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 2843 + dependencies = [ 2844 + "memchr", 2845 + ] 2846 + 2847 + [[package]] 2848 + name = "wit-bindgen" 2849 + version = "0.46.0" 2850 + source = "registry+https://github.com/rust-lang/crates.io-index" 2851 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 2852 + 2853 + [[package]] 2854 + name = "writeable" 2855 + version = "0.6.1" 2856 + source = "registry+https://github.com/rust-lang/crates.io-index" 2857 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 2858 + 2859 + [[package]] 2860 + name = "yoke" 2861 + version = "0.8.0" 2862 + source = "registry+https://github.com/rust-lang/crates.io-index" 2863 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 2864 + dependencies = [ 2865 + "serde", 2866 + "stable_deref_trait", 2867 + "yoke-derive", 2868 + "zerofrom", 2869 + ] 2870 + 2871 + [[package]] 2872 + name = "yoke-derive" 2873 + version = "0.8.0" 2874 + source = "registry+https://github.com/rust-lang/crates.io-index" 2875 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 2876 + dependencies = [ 2877 + "proc-macro2", 2878 + "quote", 2879 + "syn 2.0.106", 2880 + "synstructure", 2881 + ] 2882 + 2883 + [[package]] 2884 + name = "zerocopy" 2885 + version = "0.8.27" 2886 + source = "registry+https://github.com/rust-lang/crates.io-index" 2887 + checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 2888 + dependencies = [ 2889 + "zerocopy-derive", 2890 + ] 2891 + 2892 + [[package]] 2893 + name = "zerocopy-derive" 2894 + version = "0.8.27" 2895 + source = "registry+https://github.com/rust-lang/crates.io-index" 2896 + checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 2897 + dependencies = [ 2898 + "proc-macro2", 2899 + "quote", 2900 + "syn 2.0.106", 2901 + ] 2902 + 2903 + [[package]] 2904 + name = "zerofrom" 2905 + version = "0.1.6" 2906 + source = "registry+https://github.com/rust-lang/crates.io-index" 2907 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2908 + dependencies = [ 2909 + "zerofrom-derive", 2910 + ] 2911 + 2912 + [[package]] 2913 + name = "zerofrom-derive" 2914 + version = "0.1.6" 2915 + source = "registry+https://github.com/rust-lang/crates.io-index" 2916 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2917 + dependencies = [ 2918 + "proc-macro2", 2919 + "quote", 2920 + "syn 2.0.106", 2921 + "synstructure", 2922 + ] 2923 + 2924 + [[package]] 2925 + name = "zeroize" 2926 + version = "1.8.2" 2927 + source = "registry+https://github.com/rust-lang/crates.io-index" 2928 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 2929 + dependencies = [ 2930 + "zeroize_derive", 2931 + ] 2932 + 2933 + [[package]] 2934 + name = "zeroize_derive" 2935 + version = "1.4.2" 2936 + source = "registry+https://github.com/rust-lang/crates.io-index" 2937 + checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 2938 + dependencies = [ 2939 + "proc-macro2", 2940 + "quote", 2941 + "syn 2.0.106", 2942 + ] 2943 + 2944 + [[package]] 2945 + name = "zerotrie" 2946 + version = "0.2.2" 2947 + source = "registry+https://github.com/rust-lang/crates.io-index" 2948 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 2949 + dependencies = [ 2950 + "displaydoc", 2951 + "yoke", 2952 + "zerofrom", 2953 + ] 2954 + 2955 + [[package]] 2956 + name = "zerovec" 2957 + version = "0.11.4" 2958 + source = "registry+https://github.com/rust-lang/crates.io-index" 2959 + checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 2960 + dependencies = [ 2961 + "yoke", 2962 + "zerofrom", 2963 + "zerovec-derive", 2964 + ] 2965 + 2966 + [[package]] 2967 + name = "zerovec-derive" 2968 + version = "0.11.1" 2969 + source = "registry+https://github.com/rust-lang/crates.io-index" 2970 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 2971 + dependencies = [ 2972 + "proc-macro2", 2973 + "quote", 2974 + "syn 2.0.106", 2975 + ]
+2 -3
Cargo.toml
··· 41 41 42 42 # Storage 43 43 dirs = "5.0" 44 - keyring = "3.0" 44 + keyring = { version = "3.6", features = ["sync-secret-service", "vendored"] } 45 45 46 46 # Error Handling 47 47 anyhow = "1.0" 48 48 thiserror = "2.0" 49 49 50 50 # Utilities 51 - chrono = "0.4" 51 + chrono = { version = "0.4", features = ["serde"] } 52 52 url = "2.5" 53 53 base64 = "0.22" 54 54 regex = "1.10" ··· 58 58 tempfile = "3.10" 59 59 assert_cmd = "2.0" 60 60 predicates = "3.1" 61 -
+7 -1
README.md
··· 14 14 15 15 ## Quick start 16 16 17 - ``` 17 + ```sh 18 18 cargo run -p tangled-cli -- --help 19 + ``` 20 + 21 + ### Install from AUR (community maintained) 22 + 23 + ```sh 24 + yay -S tangled-cli-git 19 25 ``` 20 26 21 27 Building requires network to fetch crates.
+1 -1
crates/tangled-api/Cargo.toml
··· 11 11 serde_json = { workspace = true } 12 12 reqwest = { workspace = true } 13 13 tokio = { workspace = true, features = ["full"] } 14 + chrono = { workspace = true } 14 15 15 16 # Optionally depend on ATrium (wired later as endpoints solidify) 16 17 atrium-api = { workspace = true, optional = true } ··· 21 22 [features] 22 23 default = [] 23 24 atrium = ["dep:atrium-api", "dep:atrium-xrpc-client"] 24 -
+1216 -11
crates/tangled-api/src/client.rs
··· 1 - use anyhow::{bail, Result}; 2 - use serde::{Deserialize, Serialize}; 1 + use anyhow::{anyhow, Result}; 2 + use serde::{de::DeserializeOwned, Deserialize, Serialize}; 3 3 use tangled_config::session::Session; 4 4 5 5 #[derive(Clone, Debug)] ··· 7 7 base_url: String, 8 8 } 9 9 10 + const REPO_CREATE: &str = "sh.tangled.repo.create"; 11 + 12 + impl Default for TangledClient { 13 + fn default() -> Self { 14 + Self::new("https://tngl.sh") 15 + } 16 + } 17 + 10 18 impl TangledClient { 11 19 pub fn new(base_url: impl Into<String>) -> Self { 12 - Self { base_url: base_url.into() } 20 + Self { 21 + base_url: base_url.into(), 22 + } 23 + } 24 + 25 + fn xrpc_url(&self, method: &str) -> String { 26 + format!("{}/xrpc/{}", self.base_url.trim_end_matches('/'), method) 27 + } 28 + 29 + async fn post_json<TReq: Serialize, TRes: DeserializeOwned>( 30 + &self, 31 + method: &str, 32 + req: &TReq, 33 + bearer: Option<&str>, 34 + ) -> Result<TRes> { 35 + let url = self.xrpc_url(method); 36 + let client = reqwest::Client::new(); 37 + let mut reqb = client 38 + .post(url) 39 + .header(reqwest::header::CONTENT_TYPE, "application/json"); 40 + if let Some(token) = bearer { 41 + reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token)); 42 + } 43 + let res = reqb.json(req).send().await?; 44 + let status = res.status(); 45 + if !status.is_success() { 46 + let body = res.text().await.unwrap_or_default(); 47 + return Err(anyhow!("{}: {}", status, body)); 48 + } 49 + Ok(res.json::<TRes>().await?) 50 + } 51 + 52 + async fn get_json<TRes: DeserializeOwned>( 53 + &self, 54 + method: &str, 55 + params: &[(&str, String)], 56 + bearer: Option<&str>, 57 + ) -> Result<TRes> { 58 + let url = self.xrpc_url(method); 59 + let client = reqwest::Client::new(); 60 + let mut reqb = client 61 + .get(&url) 62 + .query(&params) 63 + .header(reqwest::header::ACCEPT, "application/json"); 64 + if let Some(token) = bearer { 65 + reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token)); 66 + } 67 + let res = reqb.send().await?; 68 + let status = res.status(); 69 + let body = res.text().await.unwrap_or_default(); 70 + if !status.is_success() { 71 + return Err(anyhow!("GET {} -> {}: {}", url, status, body)); 72 + } 73 + serde_json::from_str::<TRes>(&body).map_err(|e| { 74 + let snippet = body.chars().take(300).collect::<String>(); 75 + anyhow!( 76 + "error decoding response from {}: {}\nBody (first 300 chars): {}", 77 + url, 78 + e, 79 + snippet 80 + ) 81 + }) 82 + } 83 + 84 + pub async fn login_with_password( 85 + &self, 86 + handle: &str, 87 + password: &str, 88 + _pds: &str, 89 + ) -> Result<Session> { 90 + #[derive(Serialize)] 91 + struct Req<'a> { 92 + #[serde(rename = "identifier")] 93 + identifier: &'a str, 94 + #[serde(rename = "password")] 95 + password: &'a str, 96 + } 97 + #[derive(Deserialize)] 98 + struct Res { 99 + #[serde(rename = "accessJwt")] 100 + access_jwt: String, 101 + #[serde(rename = "refreshJwt")] 102 + refresh_jwt: String, 103 + did: String, 104 + handle: String, 105 + } 106 + let body = Req { 107 + identifier: handle, 108 + password, 109 + }; 110 + let res: Res = self 111 + .post_json("com.atproto.server.createSession", &body, None) 112 + .await?; 113 + Ok(Session { 114 + access_jwt: res.access_jwt, 115 + refresh_jwt: res.refresh_jwt, 116 + did: res.did, 117 + handle: res.handle, 118 + ..Default::default() 119 + }) 120 + } 121 + 122 + pub async fn list_repos( 123 + &self, 124 + user: Option<&str>, 125 + knot: Option<&str>, 126 + starred: bool, 127 + bearer: Option<&str>, 128 + ) -> Result<Vec<Repository>> { 129 + // NOTE: Repo listing is done via the user's PDS using com.atproto.repo.listRecords 130 + // for the collection "sh.tangled.repo". This does not go through the Tangled API base. 131 + // Here, `self.base_url` must be the PDS base (e.g., https://bsky.social). 132 + // Resolve handle to DID if needed 133 + let did = match user { 134 + Some(u) if u.starts_with("did:") => u.to_string(), 135 + Some(handle) => { 136 + #[derive(Deserialize)] 137 + struct Res { 138 + did: String, 139 + } 140 + let params = [("handle", handle.to_string())]; 141 + let res: Res = self 142 + .get_json("com.atproto.identity.resolveHandle", &params, bearer) 143 + .await?; 144 + res.did 145 + } 146 + None => { 147 + return Err(anyhow!( 148 + "missing user for list_repos; provide handle or DID" 149 + )); 150 + } 151 + }; 152 + 153 + #[derive(Deserialize)] 154 + struct RecordItem { 155 + uri: String, 156 + value: Repository, 157 + } 158 + #[derive(Deserialize)] 159 + struct ListRes { 160 + #[serde(default)] 161 + records: Vec<RecordItem>, 162 + } 163 + 164 + let params = vec![ 165 + ("repo", did), 166 + ("collection", "sh.tangled.repo".to_string()), 167 + ("limit", "100".to_string()), 168 + ]; 169 + 170 + let res: ListRes = self 171 + .get_json("com.atproto.repo.listRecords", &params, bearer) 172 + .await?; 173 + let mut repos: Vec<Repository> = res 174 + .records 175 + .into_iter() 176 + .map(|r| { 177 + let mut val = r.value; 178 + if val.rkey.is_none() { 179 + if let Some(k) = Self::uri_rkey(&r.uri) { 180 + val.rkey = Some(k); 181 + } 182 + } 183 + if val.did.is_none() { 184 + if let Some(d) = Self::uri_did(&r.uri) { 185 + val.did = Some(d); 186 + } 187 + } 188 + val 189 + }) 190 + .collect(); 191 + // Apply optional filters client-side 192 + if let Some(k) = knot { 193 + repos.retain(|r| r.knot.as_deref().unwrap_or("") == k); 194 + } 195 + if starred { 196 + // TODO: implement starred filtering when API is available. For now, no-op. 197 + } 198 + Ok(repos) 199 + } 200 + 201 + pub async fn create_repo(&self, opts: CreateRepoOptions<'_>) -> Result<()> { 202 + // 1) Create the sh.tangled.repo record on the user's PDS 203 + #[derive(Serialize)] 204 + struct Record<'a> { 205 + name: &'a str, 206 + knot: &'a str, 207 + #[serde(skip_serializing_if = "Option::is_none")] 208 + description: Option<&'a str>, 209 + #[serde(rename = "createdAt")] 210 + created_at: String, 211 + } 212 + #[derive(Serialize)] 213 + struct CreateRecordReq<'a> { 214 + repo: &'a str, 215 + collection: &'a str, 216 + validate: bool, 217 + record: Record<'a>, 218 + } 219 + #[derive(Deserialize)] 220 + struct CreateRecordRes { 221 + uri: String, 222 + } 223 + 224 + let now = chrono::Utc::now().to_rfc3339(); 225 + let rec = Record { 226 + name: opts.name, 227 + knot: opts.knot, 228 + description: opts.description, 229 + created_at: now, 230 + }; 231 + let create_req = CreateRecordReq { 232 + repo: opts.did, 233 + collection: "sh.tangled.repo", 234 + validate: true, 235 + record: rec, 236 + }; 237 + 238 + let pds_client = TangledClient::new(opts.pds_base); 239 + let created: CreateRecordRes = pds_client 240 + .post_json( 241 + "com.atproto.repo.createRecord", 242 + &create_req, 243 + Some(opts.access_jwt), 244 + ) 245 + .await?; 246 + 247 + // Extract rkey from at-uri: at://did/collection/rkey 248 + let rkey = created 249 + .uri 250 + .rsplit('/') 251 + .next() 252 + .ok_or_else(|| anyhow!("failed to parse rkey from uri"))?; 253 + 254 + // 2) Obtain a service auth token for the Tangled server (aud = did:web:<host>) 255 + let host = self 256 + .base_url 257 + .trim_end_matches('/') 258 + .strip_prefix("https://") 259 + .or_else(|| self.base_url.trim_end_matches('/').strip_prefix("http://")) 260 + .ok_or_else(|| anyhow!("invalid base_url"))?; 261 + let audience = format!("did:web:{}", host); 262 + 263 + #[derive(Deserialize)] 264 + struct GetSARes { 265 + token: String, 266 + } 267 + let params = [ 268 + ("aud", audience), 269 + ("exp", (chrono::Utc::now().timestamp() + 600).to_string()), 270 + ]; 271 + let sa: GetSARes = pds_client 272 + .get_json( 273 + "com.atproto.server.getServiceAuth", 274 + &params, 275 + Some(opts.access_jwt), 276 + ) 277 + .await?; 278 + 279 + // 3) Call sh.tangled.repo.create with the rkey 280 + #[derive(Serialize)] 281 + struct CreateRepoReq<'a> { 282 + rkey: &'a str, 283 + #[serde(skip_serializing_if = "Option::is_none")] 284 + #[serde(rename = "defaultBranch")] 285 + default_branch: Option<&'a str>, 286 + #[serde(skip_serializing_if = "Option::is_none")] 287 + source: Option<&'a str>, 288 + } 289 + let req = CreateRepoReq { 290 + rkey, 291 + default_branch: opts.default_branch, 292 + source: opts.source, 293 + }; 294 + // No output expected on success 295 + let _: serde_json::Value = self.post_json(REPO_CREATE, &req, Some(&sa.token)).await?; 296 + Ok(()) 297 + } 298 + 299 + pub async fn get_repo_info( 300 + &self, 301 + owner: &str, 302 + name: &str, 303 + bearer: Option<&str>, 304 + ) -> Result<RepoRecord> { 305 + let did = if owner.starts_with("did:") { 306 + owner.to_string() 307 + } else { 308 + #[derive(Deserialize)] 309 + struct Res { 310 + did: String, 311 + } 312 + let params = [("handle", owner.to_string())]; 313 + let res: Res = self 314 + .get_json("com.atproto.identity.resolveHandle", &params, bearer) 315 + .await?; 316 + res.did 317 + }; 318 + 319 + #[derive(Deserialize)] 320 + struct RecordItem { 321 + uri: String, 322 + value: Repository, 323 + } 324 + #[derive(Deserialize)] 325 + struct ListRes { 326 + #[serde(default)] 327 + records: Vec<RecordItem>, 328 + } 329 + let params = vec![ 330 + ("repo", did.clone()), 331 + ("collection", "sh.tangled.repo".to_string()), 332 + ("limit", "100".to_string()), 333 + ]; 334 + let res: ListRes = self 335 + .get_json("com.atproto.repo.listRecords", &params, bearer) 336 + .await?; 337 + for item in res.records { 338 + if item.value.name == name { 339 + let rkey = 340 + Self::uri_rkey(&item.uri).ok_or_else(|| anyhow!("missing rkey in uri"))?; 341 + let knot = item.value.knot.unwrap_or_default(); 342 + return Ok(RepoRecord { 343 + did: did.clone(), 344 + name: name.to_string(), 345 + rkey, 346 + knot, 347 + description: item.value.description, 348 + }); 349 + } 350 + } 351 + Err(anyhow!("repo not found for owner/name")) 352 + } 353 + 354 + pub async fn delete_repo( 355 + &self, 356 + did: &str, 357 + name: &str, 358 + pds_base: &str, 359 + access_jwt: &str, 360 + ) -> Result<()> { 361 + let pds_client = TangledClient::new(pds_base); 362 + let info = pds_client 363 + .get_repo_info(did, name, Some(access_jwt)) 364 + .await?; 365 + 366 + #[derive(Serialize)] 367 + struct DeleteRecordReq<'a> { 368 + repo: &'a str, 369 + collection: &'a str, 370 + rkey: &'a str, 371 + } 372 + let del = DeleteRecordReq { 373 + repo: did, 374 + collection: "sh.tangled.repo", 375 + rkey: &info.rkey, 376 + }; 377 + let _: serde_json::Value = pds_client 378 + .post_json("com.atproto.repo.deleteRecord", &del, Some(access_jwt)) 379 + .await?; 380 + 381 + let host = self 382 + .base_url 383 + .trim_end_matches('/') 384 + .strip_prefix("https://") 385 + .or_else(|| self.base_url.trim_end_matches('/').strip_prefix("http://")) 386 + .ok_or_else(|| anyhow!("invalid base_url"))?; 387 + let audience = format!("did:web:{}", host); 388 + #[derive(Deserialize)] 389 + struct GetSARes { 390 + token: String, 391 + } 392 + let params = [ 393 + ("aud", audience), 394 + ("exp", (chrono::Utc::now().timestamp() + 600).to_string()), 395 + ]; 396 + let sa: GetSARes = pds_client 397 + .get_json( 398 + "com.atproto.server.getServiceAuth", 399 + &params, 400 + Some(access_jwt), 401 + ) 402 + .await?; 403 + 404 + #[derive(Serialize)] 405 + struct DeleteReq<'a> { 406 + did: &'a str, 407 + name: &'a str, 408 + rkey: &'a str, 409 + } 410 + let body = DeleteReq { 411 + did, 412 + name, 413 + rkey: &info.rkey, 414 + }; 415 + let _: serde_json::Value = self 416 + .post_json("sh.tangled.repo.delete", &body, Some(&sa.token)) 417 + .await?; 418 + Ok(()) 419 + } 420 + 421 + pub async fn update_repo_knot( 422 + &self, 423 + did: &str, 424 + rkey: &str, 425 + new_knot: &str, 426 + pds_base: &str, 427 + access_jwt: &str, 428 + ) -> Result<()> { 429 + let pds_client = TangledClient::new(pds_base); 430 + #[derive(Deserialize, Serialize, Clone)] 431 + struct Rec { 432 + name: String, 433 + knot: String, 434 + #[serde(skip_serializing_if = "Option::is_none")] 435 + description: Option<String>, 436 + #[serde(rename = "createdAt")] 437 + created_at: String, 438 + } 439 + #[derive(Deserialize)] 440 + struct GetRes { 441 + value: Rec, 442 + } 443 + let params = [ 444 + ("repo", did.to_string()), 445 + ("collection", "sh.tangled.repo".to_string()), 446 + ("rkey", rkey.to_string()), 447 + ]; 448 + let got: GetRes = pds_client 449 + .get_json("com.atproto.repo.getRecord", &params, Some(access_jwt)) 450 + .await?; 451 + let mut rec = got.value; 452 + rec.knot = new_knot.to_string(); 453 + #[derive(Serialize)] 454 + struct PutReq<'a> { 455 + repo: &'a str, 456 + collection: &'a str, 457 + rkey: &'a str, 458 + validate: bool, 459 + record: Rec, 460 + } 461 + let req = PutReq { 462 + repo: did, 463 + collection: "sh.tangled.repo", 464 + rkey, 465 + validate: true, 466 + record: rec, 467 + }; 468 + let _: serde_json::Value = pds_client 469 + .post_json("com.atproto.repo.putRecord", &req, Some(access_jwt)) 470 + .await?; 471 + Ok(()) 472 + } 473 + 474 + pub async fn get_default_branch( 475 + &self, 476 + knot_host: &str, 477 + did: &str, 478 + name: &str, 479 + ) -> Result<DefaultBranch> { 480 + #[derive(Deserialize)] 481 + struct Res { 482 + name: String, 483 + hash: String, 484 + #[serde(rename = "shortHash")] 485 + short_hash: Option<String>, 486 + when: String, 487 + message: Option<String>, 488 + } 489 + let knot_client = TangledClient::new(knot_host); 490 + let repo_param = format!("{}/{}", did, name); 491 + let params = [("repo", repo_param)]; 492 + let res: Res = knot_client 493 + .get_json("sh.tangled.repo.getDefaultBranch", &params, None) 494 + .await?; 495 + Ok(DefaultBranch { 496 + name: res.name, 497 + hash: res.hash, 498 + short_hash: res.short_hash, 499 + when: res.when, 500 + message: res.message, 501 + }) 502 + } 503 + 504 + pub async fn get_languages(&self, knot_host: &str, did: &str, name: &str) -> Result<Languages> { 505 + let knot_client = TangledClient::new(knot_host); 506 + let repo_param = format!("{}/{}", did, name); 507 + let params = [("repo", repo_param)]; 508 + let res: serde_json::Value = knot_client 509 + .get_json("sh.tangled.repo.languages", &params, None) 510 + .await?; 511 + let langs = res 512 + .get("languages") 513 + .cloned() 514 + .unwrap_or(serde_json::json!([])); 515 + let languages: Vec<Language> = serde_json::from_value(langs)?; 516 + let total_size = res.get("totalSize").and_then(|v| v.as_u64()); 517 + let total_files = res.get("totalFiles").and_then(|v| v.as_u64()); 518 + Ok(Languages { 519 + languages, 520 + total_size, 521 + total_files, 522 + }) 523 + } 524 + 525 + pub async fn star_repo( 526 + &self, 527 + pds_base: &str, 528 + access_jwt: &str, 529 + subject_at_uri: &str, 530 + user_did: &str, 531 + ) -> Result<String> { 532 + #[derive(Serialize)] 533 + struct Rec<'a> { 534 + subject: &'a str, 535 + #[serde(rename = "createdAt")] 536 + created_at: String, 537 + } 538 + #[derive(Serialize)] 539 + struct Req<'a> { 540 + repo: &'a str, 541 + collection: &'a str, 542 + validate: bool, 543 + record: Rec<'a>, 544 + } 545 + #[derive(Deserialize)] 546 + struct Res { 547 + uri: String, 548 + } 549 + let now = chrono::Utc::now().to_rfc3339(); 550 + let rec = Rec { 551 + subject: subject_at_uri, 552 + created_at: now, 553 + }; 554 + let req = Req { 555 + repo: user_did, 556 + collection: "sh.tangled.feed.star", 557 + validate: true, 558 + record: rec, 559 + }; 560 + let pds_client = TangledClient::new(pds_base); 561 + let res: Res = pds_client 562 + .post_json("com.atproto.repo.createRecord", &req, Some(access_jwt)) 563 + .await?; 564 + let rkey = Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in star uri"))?; 565 + Ok(rkey) 566 + } 567 + 568 + pub async fn unstar_repo( 569 + &self, 570 + pds_base: &str, 571 + access_jwt: &str, 572 + subject_at_uri: &str, 573 + user_did: &str, 574 + ) -> Result<()> { 575 + #[derive(Deserialize)] 576 + struct Item { 577 + uri: String, 578 + value: StarRecord, 579 + } 580 + #[derive(Deserialize)] 581 + struct ListRes { 582 + #[serde(default)] 583 + records: Vec<Item>, 584 + } 585 + let pds_client = TangledClient::new(pds_base); 586 + let params = vec![ 587 + ("repo", user_did.to_string()), 588 + ("collection", "sh.tangled.feed.star".to_string()), 589 + ("limit", "100".to_string()), 590 + ]; 591 + let res: ListRes = pds_client 592 + .get_json("com.atproto.repo.listRecords", &params, Some(access_jwt)) 593 + .await?; 594 + let mut rkey = None; 595 + for item in res.records { 596 + if item.value.subject == subject_at_uri { 597 + rkey = Self::uri_rkey(&item.uri); 598 + if rkey.is_some() { 599 + break; 600 + } 601 + } 602 + } 603 + let rkey = rkey.ok_or_else(|| anyhow!("star record not found"))?; 604 + #[derive(Serialize)] 605 + struct Del<'a> { 606 + repo: &'a str, 607 + collection: &'a str, 608 + rkey: &'a str, 609 + } 610 + let del = Del { 611 + repo: user_did, 612 + collection: "sh.tangled.feed.star", 613 + rkey: &rkey, 614 + }; 615 + let _: serde_json::Value = pds_client 616 + .post_json("com.atproto.repo.deleteRecord", &del, Some(access_jwt)) 617 + .await?; 618 + Ok(()) 619 + } 620 + 621 + fn uri_rkey(uri: &str) -> Option<String> { 622 + uri.rsplit('/').next().map(|s| s.to_string()) 623 + } 624 + fn uri_did(uri: &str) -> Option<String> { 625 + let parts: Vec<&str> = uri.split('/').collect(); 626 + if parts.len() >= 3 { 627 + Some(parts[2].to_string()) 628 + } else { 629 + None 630 + } 631 + } 632 + 633 + // ========== Issues ========== 634 + pub async fn list_issues( 635 + &self, 636 + author_did: &str, 637 + repo_at_uri: Option<&str>, 638 + bearer: Option<&str>, 639 + ) -> Result<Vec<IssueRecord>> { 640 + #[derive(Deserialize)] 641 + struct Item { 642 + uri: String, 643 + value: Issue, 644 + } 645 + #[derive(Deserialize)] 646 + struct ListRes { 647 + #[serde(default)] 648 + records: Vec<Item>, 649 + } 650 + let params = vec![ 651 + ("repo", author_did.to_string()), 652 + ("collection", "sh.tangled.repo.issue".to_string()), 653 + ("limit", "100".to_string()), 654 + ]; 655 + let res: ListRes = self 656 + .get_json("com.atproto.repo.listRecords", &params, bearer) 657 + .await?; 658 + let mut out = vec![]; 659 + for it in res.records { 660 + if let Some(filter_repo) = repo_at_uri { 661 + if it.value.repo.as_str() != filter_repo { 662 + continue; 663 + } 664 + } 665 + let rkey = Self::uri_rkey(&it.uri).unwrap_or_default(); 666 + out.push(IssueRecord { 667 + author_did: author_did.to_string(), 668 + rkey, 669 + issue: it.value, 670 + }); 671 + } 672 + Ok(out) 673 + } 674 + 675 + #[allow(clippy::too_many_arguments)] 676 + pub async fn create_issue( 677 + &self, 678 + author_did: &str, 679 + repo_did: &str, 680 + repo_rkey: &str, 681 + title: &str, 682 + body: Option<&str>, 683 + pds_base: &str, 684 + access_jwt: &str, 685 + ) -> Result<String> { 686 + #[derive(Serialize)] 687 + struct Rec<'a> { 688 + repo: &'a str, 689 + title: &'a str, 690 + #[serde(skip_serializing_if = "Option::is_none")] 691 + body: Option<&'a str>, 692 + #[serde(rename = "createdAt")] 693 + created_at: String, 694 + } 695 + #[derive(Serialize)] 696 + struct Req<'a> { 697 + repo: &'a str, 698 + collection: &'a str, 699 + validate: bool, 700 + record: Rec<'a>, 701 + } 702 + #[derive(Deserialize)] 703 + struct Res { 704 + uri: String, 705 + } 706 + let issue_repo_at = format!("at://{}/sh.tangled.repo/{}", repo_did, repo_rkey); 707 + let now = chrono::Utc::now().to_rfc3339(); 708 + let rec = Rec { 709 + repo: &issue_repo_at, 710 + title, 711 + body, 712 + created_at: now, 713 + }; 714 + let req = Req { 715 + repo: author_did, 716 + collection: "sh.tangled.repo.issue", 717 + validate: true, 718 + record: rec, 719 + }; 720 + let pds_client = TangledClient::new(pds_base); 721 + let res: Res = pds_client 722 + .post_json("com.atproto.repo.createRecord", &req, Some(access_jwt)) 723 + .await?; 724 + Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in issue uri")) 725 + } 726 + 727 + pub async fn comment_issue( 728 + &self, 729 + author_did: &str, 730 + issue_at: &str, 731 + body: &str, 732 + pds_base: &str, 733 + access_jwt: &str, 734 + ) -> Result<String> { 735 + #[derive(Serialize)] 736 + struct Rec<'a> { 737 + issue: &'a str, 738 + body: &'a str, 739 + #[serde(rename = "createdAt")] 740 + created_at: String, 741 + } 742 + #[derive(Serialize)] 743 + struct Req<'a> { 744 + repo: &'a str, 745 + collection: &'a str, 746 + validate: bool, 747 + record: Rec<'a>, 748 + } 749 + #[derive(Deserialize)] 750 + struct Res { 751 + uri: String, 752 + } 753 + let now = chrono::Utc::now().to_rfc3339(); 754 + let rec = Rec { 755 + issue: issue_at, 756 + body, 757 + created_at: now, 758 + }; 759 + let req = Req { 760 + repo: author_did, 761 + collection: "sh.tangled.repo.issue.comment", 762 + validate: true, 763 + record: rec, 764 + }; 765 + let pds_client = TangledClient::new(pds_base); 766 + let res: Res = pds_client 767 + .post_json("com.atproto.repo.createRecord", &req, Some(access_jwt)) 768 + .await?; 769 + Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in issue comment uri")) 770 + } 771 + 772 + pub async fn get_issue_record( 773 + &self, 774 + author_did: &str, 775 + rkey: &str, 776 + bearer: Option<&str>, 777 + ) -> Result<Issue> { 778 + #[derive(Deserialize)] 779 + struct GetRes { 780 + value: Issue, 781 + } 782 + let params = [ 783 + ("repo", author_did.to_string()), 784 + ("collection", "sh.tangled.repo.issue".to_string()), 785 + ("rkey", rkey.to_string()), 786 + ]; 787 + let res: GetRes = self 788 + .get_json("com.atproto.repo.getRecord", &params, bearer) 789 + .await?; 790 + Ok(res.value) 791 + } 792 + 793 + pub async fn put_issue_record( 794 + &self, 795 + author_did: &str, 796 + rkey: &str, 797 + record: &Issue, 798 + bearer: Option<&str>, 799 + ) -> Result<()> { 800 + #[derive(Serialize)] 801 + struct PutReq<'a> { 802 + repo: &'a str, 803 + collection: &'a str, 804 + rkey: &'a str, 805 + validate: bool, 806 + record: &'a Issue, 807 + } 808 + let req = PutReq { 809 + repo: author_did, 810 + collection: "sh.tangled.repo.issue", 811 + rkey, 812 + validate: true, 813 + record, 814 + }; 815 + let _: serde_json::Value = self 816 + .post_json("com.atproto.repo.putRecord", &req, bearer) 817 + .await?; 818 + Ok(()) 819 + } 820 + 821 + pub async fn set_issue_state( 822 + &self, 823 + author_did: &str, 824 + issue_at: &str, 825 + state_nsid: &str, 826 + pds_base: &str, 827 + access_jwt: &str, 828 + ) -> Result<String> { 829 + #[derive(Serialize)] 830 + struct Rec<'a> { 831 + issue: &'a str, 832 + state: &'a str, 833 + } 834 + #[derive(Serialize)] 835 + struct Req<'a> { 836 + repo: &'a str, 837 + collection: &'a str, 838 + validate: bool, 839 + record: Rec<'a>, 840 + } 841 + #[derive(Deserialize)] 842 + struct Res { 843 + uri: String, 844 + } 845 + let rec = Rec { 846 + issue: issue_at, 847 + state: state_nsid, 848 + }; 849 + let req = Req { 850 + repo: author_did, 851 + collection: "sh.tangled.repo.issue.state", 852 + validate: true, 853 + record: rec, 854 + }; 855 + let pds_client = TangledClient::new(pds_base); 856 + let res: Res = pds_client 857 + .post_json("com.atproto.repo.createRecord", &req, Some(access_jwt)) 858 + .await?; 859 + Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in issue state uri")) 860 + } 861 + 862 + pub async fn get_pull_record( 863 + &self, 864 + author_did: &str, 865 + rkey: &str, 866 + bearer: Option<&str>, 867 + ) -> Result<Pull> { 868 + #[derive(Deserialize)] 869 + struct GetRes { 870 + value: Pull, 871 + } 872 + let params = [ 873 + ("repo", author_did.to_string()), 874 + ("collection", "sh.tangled.repo.pull".to_string()), 875 + ("rkey", rkey.to_string()), 876 + ]; 877 + let res: GetRes = self 878 + .get_json("com.atproto.repo.getRecord", &params, bearer) 879 + .await?; 880 + Ok(res.value) 881 + } 882 + 883 + // ========== Pull Requests ========== 884 + pub async fn list_pulls( 885 + &self, 886 + author_did: &str, 887 + target_repo_at_uri: Option<&str>, 888 + bearer: Option<&str>, 889 + ) -> Result<Vec<PullRecord>> { 890 + #[derive(Deserialize)] 891 + struct Item { 892 + uri: String, 893 + value: Pull, 894 + } 895 + #[derive(Deserialize)] 896 + struct ListRes { 897 + #[serde(default)] 898 + records: Vec<Item>, 899 + } 900 + let params = vec![ 901 + ("repo", author_did.to_string()), 902 + ("collection", "sh.tangled.repo.pull".to_string()), 903 + ("limit", "100".to_string()), 904 + ]; 905 + let res: ListRes = self 906 + .get_json("com.atproto.repo.listRecords", &params, bearer) 907 + .await?; 908 + let mut out = vec![]; 909 + for it in res.records { 910 + if let Some(target) = target_repo_at_uri { 911 + if it.value.target.repo.as_str() != target { 912 + continue; 913 + } 914 + } 915 + let rkey = Self::uri_rkey(&it.uri).unwrap_or_default(); 916 + out.push(PullRecord { 917 + author_did: author_did.to_string(), 918 + rkey, 919 + pull: it.value, 920 + }); 921 + } 922 + Ok(out) 923 + } 924 + 925 + #[allow(clippy::too_many_arguments)] 926 + pub async fn create_pull( 927 + &self, 928 + author_did: &str, 929 + repo_did: &str, 930 + repo_rkey: &str, 931 + target_branch: &str, 932 + patch: &str, 933 + title: &str, 934 + body: Option<&str>, 935 + pds_base: &str, 936 + access_jwt: &str, 937 + ) -> Result<String> { 938 + #[derive(Serialize)] 939 + struct Target<'a> { 940 + repo: &'a str, 941 + branch: &'a str, 942 + } 943 + #[derive(Serialize)] 944 + struct Rec<'a> { 945 + target: Target<'a>, 946 + title: &'a str, 947 + #[serde(skip_serializing_if = "Option::is_none")] 948 + body: Option<&'a str>, 949 + patch: &'a str, 950 + #[serde(rename = "createdAt")] 951 + created_at: String, 952 + } 953 + #[derive(Serialize)] 954 + struct Req<'a> { 955 + repo: &'a str, 956 + collection: &'a str, 957 + validate: bool, 958 + record: Rec<'a>, 959 + } 960 + #[derive(Deserialize)] 961 + struct Res { 962 + uri: String, 963 + } 964 + let repo_at = format!("at://{}/sh.tangled.repo/{}", repo_did, repo_rkey); 965 + let now = chrono::Utc::now().to_rfc3339(); 966 + let rec = Rec { 967 + target: Target { 968 + repo: &repo_at, 969 + branch: target_branch, 970 + }, 971 + title, 972 + body, 973 + patch, 974 + created_at: now, 975 + }; 976 + let req = Req { 977 + repo: author_did, 978 + collection: "sh.tangled.repo.pull", 979 + validate: true, 980 + record: rec, 981 + }; 982 + let pds_client = TangledClient::new(pds_base); 983 + let res: Res = pds_client 984 + .post_json("com.atproto.repo.createRecord", &req, Some(access_jwt)) 985 + .await?; 986 + Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in pull uri")) 987 + } 988 + 989 + // ========== Spindle: Secrets Management ========== 990 + pub async fn list_repo_secrets( 991 + &self, 992 + pds_base: &str, 993 + access_jwt: &str, 994 + repo_at: &str, 995 + ) -> Result<Vec<Secret>> { 996 + let sa = self.service_auth_token(pds_base, access_jwt).await?; 997 + #[derive(Deserialize)] 998 + struct Res { 999 + secrets: Vec<Secret>, 1000 + } 1001 + let params = [("repo", repo_at.to_string())]; 1002 + let res: Res = self 1003 + .get_json("sh.tangled.repo.listSecrets", &params, Some(&sa)) 1004 + .await?; 1005 + Ok(res.secrets) 13 1006 } 14 1007 15 - pub fn default() -> Self { 16 - Self::new("https://tangled.org") 1008 + pub async fn add_repo_secret( 1009 + &self, 1010 + pds_base: &str, 1011 + access_jwt: &str, 1012 + repo_at: &str, 1013 + key: &str, 1014 + value: &str, 1015 + ) -> Result<()> { 1016 + let sa = self.service_auth_token(pds_base, access_jwt).await?; 1017 + #[derive(Serialize)] 1018 + struct Req<'a> { 1019 + repo: &'a str, 1020 + key: &'a str, 1021 + value: &'a str, 1022 + } 1023 + let body = Req { 1024 + repo: repo_at, 1025 + key, 1026 + value, 1027 + }; 1028 + let _: serde_json::Value = self 1029 + .post_json("sh.tangled.repo.addSecret", &body, Some(&sa)) 1030 + .await?; 1031 + Ok(()) 17 1032 } 18 1033 19 - pub async fn login_with_password(&self, _handle: &str, _password: &str, _pds: &str) -> Result<Session> { 20 - // TODO: implement via com.atproto.server.createSession 21 - bail!("login_with_password not implemented") 1034 + pub async fn remove_repo_secret( 1035 + &self, 1036 + pds_base: &str, 1037 + access_jwt: &str, 1038 + repo_at: &str, 1039 + key: &str, 1040 + ) -> Result<()> { 1041 + let sa = self.service_auth_token(pds_base, access_jwt).await?; 1042 + #[derive(Serialize)] 1043 + struct Req<'a> { 1044 + repo: &'a str, 1045 + key: &'a str, 1046 + } 1047 + let body = Req { repo: repo_at, key }; 1048 + let _: serde_json::Value = self 1049 + .post_json("sh.tangled.repo.removeSecret", &body, Some(&sa)) 1050 + .await?; 1051 + Ok(()) 22 1052 } 23 1053 24 - pub async fn list_repos(&self, _user: Option<&str>, _knot: Option<&str>, _starred: bool) -> Result<Vec<Repository>> { 25 - // TODO: implement XRPC sh.tangled.repo.list 26 - Ok(vec![]) 1054 + async fn service_auth_token(&self, pds_base: &str, access_jwt: &str) -> Result<String> { 1055 + let host = self 1056 + .base_url 1057 + .trim_end_matches('/') 1058 + .strip_prefix("https://") 1059 + .or_else(|| self.base_url.trim_end_matches('/').strip_prefix("http://")) 1060 + .ok_or_else(|| anyhow!("invalid base_url"))?; 1061 + let audience = format!("did:web:{}", host); 1062 + #[derive(Deserialize)] 1063 + struct GetSARes { 1064 + token: String, 1065 + } 1066 + let pds = TangledClient::new(pds_base); 1067 + let params = [ 1068 + ("aud", audience), 1069 + ("exp", (chrono::Utc::now().timestamp() + 600).to_string()), 1070 + ]; 1071 + let sa: GetSARes = pds 1072 + .get_json( 1073 + "com.atproto.server.getServiceAuth", 1074 + &params, 1075 + Some(access_jwt), 1076 + ) 1077 + .await?; 1078 + Ok(sa.token) 1079 + } 1080 + 1081 + pub async fn comment_pull( 1082 + &self, 1083 + author_did: &str, 1084 + pull_at: &str, 1085 + body: &str, 1086 + pds_base: &str, 1087 + access_jwt: &str, 1088 + ) -> Result<String> { 1089 + #[derive(Serialize)] 1090 + struct Rec<'a> { 1091 + pull: &'a str, 1092 + body: &'a str, 1093 + #[serde(rename = "createdAt")] 1094 + created_at: String, 1095 + } 1096 + #[derive(Serialize)] 1097 + struct Req<'a> { 1098 + repo: &'a str, 1099 + collection: &'a str, 1100 + validate: bool, 1101 + record: Rec<'a>, 1102 + } 1103 + #[derive(Deserialize)] 1104 + struct Res { 1105 + uri: String, 1106 + } 1107 + let now = chrono::Utc::now().to_rfc3339(); 1108 + let rec = Rec { 1109 + pull: pull_at, 1110 + body, 1111 + created_at: now, 1112 + }; 1113 + let req = Req { 1114 + repo: author_did, 1115 + collection: "sh.tangled.repo.pull.comment", 1116 + validate: true, 1117 + record: rec, 1118 + }; 1119 + let pds_client = TangledClient::new(pds_base); 1120 + let res: Res = pds_client 1121 + .post_json("com.atproto.repo.createRecord", &req, Some(access_jwt)) 1122 + .await?; 1123 + Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in pull comment uri")) 27 1124 } 28 1125 } 29 1126 ··· 34 1131 pub name: String, 35 1132 pub knot: Option<String>, 36 1133 pub description: Option<String>, 1134 + #[serde(default)] 37 1135 pub private: bool, 38 1136 } 39 1137 1138 + // Issue record value 1139 + #[derive(Debug, Clone, Serialize, Deserialize)] 1140 + pub struct Issue { 1141 + pub repo: String, 1142 + pub title: String, 1143 + #[serde(default)] 1144 + pub body: String, 1145 + #[serde(rename = "createdAt")] 1146 + pub created_at: String, 1147 + } 1148 + 1149 + #[derive(Debug, Clone)] 1150 + pub struct IssueRecord { 1151 + pub author_did: String, 1152 + pub rkey: String, 1153 + pub issue: Issue, 1154 + } 1155 + 1156 + // Pull record value (subset) 1157 + #[derive(Debug, Clone, Serialize, Deserialize)] 1158 + pub struct PullTarget { 1159 + pub repo: String, 1160 + pub branch: String, 1161 + } 1162 + 1163 + #[derive(Debug, Clone, Serialize, Deserialize)] 1164 + pub struct Pull { 1165 + pub target: PullTarget, 1166 + pub title: String, 1167 + #[serde(default)] 1168 + pub body: String, 1169 + pub patch: String, 1170 + #[serde(rename = "createdAt")] 1171 + pub created_at: String, 1172 + } 1173 + 1174 + #[derive(Debug, Clone)] 1175 + pub struct PullRecord { 1176 + pub author_did: String, 1177 + pub rkey: String, 1178 + pub pull: Pull, 1179 + } 1180 + 1181 + #[derive(Debug, Clone)] 1182 + pub struct RepoRecord { 1183 + pub did: String, 1184 + pub name: String, 1185 + pub rkey: String, 1186 + pub knot: String, 1187 + pub description: Option<String>, 1188 + } 1189 + 1190 + #[derive(Debug, Clone, Serialize, Deserialize)] 1191 + pub struct DefaultBranch { 1192 + pub name: String, 1193 + pub hash: String, 1194 + #[serde(skip_serializing_if = "Option::is_none")] 1195 + pub short_hash: Option<String>, 1196 + pub when: String, 1197 + #[serde(skip_serializing_if = "Option::is_none")] 1198 + pub message: Option<String>, 1199 + } 1200 + 1201 + #[derive(Debug, Clone, Serialize, Deserialize)] 1202 + pub struct Language { 1203 + pub name: String, 1204 + pub size: u64, 1205 + pub percentage: u64, 1206 + } 1207 + 1208 + #[derive(Debug, Clone, Serialize, Deserialize)] 1209 + pub struct Languages { 1210 + pub languages: Vec<Language>, 1211 + #[serde(skip_serializing_if = "Option::is_none")] 1212 + pub total_size: Option<u64>, 1213 + #[serde(skip_serializing_if = "Option::is_none")] 1214 + pub total_files: Option<u64>, 1215 + } 1216 + 1217 + #[derive(Debug, Clone, Serialize, Deserialize)] 1218 + pub struct StarRecord { 1219 + pub subject: String, 1220 + #[serde(rename = "createdAt")] 1221 + pub created_at: String, 1222 + } 1223 + 1224 + #[derive(Debug, Clone, Serialize, Deserialize)] 1225 + pub struct Secret { 1226 + pub repo: String, 1227 + pub key: String, 1228 + #[serde(rename = "createdAt")] 1229 + pub created_at: String, 1230 + #[serde(rename = "createdBy")] 1231 + pub created_by: String, 1232 + } 1233 + 1234 + #[derive(Debug, Clone)] 1235 + pub struct CreateRepoOptions<'a> { 1236 + pub did: &'a str, 1237 + pub name: &'a str, 1238 + pub knot: &'a str, 1239 + pub description: Option<&'a str>, 1240 + pub default_branch: Option<&'a str>, 1241 + pub source: Option<&'a str>, 1242 + pub pds_base: &'a str, 1243 + pub access_jwt: &'a str, 1244 + }
+4 -1
crates/tangled-api/src/lib.rs
··· 1 1 pub mod client; 2 2 3 3 pub use client::TangledClient; 4 - 4 + pub use client::{ 5 + CreateRepoOptions, DefaultBranch, Issue, IssueRecord, Language, Languages, Pull, PullRecord, 6 + RepoRecord, Repository, Secret, 7 + };
+2 -1
crates/tangled-cli/Cargo.toml
··· 14 14 serde = { workspace = true, features = ["derive"] } 15 15 serde_json = { workspace = true } 16 16 tokio = { workspace = true, features = ["full"] } 17 + git2 = { workspace = true } 18 + url = { workspace = true } 17 19 18 20 # Internal crates 19 21 tangled-config = { path = "../tangled-config" } 20 22 tangled-api = { path = "../tangled-api" } 21 23 tangled-git = { path = "../tangled-git" } 22 -
+69
crates/tangled-cli/src/cli.rs
··· 40 40 #[derive(Subcommand, Debug, Clone)] 41 41 pub enum Command { 42 42 /// Authentication commands 43 + #[command(subcommand)] 43 44 Auth(AuthCommand), 44 45 /// Repository commands 46 + #[command(subcommand)] 45 47 Repo(RepoCommand), 46 48 /// Issue commands 49 + #[command(subcommand)] 47 50 Issue(IssueCommand), 48 51 /// Pull request commands 52 + #[command(subcommand)] 49 53 Pr(PrCommand), 50 54 /// Knot management commands 55 + #[command(subcommand)] 51 56 Knot(KnotCommand), 52 57 /// Spindle integration commands 58 + #[command(subcommand)] 53 59 Spindle(SpindleCommand), 54 60 } 55 61 ··· 102 108 pub user: Option<String>, 103 109 #[arg(long, default_value_t = false)] 104 110 pub starred: bool, 111 + /// Tangled API base URL (overrides env) 112 + #[arg(long)] 113 + pub base: Option<String>, 105 114 } 106 115 107 116 #[derive(Args, Debug, Clone)] ··· 290 299 Verify(KnotVerifyArgs), 291 300 SetDefault(KnotRefArgs), 292 301 Remove(KnotRefArgs), 302 + /// Migrate a repository to another knot 303 + Migrate(KnotMigrateArgs), 293 304 } 294 305 295 306 #[derive(Args, Debug, Clone)] ··· 321 332 pub url: String, 322 333 } 323 334 335 + #[derive(Args, Debug, Clone)] 336 + pub struct KnotMigrateArgs { 337 + /// Repo to migrate: <owner>/<name> (owner defaults to your handle) 338 + #[arg(long)] 339 + pub repo: String, 340 + /// Target knot hostname (e.g. knot1.tangled.sh) 341 + #[arg(long, value_name = "HOST")] 342 + pub to: String, 343 + /// Use HTTPS source when seeding new repo 344 + #[arg(long, default_value_t = true)] 345 + pub https: bool, 346 + /// Update PDS record knot field after seeding 347 + #[arg(long, default_value_t = true)] 348 + pub update_record: bool, 349 + } 350 + 324 351 #[derive(Subcommand, Debug, Clone)] 325 352 pub enum SpindleCommand { 326 353 List(SpindleListArgs), 327 354 Config(SpindleConfigArgs), 328 355 Run(SpindleRunArgs), 329 356 Logs(SpindleLogsArgs), 357 + /// Secrets management 358 + #[command(subcommand)] 359 + Secret(SpindleSecretCommand), 330 360 } 331 361 332 362 #[derive(Args, Debug, Clone)] ··· 366 396 pub lines: Option<usize>, 367 397 } 368 398 399 + #[derive(Subcommand, Debug, Clone)] 400 + pub enum SpindleSecretCommand { 401 + /// List secrets for a repo 402 + List(SpindleSecretListArgs), 403 + /// Add or update a secret 404 + Add(SpindleSecretAddArgs), 405 + /// Remove a secret 406 + Remove(SpindleSecretRemoveArgs), 407 + } 408 + 409 + #[derive(Args, Debug, Clone)] 410 + pub struct SpindleSecretListArgs { 411 + /// Repo: <owner>/<name> 412 + #[arg(long)] 413 + pub repo: String, 414 + } 415 + 416 + #[derive(Args, Debug, Clone)] 417 + pub struct SpindleSecretAddArgs { 418 + /// Repo: <owner>/<name> 419 + #[arg(long)] 420 + pub repo: String, 421 + /// Secret key 422 + #[arg(long)] 423 + pub key: String, 424 + /// Secret value 425 + #[arg(long)] 426 + pub value: String, 427 + } 428 + 429 + #[derive(Args, Debug, Clone)] 430 + pub struct SpindleSecretRemoveArgs { 431 + /// Repo: <owner>/<name> 432 + #[arg(long)] 433 + pub repo: String, 434 + /// Secret key 435 + #[arg(long)] 436 + pub key: String, 437 + }
+21 -17
crates/tangled-cli/src/commands/auth.rs
··· 1 1 use anyhow::Result; 2 2 use dialoguer::{Input, Password}; 3 + use tangled_config::session::SessionManager; 3 4 4 5 use crate::cli::{AuthCommand, AuthLoginArgs, Cli}; 5 6 ··· 20 21 Some(p) => p, 21 22 None => Password::new().with_prompt("Password").interact()?, 22 23 }; 23 - let pds = args.pds.unwrap_or_else(|| "https://bsky.social".to_string()); 24 - 25 - // Placeholder: integrate tangled_api authentication here 26 - println!( 27 - "Logging in as '{}' against PDS '{}'... (stub)", 28 - handle, pds 29 - ); 30 - 31 - // Example future flow: 32 - // let client = tangled_api::TangledClient::new(&pds); 33 - // let session = client.login(&handle, &password).await?; 34 - // tangled_config::session::SessionManager::default().save(&session)?; 24 + let pds = args 25 + .pds 26 + .unwrap_or_else(|| "https://bsky.social".to_string()); 35 27 28 + let client = tangled_api::TangledClient::new(&pds); 29 + let mut session = client.login_with_password(&handle, &password, &pds).await?; 30 + session.pds = Some(pds.clone()); 31 + SessionManager::default().save(&session)?; 32 + println!("Logged in as '{}' ({})", session.handle, session.did); 36 33 Ok(()) 37 34 } 38 35 39 36 async fn status(_cli: &Cli) -> Result<()> { 40 - // Placeholder: read session from keyring/config 41 - println!("Authentication status: (stub) not implemented"); 37 + let mgr = SessionManager::default(); 38 + match mgr.load()? { 39 + Some(s) => println!("Logged in as '{}' ({})", s.handle, s.did), 40 + None => println!("Not logged in. Run: tangled auth login"), 41 + } 42 42 Ok(()) 43 43 } 44 44 45 45 async fn logout(_cli: &Cli) -> Result<()> { 46 - // Placeholder: remove session from keyring/config 47 - println!("Logged out (stub)"); 46 + let mgr = SessionManager::default(); 47 + if mgr.load()?.is_some() { 48 + mgr.clear()?; 49 + println!("Logged out"); 50 + } else { 51 + println!("No session found"); 52 + } 48 53 Ok(()) 49 54 } 50 -
+211 -10
crates/tangled-cli/src/commands/issue.rs
··· 1 - use anyhow::Result; 2 - use crate::cli::{Cli, IssueCommand, IssueListArgs, IssueCreateArgs, IssueShowArgs, IssueEditArgs, IssueCommentArgs}; 1 + use crate::cli::{ 2 + Cli, IssueCommand, IssueCommentArgs, IssueCreateArgs, IssueEditArgs, IssueListArgs, 3 + IssueShowArgs, 4 + }; 5 + use anyhow::{anyhow, Result}; 6 + use tangled_api::Issue; 7 + use tangled_config::session::SessionManager; 3 8 4 9 pub async fn run(_cli: &Cli, cmd: IssueCommand) -> Result<()> { 5 10 match cmd { ··· 12 17 } 13 18 14 19 async fn list(args: IssueListArgs) -> Result<()> { 15 - println!("Issue list (stub) repo={:?} state={:?} author={:?} label={:?} assigned={:?}", 16 - args.repo, args.state, args.author, args.label, args.assigned); 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() 27 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 28 + .unwrap_or_else(|| "https://bsky.social".into()); 29 + let client = tangled_api::TangledClient::new(&pds); 30 + 31 + let repo_filter_at = if let Some(repo) = &args.repo { 32 + let (owner, name) = parse_repo_ref(repo, &session.handle); 33 + let info = client 34 + .get_repo_info(owner, name, Some(session.access_jwt.as_str())) 35 + .await?; 36 + Some(format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey)) 37 + } else { 38 + None 39 + }; 40 + 41 + let items = client 42 + .list_issues( 43 + &session.did, 44 + repo_filter_at.as_deref(), 45 + Some(session.access_jwt.as_str()), 46 + ) 47 + .await?; 48 + if items.is_empty() { 49 + println!("No issues found (showing only issues you created)"); 50 + } else { 51 + println!("RKEY\tTITLE\tREPO"); 52 + for it in items { 53 + println!("{}\t{}\t{}", it.rkey, it.issue.title, it.issue.repo); 54 + } 55 + } 17 56 Ok(()) 18 57 } 19 58 20 59 async fn create(args: IssueCreateArgs) -> Result<()> { 21 - println!("Issue create (stub) repo={:?} title={:?} body={:?} labels={:?} assign={:?}", 22 - args.repo, args.title, args.body, args.label, args.assign); 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() 67 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 68 + .unwrap_or_else(|| "https://bsky.social".into()); 69 + let client = tangled_api::TangledClient::new(&pds); 70 + 71 + let repo = args 72 + .repo 73 + .as_ref() 74 + .ok_or_else(|| anyhow!("--repo is required for issue create"))?; 75 + let (owner, name) = parse_repo_ref(repo, &session.handle); 76 + let info = client 77 + .get_repo_info(owner, name, Some(session.access_jwt.as_str())) 78 + .await?; 79 + let title = args 80 + .title 81 + .as_deref() 82 + .ok_or_else(|| anyhow!("--title is required for issue create"))?; 83 + let rkey = client 84 + .create_issue( 85 + &session.did, 86 + &info.did, 87 + &info.rkey, 88 + title, 89 + args.body.as_deref(), 90 + &pds, 91 + &session.access_jwt, 92 + ) 93 + .await?; 94 + println!("Created issue rkey={} in {}/{}", rkey, owner, name); 23 95 Ok(()) 24 96 } 25 97 26 98 async fn show(args: IssueShowArgs) -> Result<()> { 27 - println!("Issue show (stub) id={} comments={} json={}", args.id, args.comments, args.json); 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 107 + .pds 108 + .clone() 109 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 110 + .unwrap_or_else(|| "https://bsky.social".into()); 111 + let client = tangled_api::TangledClient::new(&pds); 112 + // Fetch all issues by this DID and find rkey 113 + let items = client 114 + .list_issues(&did, None, Some(session.access_jwt.as_str())) 115 + .await?; 116 + if let Some(it) = items.into_iter().find(|i| i.rkey == rkey) { 117 + println!("TITLE: {}", it.issue.title); 118 + if !it.issue.body.is_empty() { 119 + println!("BODY:\n{}", it.issue.body); 120 + } 121 + println!("REPO: {}", it.issue.repo); 122 + println!("AUTHOR: {}", it.author_did); 123 + println!("RKEY: {}", rkey); 124 + } else { 125 + println!("Issue not found for did={} rkey={}", did, rkey); 126 + } 28 127 Ok(()) 29 128 } 30 129 31 130 async fn edit(args: IssueEditArgs) -> Result<()> { 32 - println!("Issue edit (stub) id={} title={:?} body={:?} state={:?}", 33 - args.id, args.title, args.body, args.state); 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 139 + .clone() 140 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 141 + .unwrap_or_else(|| "https://bsky.social".into()); 142 + // Get existing 143 + let client = tangled_api::TangledClient::new(&pds); 144 + let mut rec: Issue = client 145 + .get_issue_record(&did, &rkey, Some(session.access_jwt.as_str())) 146 + .await?; 147 + if let Some(t) = args.title.as_deref() { 148 + rec.title = t.to_string(); 149 + } 150 + if let Some(b) = args.body.as_deref() { 151 + rec.body = b.to_string(); 152 + } 153 + // Put record back 154 + client 155 + .put_issue_record(&did, &rkey, &rec, Some(session.access_jwt.as_str())) 156 + .await?; 157 + 158 + // Optional state change 159 + if let Some(state) = args.state.as_deref() { 160 + let state_nsid = match state { 161 + "open" => "sh.tangled.repo.issue.state.open", 162 + "closed" => "sh.tangled.repo.issue.state.closed", 163 + other => { 164 + return Err(anyhow!(format!( 165 + "unknown state '{}', expected 'open' or 'closed'", 166 + other 167 + ))) 168 + } 169 + }; 170 + let issue_at = rec.repo.clone(); 171 + client 172 + .set_issue_state( 173 + &session.did, 174 + &issue_at, 175 + state_nsid, 176 + &pds, 177 + &session.access_jwt, 178 + ) 179 + .await?; 180 + } 181 + println!("Updated issue {}:{}", did, rkey); 34 182 Ok(()) 35 183 } 36 184 37 185 async fn comment(args: IssueCommentArgs) -> Result<()> { 38 - println!("Issue comment (stub) id={} close={} body={:?}", args.id, args.close, args.body); 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 193 + .clone() 194 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 195 + .unwrap_or_else(|| "https://bsky.social".into()); 196 + // Resolve issue AT-URI 197 + let client = tangled_api::TangledClient::new(&pds); 198 + let issue_at = client 199 + .get_issue_record(&did, &rkey, Some(session.access_jwt.as_str())) 200 + .await? 201 + .repo; 202 + if let Some(body) = args.body.as_deref() { 203 + client 204 + .comment_issue(&session.did, &issue_at, body, &pds, &session.access_jwt) 205 + .await?; 206 + println!("Comment posted"); 207 + } 208 + if args.close { 209 + client 210 + .set_issue_state( 211 + &session.did, 212 + &issue_at, 213 + "sh.tangled.repo.issue.state.closed", 214 + &pds, 215 + &session.access_jwt, 216 + ) 217 + .await?; 218 + println!("Issue closed"); 219 + } 39 220 Ok(()) 40 221 } 41 222 223 + fn parse_repo_ref<'a>(spec: &'a str, default_owner: &'a str) -> (&'a str, &'a str) { 224 + if let Some((owner, name)) = spec.split_once('/') { 225 + (owner, name) 226 + } else { 227 + (default_owner, spec) 228 + } 229 + } 230 + 231 + fn parse_record_id<'a>(id: &'a str, default_did: &'a str) -> Result<(String, String)> { 232 + if let Some(rest) = id.strip_prefix("at://") { 233 + let parts: Vec<&str> = rest.split('/').collect(); 234 + if parts.len() >= 4 { 235 + return Ok((parts[0].to_string(), parts[3].to_string())); 236 + } 237 + } 238 + if let Some((did, rkey)) = id.split_once(':') { 239 + return Ok((did.to_string(), rkey.to_string())); 240 + } 241 + Ok((default_did.to_string(), id.to_string())) 242 + }
+197 -3
crates/tangled-cli/src/commands/knot.rs
··· 1 + use crate::cli::{ 2 + Cli, KnotAddArgs, KnotCommand, KnotListArgs, KnotMigrateArgs, KnotRefArgs, KnotVerifyArgs, 3 + }; 4 + use anyhow::anyhow; 1 5 use anyhow::Result; 2 - use crate::cli::{Cli, KnotCommand, KnotListArgs, KnotAddArgs, KnotVerifyArgs, KnotRefArgs}; 6 + use git2::{Direction, Repository as GitRepository, StatusOptions}; 7 + use std::path::Path; 8 + use tangled_config::session::SessionManager; 3 9 4 10 pub async fn run(_cli: &Cli, cmd: KnotCommand) -> Result<()> { 5 11 match cmd { ··· 8 14 KnotCommand::Verify(args) => verify(args).await, 9 15 KnotCommand::SetDefault(args) => set_default(args).await, 10 16 KnotCommand::Remove(args) => remove(args).await, 17 + KnotCommand::Migrate(args) => migrate(args).await, 11 18 } 12 19 } 13 20 14 21 async fn list(args: KnotListArgs) -> Result<()> { 15 - println!("Knot list (stub) public={} owned={}", args.public, args.owned); 22 + println!( 23 + "Knot list (stub) public={} owned={}", 24 + args.public, args.owned 25 + ); 16 26 Ok(()) 17 27 } 18 28 19 29 async fn add(args: KnotAddArgs) -> Result<()> { 20 - println!("Knot add (stub) url={} did={:?} name={:?} verify={}", args.url, args.did, args.name, args.verify); 30 + println!( 31 + "Knot add (stub) url={} did={:?} name={:?} verify={}", 32 + args.url, args.did, args.name, args.verify 33 + ); 21 34 Ok(()) 22 35 } 23 36 ··· 36 49 Ok(()) 37 50 } 38 51 52 + async fn migrate(args: KnotMigrateArgs) -> Result<()> { 53 + let mgr = SessionManager::default(); 54 + let session = mgr 55 + .load()? 56 + .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 57 + // 1) Ensure we're inside a git repository and working tree is clean 58 + let repo = GitRepository::discover(Path::new("."))?; 59 + let mut status_opts = StatusOptions::new(); 60 + status_opts.include_untracked(false).include_ignored(false); 61 + let statuses = repo.statuses(Some(&mut status_opts))?; 62 + if !statuses.is_empty() { 63 + return Err(anyhow!( 64 + "working tree has uncommitted changes; commit/push before migrating" 65 + )); 66 + } 67 + 68 + // 2) Derive current branch and ensure it's pushed to origin 69 + let head = match repo.head() { 70 + Ok(h) => h, 71 + Err(_) => return Err(anyhow!("repository does not have a HEAD")), 72 + }; 73 + let head_oid = head 74 + .target() 75 + .ok_or_else(|| anyhow!("failed to resolve HEAD OID"))?; 76 + let head_name = head.shorthand().unwrap_or(""); 77 + let full_ref = head.name().unwrap_or("").to_string(); 78 + if !full_ref.starts_with("refs/heads/") { 79 + return Err(anyhow!( 80 + "HEAD is detached; please checkout a branch before migrating" 81 + )); 82 + } 83 + let branch = head_name.to_string(); 84 + 85 + let origin = repo.find_remote("origin").or_else(|_| { 86 + repo.remotes().and_then(|rems| { 87 + rems.get(0) 88 + .ok_or(git2::Error::from_str("no remotes configured")) 89 + .and_then(|name| repo.find_remote(name)) 90 + }) 91 + })?; 92 + 93 + // Connect and list remote heads to find refs/heads/<branch> 94 + let mut remote = origin; 95 + remote.connect(Direction::Fetch)?; 96 + let remote_heads = remote.list()?; 97 + let remote_oid = remote_heads 98 + .iter() 99 + .find_map(|h| { 100 + if h.name() == format!("refs/heads/{}", branch) { 101 + Some(h.oid()) 102 + } else { 103 + None 104 + } 105 + }) 106 + .ok_or_else(|| anyhow!("origin does not have branch '{}' โ€” push first", branch))?; 107 + if remote_oid != head_oid { 108 + return Err(anyhow!( 109 + "local {} ({}) != origin {} ({}); please push before migrating", 110 + branch, 111 + head_oid, 112 + branch, 113 + remote_oid 114 + )); 115 + } 116 + 117 + // 3) Parse origin URL to verify repo identity 118 + let origin_url = remote 119 + .url() 120 + .ok_or_else(|| anyhow!("origin has no URL"))? 121 + .to_string(); 122 + let (origin_owner, origin_name, _origin_host) = parse_remote_url(&origin_url) 123 + .ok_or_else(|| anyhow!("unsupported origin URL: {}", origin_url))?; 124 + 125 + let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 126 + if origin_owner.trim_start_matches('@') != owner.trim_start_matches('@') || origin_name != name 127 + { 128 + return Err(anyhow!( 129 + "repo mismatch: current checkout '{}'/{} != argument '{}'/{}", 130 + origin_owner, 131 + origin_name, 132 + owner, 133 + name 134 + )); 135 + } 136 + 137 + let pds = session 138 + .pds 139 + .clone() 140 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 141 + .unwrap_or_else(|| "https://bsky.social".into()); 142 + let pds_client = tangled_api::TangledClient::new(&pds); 143 + let info = pds_client 144 + .get_repo_info(owner, &name, Some(session.access_jwt.as_str())) 145 + .await?; 146 + 147 + // Build a publicly accessible source URL on tangled.org for the existing repo 148 + let owner_path = if owner.starts_with('@') { 149 + owner.to_string() 150 + } else { 151 + format!("@{}", owner) 152 + }; 153 + let source = if args.https { 154 + format!("https://tangled.org/{}/{}", owner_path, name) 155 + } else { 156 + format!( 157 + "git@{}:{}/{}", 158 + info.knot, 159 + owner.trim_start_matches('@'), 160 + name 161 + ) 162 + }; 163 + 164 + // Create the repo on the target knot, seeding from source 165 + let client = tangled_api::TangledClient::default(); 166 + let opts = tangled_api::client::CreateRepoOptions { 167 + did: &session.did, 168 + name: &name, 169 + knot: &args.to, 170 + description: info.description.as_deref(), 171 + default_branch: None, 172 + source: Some(&source), 173 + pds_base: &pds, 174 + access_jwt: &session.access_jwt, 175 + }; 176 + client.create_repo(opts).await?; 177 + 178 + // Update the PDS record to point to the new knot 179 + if args.update_record { 180 + client 181 + .update_repo_knot( 182 + &session.did, 183 + &info.rkey, 184 + &args.to, 185 + &pds, 186 + &session.access_jwt, 187 + ) 188 + .await?; 189 + } 190 + 191 + println!("Migrated repo '{}' to knot {}", name, args.to); 192 + println!( 193 + "Note: old repository on {} is not deleted automatically.", 194 + info.knot 195 + ); 196 + Ok(()) 197 + } 198 + 199 + fn parse_repo_ref<'a>(spec: &'a str, default_owner: &'a str) -> (&'a str, String) { 200 + if let Some((owner, name)) = spec.split_once('/') { 201 + (owner, name.to_string()) 202 + } else { 203 + (default_owner, spec.to_string()) 204 + } 205 + } 206 + 207 + fn parse_remote_url(url: &str) -> Option<(String, String, String)> { 208 + // Returns (owner, name, host) 209 + if let Some(rest) = url.strip_prefix("git@") { 210 + // git@host:owner/name(.git) 211 + let mut parts = rest.split(':'); 212 + let host = parts.next()?.to_string(); 213 + let path = parts.next()?; 214 + let mut segs = path.trim_end_matches(".git").split('/'); 215 + let owner = segs.next()?.to_string(); 216 + let name = segs.next()?.to_string(); 217 + return Some((owner, name, host)); 218 + } 219 + if url.starts_with("http://") || url.starts_with("https://") { 220 + if let Ok(parsed) = url::Url::parse(url) { 221 + let host = parsed.host_str().unwrap_or("").to_string(); 222 + let path = parsed.path().trim_matches('/'); 223 + // paths may be like '@owner/name' or 'owner/name' 224 + let mut segs = path.trim_end_matches(".git").split('/'); 225 + let first = segs.next()?; 226 + let owner = first.trim_start_matches('@').to_string(); 227 + let name = segs.next()?.to_string(); 228 + return Some((owner, name, host)); 229 + } 230 + } 231 + None 232 + }
+10 -15
crates/tangled-cli/src/commands/mod.rs
··· 1 1 pub mod auth; 2 - pub mod repo; 3 2 pub mod issue; 4 - pub mod pr; 5 3 pub mod knot; 4 + pub mod pr; 5 + pub mod repo; 6 6 pub mod spindle; 7 7 8 8 use anyhow::Result; 9 - use colored::Colorize; 10 9 11 10 use crate::cli::{Cli, Command}; 12 11 13 12 pub async fn dispatch(cli: Cli) -> Result<()> { 14 - match cli.command { 15 - Command::Auth(cmd) => auth::run(&cli, cmd).await, 16 - Command::Repo(cmd) => repo::run(&cli, cmd).await, 17 - Command::Issue(cmd) => issue::run(&cli, cmd).await, 18 - Command::Pr(cmd) => pr::run(&cli, cmd).await, 19 - Command::Knot(cmd) => knot::run(&cli, cmd).await, 20 - Command::Spindle(cmd) => spindle::run(&cli, cmd).await, 13 + match &cli.command { 14 + Command::Auth(cmd) => auth::run(&cli, cmd.clone()).await, 15 + Command::Repo(cmd) => repo::run(&cli, cmd.clone()).await, 16 + Command::Issue(cmd) => issue::run(&cli, cmd.clone()).await, 17 + Command::Pr(cmd) => pr::run(&cli, cmd.clone()).await, 18 + Command::Knot(cmd) => knot::run(&cli, cmd.clone()).await, 19 + Command::Spindle(cmd) => spindle::run(&cli, cmd.clone()).await, 21 20 } 22 21 } 23 22 24 - fn not_implemented(feature: &str) -> Result<()> { 25 - eprintln!("{} {}", "[todo]".yellow().bold(), feature); 26 - Ok(()) 27 - } 28 - 23 + // All subcommands are currently implemented with stubs where needed.
+185 -12
crates/tangled-cli/src/commands/pr.rs
··· 1 - use anyhow::Result; 2 - use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrShowArgs, PrReviewArgs, PrMergeArgs}; 1 + use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrMergeArgs, PrReviewArgs, PrShowArgs}; 2 + use anyhow::{anyhow, Result}; 3 + use std::path::Path; 4 + use std::process::Command; 5 + use tangled_config::session::SessionManager; 3 6 4 7 pub async fn run(_cli: &Cli, cmd: PrCommand) -> Result<()> { 5 8 match cmd { ··· 12 15 } 13 16 14 17 async fn list(args: PrListArgs) -> Result<()> { 15 - println!("PR list (stub) repo={:?} state={:?} author={:?} reviewer={:?}", 16 - args.repo, args.state, args.author, args.reviewer); 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() 25 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 26 + .unwrap_or_else(|| "https://bsky.social".into()); 27 + let client = tangled_api::TangledClient::new(&pds); 28 + let target_repo_at = if let Some(repo) = &args.repo { 29 + let (owner, name) = parse_repo_ref(repo, &session.handle); 30 + let info = client 31 + .get_repo_info(owner, name, Some(session.access_jwt.as_str())) 32 + .await?; 33 + Some(format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey)) 34 + } else { 35 + None 36 + }; 37 + let pulls = client 38 + .list_pulls( 39 + &session.did, 40 + target_repo_at.as_deref(), 41 + Some(session.access_jwt.as_str()), 42 + ) 43 + .await?; 44 + if pulls.is_empty() { 45 + println!("No pull requests found (showing only those you created)"); 46 + } else { 47 + println!("RKEY\tTITLE\tTARGET"); 48 + for pr in pulls { 49 + println!("{}\t{}\t{}", pr.rkey, pr.pull.title, pr.pull.target.repo); 50 + } 51 + } 17 52 Ok(()) 18 53 } 19 54 20 55 async fn create(args: PrCreateArgs) -> Result<()> { 21 - println!("PR create (stub) repo={:?} base={:?} head={:?} title={:?} draft={}", 22 - args.repo, args.base, args.head, args.title, args.draft); 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() 64 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 65 + .unwrap_or_else(|| "https://bsky.social".into()); 66 + let client = tangled_api::TangledClient::new(&pds); 67 + 68 + let repo = args 69 + .repo 70 + .as_ref() 71 + .ok_or_else(|| anyhow!("--repo is required for pr create"))?; 72 + let (owner, name) = parse_repo_ref(repo, ""); 73 + let info = client 74 + .get_repo_info(owner, name, Some(session.access_jwt.as_str())) 75 + .await?; 76 + 77 + let base = args 78 + .base 79 + .as_deref() 80 + .ok_or_else(|| anyhow!("--base is required (target branch)"))?; 81 + let head = args 82 + .head 83 + .as_deref() 84 + .ok_or_else(|| anyhow!("--head is required (source range/branch)"))?; 85 + 86 + // Generate format-patch using external git for fidelity 87 + let output = Command::new("git") 88 + .arg("format-patch") 89 + .arg("--stdout") 90 + .arg(format!("{}..{}", base, head)) 91 + .current_dir(Path::new(".")) 92 + .output()?; 93 + if !output.status.success() { 94 + return Err(anyhow!("failed to run git format-patch")); 95 + } 96 + let patch = String::from_utf8_lossy(&output.stdout).to_string(); 97 + if patch.trim().is_empty() { 98 + return Err(anyhow!("no changes between base and head")); 99 + } 100 + 101 + let title_buf; 102 + let title = if let Some(t) = args.title.as_deref() { 103 + t 104 + } else { 105 + title_buf = format!("{} -> {}", head, base); 106 + &title_buf 107 + }; 108 + let rkey = client 109 + .create_pull( 110 + &session.did, 111 + &info.did, 112 + &info.rkey, 113 + base, 114 + &patch, 115 + title, 116 + args.body.as_deref(), 117 + &pds, 118 + &session.access_jwt, 119 + ) 120 + .await?; 121 + println!( 122 + "Created PR rkey={} targeting {} branch {}", 123 + rkey, info.did, base 124 + ); 23 125 Ok(()) 24 126 } 25 127 26 128 async fn show(args: PrShowArgs) -> Result<()> { 27 - println!("PR show (stub) id={} diff={} comments={} checks={}", args.id, args.diff, args.comments, args.checks); 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 136 + .clone() 137 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 138 + .unwrap_or_else(|| "https://bsky.social".into()); 139 + let client = tangled_api::TangledClient::new(&pds); 140 + let pr = client 141 + .get_pull_record(&did, &rkey, Some(session.access_jwt.as_str())) 142 + .await?; 143 + println!("TITLE: {}", pr.title); 144 + if !pr.body.is_empty() { 145 + println!("BODY:\n{}", pr.body); 146 + } 147 + println!("TARGET: {} @ {}", pr.target.repo, pr.target.branch); 148 + if args.diff { 149 + println!("PATCH:\n{}", pr.patch); 150 + } 28 151 Ok(()) 29 152 } 30 153 31 154 async fn review(args: PrReviewArgs) -> Result<()> { 32 - println!("PR review (stub) id={} approve={} request_changes={} comment={:?}", 33 - args.id, args.approve, args.request_changes, args.comment); 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 162 + .clone() 163 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 164 + .unwrap_or_else(|| "https://bsky.social".into()); 165 + let pr_at = format!("at://{}/sh.tangled.repo.pull/{}", did, rkey); 166 + let note = if let Some(c) = args.comment.as_deref() { 167 + c 168 + } else if args.approve { 169 + "LGTM" 170 + } else if args.request_changes { 171 + "Requesting changes" 172 + } else { 173 + "" 174 + }; 175 + if note.is_empty() { 176 + return Err(anyhow!("provide --comment or --approve/--request-changes")); 177 + } 178 + let client = tangled_api::TangledClient::new(&pds); 179 + client 180 + .comment_pull(&session.did, &pr_at, note, &pds, &session.access_jwt) 181 + .await?; 182 + println!("Review comment posted"); 34 183 Ok(()) 35 184 } 36 185 37 - async fn merge(args: PrMergeArgs) -> Result<()> { 38 - println!("PR merge (stub) id={} squash={} rebase={} no_ff={}", 39 - args.id, args.squash, args.rebase, args.no_ff); 186 + async fn merge(_args: PrMergeArgs) -> Result<()> { 187 + // Placeholder: merging requires server-side merge call with the patch and target branch. 188 + println!("Merge via CLI is not implemented yet. Use the web UI for now."); 40 189 Ok(()) 41 190 } 42 191 192 + fn parse_repo_ref<'a>(spec: &'a str, default_owner: &'a str) -> (&'a str, &'a str) { 193 + if let Some((owner, name)) = spec.split_once('/') { 194 + if !owner.is_empty() { 195 + (owner, name) 196 + } else { 197 + (default_owner, name) 198 + } 199 + } else { 200 + (default_owner, spec) 201 + } 202 + } 203 + 204 + fn parse_record_id<'a>(id: &'a str, default_did: &'a str) -> Result<(String, String)> { 205 + if let Some(rest) = id.strip_prefix("at://") { 206 + let parts: Vec<&str> = rest.split('/').collect(); 207 + if parts.len() >= 4 { 208 + return Ok((parts[0].to_string(), parts[3].to_string())); 209 + } 210 + } 211 + if let Some((did, rkey)) = id.split_once(':') { 212 + return Ok((did.to_string(), rkey.to_string())); 213 + } 214 + Ok((default_did.to_string(), id.to_string())) 215 + }
+270 -20
crates/tangled-cli/src/commands/repo.rs
··· 1 - use anyhow::Result; 2 - use crate::cli::{Cli, RepoCommand, RepoCreateArgs, RepoInfoArgs, RepoListArgs, RepoCloneArgs, RepoDeleteArgs, RepoRefArgs}; 1 + use anyhow::{anyhow, Result}; 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, 9 + RepoListArgs, RepoRefArgs, 10 + }; 3 11 4 - pub async fn run(_cli: &Cli, cmd: RepoCommand) -> Result<()> { 12 + pub async fn run(cli: &Cli, cmd: RepoCommand) -> Result<()> { 5 13 match cmd { 6 - RepoCommand::List(args) => list(args).await, 14 + RepoCommand::List(args) => list(cli, args).await, 7 15 RepoCommand::Create(args) => create(args).await, 8 16 RepoCommand::Clone(args) => clone(args).await, 9 17 RepoCommand::Info(args) => info(args).await, ··· 13 21 } 14 22 } 15 23 16 - async fn list(args: RepoListArgs) -> Result<()> { 17 - println!("Listing repositories (stub) knot={:?} user={:?} starred={}", 18 - args.knot, args.user, args.starred); 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 33 + .pds 34 + .clone() 35 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 36 + .unwrap_or_else(|| "https://bsky.social".into()); 37 + let pds_client = tangled_api::TangledClient::new(pds); 38 + // Default to the logged-in user handle if --user is not provided 39 + let effective_user = args.user.as_deref().unwrap_or(session.handle.as_str()); 40 + let repos = pds_client 41 + .list_repos( 42 + Some(effective_user), 43 + args.knot.as_deref(), 44 + args.starred, 45 + Some(session.access_jwt.as_str()), 46 + ) 47 + .await?; 48 + 49 + match cli.format { 50 + OutputFormat::Json => { 51 + let json = serde_json::to_string_pretty(&repos)?; 52 + println!("{}", json); 53 + } 54 + OutputFormat::Table => { 55 + println!("NAME\tKNOT\tPRIVATE"); 56 + for r in repos { 57 + println!("{}\t{}\t{}", r.name, r.knot.unwrap_or_default(), r.private); 58 + } 59 + } 60 + } 61 + 19 62 Ok(()) 20 63 } 21 64 22 65 async fn create(args: RepoCreateArgs) -> Result<()> { 23 - println!( 24 - "Creating repo '{}' (stub) knot={:?} private={} init={} desc={:?}", 25 - args.name, args.knot, args.private, args.init, args.description 26 - ); 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); 74 + 75 + // Determine PDS base and target knot hostname 76 + let pds = session 77 + .pds 78 + .clone() 79 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 80 + .unwrap_or_else(|| "https://bsky.social".into()); 81 + let knot = args.knot.unwrap_or_else(|| "tngl.sh".to_string()); 82 + 83 + let opts = tangled_api::client::CreateRepoOptions { 84 + did: &session.did, 85 + name: &args.name, 86 + knot: &knot, 87 + description: args.description.as_deref(), 88 + default_branch: None, 89 + source: None, 90 + pds_base: &pds, 91 + access_jwt: &session.access_jwt, 92 + }; 93 + client.create_repo(opts).await?; 94 + 95 + println!("Created repo '{}' (knot: {})", args.name, knot); 27 96 Ok(()) 28 97 } 29 98 30 99 async fn clone(args: RepoCloneArgs) -> Result<()> { 31 - println!("Cloning repo '{}' (stub) https={} depth={:?}", args.repo, args.https, args.depth); 32 - Ok(()) 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 107 + .pds 108 + .clone() 109 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 110 + .unwrap_or_else(|| "https://bsky.social".into()); 111 + let pds_client = tangled_api::TangledClient::new(&pds); 112 + let info = pds_client 113 + .get_repo_info(owner, &name, Some(session.access_jwt.as_str())) 114 + .await?; 115 + 116 + let remote = if args.https { 117 + let owner_path = if owner.starts_with('@') { 118 + owner.to_string() 119 + } else { 120 + format!("@{}", owner) 121 + }; 122 + format!("https://tangled.org/{}/{}", owner_path, name) 123 + } else { 124 + let knot = if info.knot == "knot1.tangled.sh" { 125 + "tangled.org".to_string() 126 + } else { 127 + info.knot.clone() 128 + }; 129 + format!("git@{}:{}/{}", knot, owner.trim_start_matches('@'), name) 130 + }; 131 + 132 + let target = PathBuf::from(&name); 133 + println!("Cloning {} -> {:?}", remote, target); 134 + 135 + let mut callbacks = RemoteCallbacks::new(); 136 + callbacks.credentials(|_url, username_from_url, _allowed| { 137 + if let Some(user) = username_from_url { 138 + Cred::ssh_key_from_agent(user) 139 + } else { 140 + Cred::default() 141 + } 142 + }); 143 + let mut fetch_opts = FetchOptions::new(); 144 + fetch_opts.remote_callbacks(callbacks); 145 + if let Some(d) = args.depth { 146 + fetch_opts.depth(d as i32); 147 + } 148 + let mut builder = RepoBuilder::new(); 149 + builder.fetch_options(fetch_opts); 150 + match builder.clone(&remote, &target) { 151 + Ok(_) => Ok(()), 152 + Err(e) => { 153 + println!("Failed to clone via libgit2: {}", e); 154 + println!( 155 + "Hint: try: git clone{} {}", 156 + args.depth 157 + .map(|d| format!(" --depth {}", d)) 158 + .unwrap_or_default(), 159 + remote 160 + ); 161 + Err(anyhow!(e.to_string())) 162 + } 163 + } 33 164 } 34 165 35 166 async fn info(args: RepoInfoArgs) -> Result<()> { 36 - println!( 37 - "Repository info '{}' (stub) stats={} contributors={}", 38 - args.repo, args.stats, args.contributors 39 - ); 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 174 + .clone() 175 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 176 + .unwrap_or_else(|| "https://bsky.social".into()); 177 + let pds_client = tangled_api::TangledClient::new(&pds); 178 + let info = pds_client 179 + .get_repo_info(owner, &name, Some(session.access_jwt.as_str())) 180 + .await?; 181 + 182 + println!("NAME: {}", info.name); 183 + println!("OWNER DID: {}", info.did); 184 + println!("KNOT: {}", info.knot); 185 + if let Some(desc) = info.description.as_deref() { 186 + if !desc.is_empty() { 187 + println!("DESCRIPTION: {}", desc); 188 + } 189 + } 190 + 191 + let knot_host = if info.knot == "knot1.tangled.sh" { 192 + "tangled.org".to_string() 193 + } else { 194 + info.knot.clone() 195 + }; 196 + if args.stats { 197 + let client = tangled_api::TangledClient::default(); 198 + if let Ok(def) = client 199 + .get_default_branch(&knot_host, &info.did, &info.name) 200 + .await 201 + { 202 + println!( 203 + "DEFAULT BRANCH: {} ({})", 204 + def.name, 205 + def.short_hash.unwrap_or(def.hash) 206 + ); 207 + if let Some(msg) = def.message { 208 + if !msg.is_empty() { 209 + println!("LAST COMMIT: {}", msg); 210 + } 211 + } 212 + } 213 + if let Ok(langs) = client 214 + .get_languages(&knot_host, &info.did, &info.name) 215 + .await 216 + { 217 + if !langs.languages.is_empty() { 218 + println!("LANGUAGES:"); 219 + for l in langs.languages.iter().take(6) { 220 + println!(" - {} ({}%)", l.name, l.percentage); 221 + } 222 + } 223 + } 224 + } 225 + 226 + if args.contributors { 227 + println!("Contributors: not implemented yet"); 228 + } 40 229 Ok(()) 41 230 } 42 231 43 232 async fn delete(args: RepoDeleteArgs) -> Result<()> { 44 - println!("Deleting repo '{}' (stub) force={}", args.repo, args.force); 233 + let mgr = SessionManager::default(); 234 + let session = mgr 235 + .load()? 236 + .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 237 + let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 238 + let pds = session 239 + .pds 240 + .clone() 241 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 242 + .unwrap_or_else(|| "https://bsky.social".into()); 243 + let pds_client = tangled_api::TangledClient::new(&pds); 244 + let record = pds_client 245 + .get_repo_info(owner, &name, Some(session.access_jwt.as_str())) 246 + .await?; 247 + let did = record.did; 248 + let api = tangled_api::TangledClient::default(); 249 + api.delete_repo(&did, &name, &pds, &session.access_jwt) 250 + .await?; 251 + println!("Deleted repo '{}'", name); 45 252 Ok(()) 46 253 } 47 254 48 255 async fn star(args: RepoRefArgs) -> Result<()> { 49 - println!("Starring repo '{}' (stub)", args.repo); 256 + let mgr = SessionManager::default(); 257 + let session = mgr 258 + .load()? 259 + .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 260 + let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 261 + let pds = session 262 + .pds 263 + .clone() 264 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 265 + .unwrap_or_else(|| "https://bsky.social".into()); 266 + let pds_client = tangled_api::TangledClient::new(&pds); 267 + let info = pds_client 268 + .get_repo_info(owner, &name, Some(session.access_jwt.as_str())) 269 + .await?; 270 + let subject = format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey); 271 + let api = tangled_api::TangledClient::default(); 272 + api.star_repo(&pds, &session.access_jwt, &subject, &session.did) 273 + .await?; 274 + println!("Starred {}/{}", owner, name); 50 275 Ok(()) 51 276 } 52 277 53 278 async fn unstar(args: RepoRefArgs) -> Result<()> { 54 - println!("Unstarring repo '{}' (stub)", args.repo); 279 + let mgr = SessionManager::default(); 280 + let session = mgr 281 + .load()? 282 + .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 283 + let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 284 + let pds = session 285 + .pds 286 + .clone() 287 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 288 + .unwrap_or_else(|| "https://bsky.social".into()); 289 + let pds_client = tangled_api::TangledClient::new(&pds); 290 + let info = pds_client 291 + .get_repo_info(owner, &name, Some(session.access_jwt.as_str())) 292 + .await?; 293 + let subject = format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey); 294 + let api = tangled_api::TangledClient::default(); 295 + api.unstar_repo(&pds, &session.access_jwt, &subject, &session.did) 296 + .await?; 297 + println!("Unstarred {}/{}", owner, name); 55 298 Ok(()) 56 299 } 57 300 301 + fn parse_repo_ref<'a>(spec: &'a str, default_owner: &'a str) -> (&'a str, String) { 302 + if let Some((owner, name)) = spec.split_once('/') { 303 + (owner, name.to_string()) 304 + } else { 305 + (default_owner, spec.to_string()) 306 + } 307 + }
+107 -4
crates/tangled-cli/src/commands/spindle.rs
··· 1 - use anyhow::Result; 2 - use crate::cli::{Cli, SpindleCommand, SpindleListArgs, SpindleConfigArgs, SpindleRunArgs, SpindleLogsArgs}; 1 + use crate::cli::{ 2 + Cli, SpindleCommand, SpindleConfigArgs, SpindleListArgs, SpindleLogsArgs, SpindleRunArgs, 3 + SpindleSecretAddArgs, SpindleSecretCommand, SpindleSecretListArgs, SpindleSecretRemoveArgs, 4 + }; 5 + use anyhow::{anyhow, Result}; 6 + use tangled_config::session::SessionManager; 3 7 4 8 pub async fn run(_cli: &Cli, cmd: SpindleCommand) -> Result<()> { 5 9 match cmd { ··· 7 11 SpindleCommand::Config(args) => config(args).await, 8 12 SpindleCommand::Run(args) => run_pipeline(args).await, 9 13 SpindleCommand::Logs(args) => logs(args).await, 14 + SpindleCommand::Secret(cmd) => secret(cmd).await, 10 15 } 11 16 } 12 17 ··· 24 29 } 25 30 26 31 async fn run_pipeline(args: SpindleRunArgs) -> Result<()> { 27 - println!("Spindle run (stub) repo={:?} branch={:?} wait={}", args.repo, args.branch, args.wait); 32 + println!( 33 + "Spindle run (stub) repo={:?} branch={:?} wait={}", 34 + args.repo, args.branch, args.wait 35 + ); 28 36 Ok(()) 29 37 } 30 38 31 39 async fn logs(args: SpindleLogsArgs) -> Result<()> { 32 - println!("Spindle logs (stub) job_id={} follow={} lines={:?}", args.job_id, args.follow, args.lines); 40 + println!( 41 + "Spindle logs (stub) job_id={} follow={} lines={:?}", 42 + args.job_id, args.follow, args.lines 43 + ); 44 + Ok(()) 45 + } 46 + 47 + async fn secret(cmd: SpindleSecretCommand) -> Result<()> { 48 + match cmd { 49 + SpindleSecretCommand::List(args) => secret_list(args).await, 50 + SpindleSecretCommand::Add(args) => secret_add(args).await, 51 + SpindleSecretCommand::Remove(args) => secret_remove(args).await, 52 + } 53 + } 54 + 55 + async fn secret_list(args: SpindleSecretListArgs) -> Result<()> { 56 + let mgr = SessionManager::default(); 57 + let session = mgr 58 + .load()? 59 + .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 60 + let pds = session 61 + .pds 62 + .clone() 63 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 64 + .unwrap_or_else(|| "https://bsky.social".into()); 65 + let pds_client = tangled_api::TangledClient::new(&pds); 66 + let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 67 + let info = pds_client 68 + .get_repo_info(owner, name, Some(session.access_jwt.as_str())) 69 + .await?; 70 + let repo_at = format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey); 71 + let api = tangled_api::TangledClient::default(); // base tngl.sh 72 + let secrets = api 73 + .list_repo_secrets(&pds, &session.access_jwt, &repo_at) 74 + .await?; 75 + if secrets.is_empty() { 76 + println!("No secrets configured for {}", args.repo); 77 + } else { 78 + println!("KEY\tCREATED AT\tCREATED BY"); 79 + for s in secrets { 80 + println!("{}\t{}\t{}", s.key, s.created_at, s.created_by); 81 + } 82 + } 83 + Ok(()) 84 + } 85 + 86 + async fn secret_add(args: SpindleSecretAddArgs) -> Result<()> { 87 + let mgr = SessionManager::default(); 88 + let session = mgr 89 + .load()? 90 + .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 91 + let pds = session 92 + .pds 93 + .clone() 94 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 95 + .unwrap_or_else(|| "https://bsky.social".into()); 96 + let pds_client = tangled_api::TangledClient::new(&pds); 97 + let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 98 + let info = pds_client 99 + .get_repo_info(owner, name, Some(session.access_jwt.as_str())) 100 + .await?; 101 + let repo_at = format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey); 102 + let api = tangled_api::TangledClient::default(); 103 + api.add_repo_secret(&pds, &session.access_jwt, &repo_at, &args.key, &args.value) 104 + .await?; 105 + println!("Added secret '{}' to {}", args.key, args.repo); 106 + Ok(()) 107 + } 108 + 109 + async fn secret_remove(args: SpindleSecretRemoveArgs) -> Result<()> { 110 + let mgr = SessionManager::default(); 111 + let session = mgr 112 + .load()? 113 + .ok_or_else(|| anyhow!("Please login first: tangled auth login"))?; 114 + let pds = session 115 + .pds 116 + .clone() 117 + .or_else(|| std::env::var("TANGLED_PDS_BASE").ok()) 118 + .unwrap_or_else(|| "https://bsky.social".into()); 119 + let pds_client = tangled_api::TangledClient::new(&pds); 120 + let (owner, name) = parse_repo_ref(&args.repo, &session.handle); 121 + let info = pds_client 122 + .get_repo_info(owner, name, Some(session.access_jwt.as_str())) 123 + .await?; 124 + let repo_at = format!("at://{}/sh.tangled.repo/{}", info.did, info.rkey); 125 + let api = tangled_api::TangledClient::default(); 126 + api.remove_repo_secret(&pds, &session.access_jwt, &repo_at, &args.key) 127 + .await?; 128 + println!("Removed secret '{}' from {}", args.key, args.repo); 33 129 Ok(()) 34 130 } 35 131 132 + fn parse_repo_ref<'a>(spec: &'a str, default_owner: &'a str) -> (&'a str, &'a str) { 133 + if let Some((owner, name)) = spec.split_once('/') { 134 + (owner, name) 135 + } else { 136 + (default_owner, spec) 137 + } 138 + }
+1 -1
crates/tangled-cli/src/main.rs
··· 2 2 mod commands; 3 3 4 4 use anyhow::Result; 5 - use cli::Cli; 6 5 use clap::Parser; 6 + use cli::Cli; 7 7 8 8 #[tokio::main] 9 9 async fn main() -> Result<()> {
+7 -4
crates/tangled-config/src/config.rs
··· 22 22 pub knot: Option<String>, 23 23 pub editor: Option<String>, 24 24 pub pager: Option<String>, 25 - #[serde(default = "default_format")] 25 + #[serde(default = "default_format")] 26 26 pub format: String, 27 27 } 28 28 29 - fn default_format() -> String { "table".to_string() } 29 + fn default_format() -> String { 30 + "table".to_string() 31 + } 30 32 31 33 #[derive(Debug, Clone, Serialize, Deserialize, Default)] 32 34 pub struct AuthSection { ··· 74 76 let path = path 75 77 .map(|p| p.to_path_buf()) 76 78 .unwrap_or(default_config_path()?); 77 - if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } 79 + if let Some(parent) = path.parent() { 80 + std::fs::create_dir_all(parent)?; 81 + } 78 82 let toml = toml::to_string_pretty(cfg)?; 79 83 fs::write(&path, toml) 80 84 .with_context(|| format!("Failed writing config file: {}", path.display()))?; 81 85 Ok(()) 82 86 } 83 -
+13 -5
crates/tangled-config/src/keychain.rs
··· 8 8 9 9 impl Keychain { 10 10 pub fn new(service: &str, account: &str) -> Self { 11 - Self { service: service.into(), account: account.into() } 11 + Self { 12 + service: service.into(), 13 + account: account.into(), 14 + } 12 15 } 13 16 14 17 fn entry(&self) -> Result<Entry> { ··· 16 19 } 17 20 18 21 pub fn set_password(&self, secret: &str) -> Result<()> { 19 - self.entry()?.set_password(secret).map_err(|e| anyhow!("keyring error: {e}")) 22 + self.entry()? 23 + .set_password(secret) 24 + .map_err(|e| anyhow!("keyring error: {e}")) 20 25 } 21 26 22 27 pub fn get_password(&self) -> Result<String> { 23 - self.entry()?.get_password().map_err(|e| anyhow!("keyring error: {e}")) 28 + self.entry()? 29 + .get_password() 30 + .map_err(|e| anyhow!("keyring error: {e}")) 24 31 } 25 32 26 33 pub fn delete_password(&self) -> Result<()> { 27 - self.entry()?.delete_password().map_err(|e| anyhow!("keyring error: {e}")) 34 + self.entry()? 35 + .delete_credential() 36 + .map_err(|e| anyhow!("keyring error: {e}")) 28 37 } 29 38 } 30 -
+1 -2
crates/tangled-config/src/lib.rs
··· 1 1 pub mod config; 2 - pub mod session; 3 2 pub mod keychain; 4 - 3 + pub mod session;
+13 -3
crates/tangled-config/src/session.rs
··· 11 11 pub did: String, 12 12 pub handle: String, 13 13 #[serde(default)] 14 + pub pds: Option<String>, 15 + #[serde(default)] 14 16 pub created_at: DateTime<Utc>, 15 17 } 16 18 ··· 21 23 refresh_jwt: String::new(), 22 24 did: String::new(), 23 25 handle: String::new(), 26 + pds: None, 24 27 created_at: Utc::now(), 25 28 } 26 29 } ··· 33 36 34 37 impl Default for SessionManager { 35 38 fn default() -> Self { 36 - Self { service: "tangled-cli".into(), account: "default".into() } 39 + Self { 40 + service: "tangled-cli".into(), 41 + account: "default".into(), 42 + } 37 43 } 38 44 } 39 45 40 46 impl SessionManager { 41 - pub fn new(service: &str, account: &str) -> Self { Self { service: service.into(), account: account.into() } } 47 + pub fn new(service: &str, account: &str) -> Self { 48 + Self { 49 + service: service.into(), 50 + account: account.into(), 51 + } 52 + } 42 53 43 54 pub fn save(&self, session: &Session) -> Result<()> { 44 55 let keychain = Keychain::new(&self.service, &self.account); ··· 59 70 keychain.delete_password() 60 71 } 61 72 } 62 -
-1
crates/tangled-git/src/lib.rs
··· 1 1 pub mod operations; 2 -
-1
crates/tangled-git/src/operations.rs
··· 5 5 // TODO: support ssh/https and depth 6 6 bail!("clone_repo not implemented") 7 7 } 8 -