Rust CLI for tangled

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