+1
.gitignore
+1
.gitignore
···
···
1
+
target/
+18
.pre-commit-config.yaml
+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
+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
+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
+6
-3
Cargo.toml
···
41
42
# Storage
43
dirs = "5.0"
44
-
keyring = "3.0"
45
46
# Error Handling
47
anyhow = "1.0"
48
thiserror = "2.0"
49
50
# Utilities
51
-
chrono = "0.4"
52
url = "2.5"
53
base64 = "0.22"
54
regex = "1.10"
55
56
# Testing
57
mockito = "1.4"
58
tempfile = "3.10"
59
assert_cmd = "2.0"
60
predicates = "3.1"
61
-
···
41
42
# Storage
43
dirs = "5.0"
44
+
keyring = { version = "3.6", features = ["sync-secret-service", "vendored"] }
45
46
# Error Handling
47
anyhow = "1.0"
48
thiserror = "2.0"
49
50
# Utilities
51
+
chrono = { version = "0.4", features = ["serde"] }
52
url = "2.5"
53
base64 = "0.22"
54
regex = "1.10"
55
56
+
# WebSocket
57
+
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
58
+
futures-util = "0.3"
59
+
60
# Testing
61
mockito = "1.4"
62
tempfile = "3.10"
63
assert_cmd = "2.0"
64
predicates = "3.1"
+173
-16
README.md
+173
-16
README.md
···
1
-
# Tangled CLI (Rust)
2
3
A Rust CLI for Tangled, a decentralized git collaboration platform built on the AT Protocol.
4
5
-
Status: project scaffold with CLI, config, API and git crates. Commands are stubs pending endpoint wiring.
6
7
-
## Workspace
8
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
14
15
-
## Quick start
16
17
```
18
-
cargo run -p tangled-cli -- --help
19
```
20
21
-
Building requires network to fetch crates.
22
23
-
## Next steps
24
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)
29
···
1
+
# Tangled CLI
2
3
A Rust CLI for Tangled, a decentralized git collaboration platform built on the AT Protocol.
4
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
99
100
+
## Configuration
101
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"
120
121
+
# List issues
122
+
tangled issue list --repo myrepo
123
124
+
# Comment on an issue
125
+
tangled issue comment <issue-id> --body "I'll fix this"
126
```
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>
139
```
140
141
+
### CI/CD with Spindle
142
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
181
182
+
Contributions are welcome! Please feel free to submit issues or pull requests.
183
+
184
+
## License
185
186
+
MIT OR Apache-2.0
+36
TODO.md
+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
+1
-1
crates/tangled-api/Cargo.toml
···
11
serde_json = { workspace = true }
12
reqwest = { workspace = true }
13
tokio = { workspace = true, features = ["full"] }
14
15
# Optionally depend on ATrium (wired later as endpoints solidify)
16
atrium-api = { workspace = true, optional = true }
···
21
[features]
22
default = []
23
atrium = ["dep:atrium-api", "dep:atrium-xrpc-client"]
24
-
···
11
serde_json = { workspace = true }
12
reqwest = { workspace = true }
13
tokio = { workspace = true, features = ["full"] }
14
+
chrono = { workspace = true }
15
16
# Optionally depend on ATrium (wired later as endpoints solidify)
17
atrium-api = { workspace = true, optional = true }
···
22
[features]
23
default = []
24
atrium = ["dep:atrium-api", "dep:atrium-xrpc-client"]
+1512
-11
crates/tangled-api/src/client.rs
+1512
-11
crates/tangled-api/src/client.rs
···
1
-
use anyhow::{bail, Result};
2
-
use serde::{Deserialize, Serialize};
3
use tangled_config::session::Session;
4
5
#[derive(Clone, Debug)]
6
pub struct TangledClient {
7
base_url: String,
8
}
9
10
impl TangledClient {
11
pub fn new(base_url: impl Into<String>) -> Self {
12
-
Self { base_url: base_url.into() }
13
}
14
15
-
pub fn default() -> Self {
16
-
Self::new("https://tangled.org")
17
}
18
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")
22
}
23
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![])
27
}
28
}
29
···
34
pub name: String,
35
pub knot: Option<String>,
36
pub description: Option<String>,
37
pub private: bool,
38
}
39
···
1
+
use anyhow::{anyhow, Result};
2
+
use serde::{de::DeserializeOwned, Deserialize, Serialize};
3
use tangled_config::session::Session;
4
5
#[derive(Clone, Debug)]
6
pub struct TangledClient {
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
+
}
16
}
17
18
impl TangledClient {
19
pub fn new(base_url: impl Into<String>) -> Self {
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(¶ms)
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", ¶ms, 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", ¶ms, 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
+
¶ms,
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", ¶ms, 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", ¶ms, 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
+
¶ms,
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", ¶ms, 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", ¶ms, 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", ¶ms, 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", ¶ms, 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", ¶ms, 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", ¶ms, 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", ¶ms, 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", ¶ms, 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", ¶ms, 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", ¶ms, 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
+
¶ms,
1161
+
Some(access_jwt),
1162
+
)
1163
+
.await?;
1164
+
Ok(sa.token)
1165
}
1166
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"))
1210
}
1211
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(())
1268
}
1269
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", ¶ms, 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", ¶ms, 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)
1357
}
1358
}
1359
···
1364
pub name: String,
1365
pub knot: Option<String>,
1366
pub description: Option<String>,
1367
+
pub spindle: Option<String>,
1368
+
#[serde(default)]
1369
pub private: bool,
1370
}
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
+4
-1
crates/tangled-api/src/lib.rs
+5
-1
crates/tangled-cli/Cargo.toml
+5
-1
crates/tangled-cli/Cargo.toml
···
14
serde = { workspace = true, features = ["derive"] }
15
serde_json = { workspace = true }
16
tokio = { workspace = true, features = ["full"] }
17
18
# Internal crates
19
tangled-config = { path = "../tangled-config" }
20
tangled-api = { path = "../tangled-api" }
21
tangled-git = { path = "../tangled-git" }
22
-
···
14
serde = { workspace = true, features = ["derive"] }
15
serde_json = { workspace = true }
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 }
22
23
# Internal crates
24
tangled-config = { path = "../tangled-config" }
25
tangled-api = { path = "../tangled-api" }
26
tangled-git = { path = "../tangled-git" }
+65
-36
crates/tangled-cli/src/cli.rs
+65
-36
crates/tangled-cli/src/cli.rs
···
40
#[derive(Subcommand, Debug, Clone)]
41
pub enum Command {
42
/// Authentication commands
43
Auth(AuthCommand),
44
/// Repository commands
45
Repo(RepoCommand),
46
/// Issue commands
47
Issue(IssueCommand),
48
/// Pull request commands
49
Pr(PrCommand),
50
/// Knot management commands
51
Knot(KnotCommand),
52
/// Spindle integration commands
53
Spindle(SpindleCommand),
54
}
55
···
102
pub user: Option<String>,
103
#[arg(long, default_value_t = false)]
104
pub starred: bool,
105
}
106
107
#[derive(Args, Debug, Clone)]
···
275
#[derive(Args, Debug, Clone)]
276
pub struct PrMergeArgs {
277
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
}
285
286
#[derive(Subcommand, Debug, Clone)]
287
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,
301
}
302
303
#[derive(Args, Debug, Clone)]
304
-
pub struct KnotAddArgs {
305
-
pub url: String,
306
#[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,
322
}
323
324
#[derive(Subcommand, Debug, Clone)]
···
327
Config(SpindleConfigArgs),
328
Run(SpindleRunArgs),
329
Logs(SpindleLogsArgs),
330
}
331
332
#[derive(Args, Debug, Clone)]
···
366
pub lines: Option<usize>,
367
}
368
···
40
#[derive(Subcommand, Debug, Clone)]
41
pub enum Command {
42
/// Authentication commands
43
+
#[command(subcommand)]
44
Auth(AuthCommand),
45
/// Repository commands
46
+
#[command(subcommand)]
47
Repo(RepoCommand),
48
/// Issue commands
49
+
#[command(subcommand)]
50
Issue(IssueCommand),
51
/// Pull request commands
52
+
#[command(subcommand)]
53
Pr(PrCommand),
54
/// Knot management commands
55
+
#[command(subcommand)]
56
Knot(KnotCommand),
57
/// Spindle integration commands
58
+
#[command(subcommand)]
59
Spindle(SpindleCommand),
60
}
61
···
108
pub user: Option<String>,
109
#[arg(long, default_value_t = false)]
110
pub starred: bool,
111
+
/// Tangled API base URL (overrides env)
112
+
#[arg(long)]
113
+
pub base: Option<String>,
114
}
115
116
#[derive(Args, Debug, Clone)]
···
284
#[derive(Args, Debug, Clone)]
285
pub struct PrMergeArgs {
286
pub id: String,
287
}
288
289
#[derive(Subcommand, Debug, Clone)]
290
pub enum KnotCommand {
291
+
/// Migrate a repository to another knot
292
+
Migrate(KnotMigrateArgs),
293
}
294
295
#[derive(Args, Debug, Clone)]
296
+
pub struct KnotMigrateArgs {
297
+
/// Repo to migrate: <owner>/<name> (owner defaults to your handle)
298
#[arg(long)]
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,
309
}
310
311
#[derive(Subcommand, Debug, Clone)]
···
314
Config(SpindleConfigArgs),
315
Run(SpindleRunArgs),
316
Logs(SpindleLogsArgs),
317
+
/// Secrets management
318
+
#[command(subcommand)]
319
+
Secret(SpindleSecretCommand),
320
}
321
322
#[derive(Args, Debug, Clone)]
···
356
pub lines: Option<usize>,
357
}
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
+27
-17
crates/tangled-cli/src/commands/auth.rs
···
1
use anyhow::Result;
2
use dialoguer::{Input, Password};
3
4
use crate::cli::{AuthCommand, AuthLoginArgs, Cli};
5
···
20
Some(p) => p,
21
None => Password::new().with_prompt("Password").interact()?,
22
};
23
-
let pds = args.pds.unwrap_or_else(|| "https://bsky.social".to_string());
24
25
-
// Placeholder: integrate tangled_api authentication here
26
-
println!(
27
-
"Logging in as '{}' against PDS '{}'... (stub)",
28
-
handle, pds
29
-
);
30
-
31
-
// Example future flow:
32
-
// let client = tangled_api::TangledClient::new(&pds);
33
-
// let session = client.login(&handle, &password).await?;
34
-
// tangled_config::session::SessionManager::default().save(&session)?;
35
-
36
Ok(())
37
}
38
39
async fn status(_cli: &Cli) -> Result<()> {
40
-
// Placeholder: read session from keyring/config
41
-
println!("Authentication status: (stub) not implemented");
42
Ok(())
43
}
44
45
async fn logout(_cli: &Cli) -> Result<()> {
46
-
// Placeholder: remove session from keyring/config
47
-
println!("Logged out (stub)");
48
Ok(())
49
}
50
-
···
1
use anyhow::Result;
2
use dialoguer::{Input, Password};
3
+
use tangled_config::session::SessionManager;
4
5
use crate::cli::{AuthCommand, AuthLoginArgs, Cli};
6
···
21
Some(p) => p,
22
None => Password::new().with_prompt("Password").interact()?,
23
};
24
+
let pds = args
25
+
.pds
26
+
.unwrap_or_else(|| "https://bsky.social".to_string());
27
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);
39
Ok(())
40
}
41
42
async fn status(_cli: &Cli) -> Result<()> {
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
+
}
48
Ok(())
49
}
50
51
async fn logout(_cli: &Cli) -> Result<()> {
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
+
}
59
Ok(())
60
}
+195
-10
crates/tangled-cli/src/commands/issue.rs
+195
-10
crates/tangled-cli/src/commands/issue.rs
···
1
-
use anyhow::Result;
2
-
use crate::cli::{Cli, IssueCommand, IssueListArgs, IssueCreateArgs, IssueShowArgs, IssueEditArgs, IssueCommentArgs};
3
4
pub async fn run(_cli: &Cli, cmd: IssueCommand) -> Result<()> {
5
match cmd {
···
12
}
13
14
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);
17
Ok(())
18
}
19
20
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);
23
Ok(())
24
}
25
26
async fn show(args: IssueShowArgs) -> Result<()> {
27
-
println!("Issue show (stub) id={} comments={} json={}", args.id, args.comments, args.json);
28
Ok(())
29
}
30
31
async fn edit(args: IssueEditArgs) -> Result<()> {
32
-
println!("Issue edit (stub) id={} title={:?} body={:?} state={:?}",
33
-
args.id, args.title, args.body, args.state);
34
Ok(())
35
}
36
37
async fn comment(args: IssueCommentArgs) -> Result<()> {
38
-
println!("Issue comment (stub) id={} close={} body={:?}", args.id, args.close, args.body);
39
Ok(())
40
}
41
···
1
+
use crate::cli::{
2
+
Cli, IssueCommand, IssueCommentArgs, IssueCreateArgs, IssueEditArgs, IssueListArgs,
3
+
IssueShowArgs,
4
+
};
5
+
use anyhow::{anyhow, Result};
6
+
use tangled_api::Issue;
7
8
pub async fn run(_cli: &Cli, cmd: IssueCommand) -> Result<()> {
9
match cmd {
···
16
}
17
18
async fn list(args: IssueListArgs) -> Result<()> {
19
+
let session = crate::util::load_session_with_refresh().await?;
20
+
let pds = session
21
+
.pds
22
+
.clone()
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
+
}
52
Ok(())
53
}
54
55
async fn create(args: IssueCreateArgs) -> Result<()> {
56
+
let session = crate::util::load_session_with_refresh().await?;
57
+
let pds = session
58
+
.pds
59
+
.clone()
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);
88
Ok(())
89
}
90
91
async fn show(args: IssueShowArgs) -> Result<()> {
92
+
// For now, show only accepts at-uri or did:rkey or rkey (for your DID)
93
+
let session = crate::util::load_session_with_refresh().await?;
94
+
let id = args.id;
95
+
let (did, rkey) = parse_record_id(&id, &session.did)?;
96
+
let pds = session
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
+
}
117
Ok(())
118
}
119
120
async fn edit(args: IssueEditArgs) -> Result<()> {
121
+
// Simple edit: fetch existing record and putRecord with new title/body
122
+
let session = crate::util::load_session_with_refresh().await?;
123
+
let (did, rkey) = parse_record_id(&args.id, &session.did)?;
124
+
let pds = session
125
+
.pds
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);
169
Ok(())
170
}
171
172
async fn comment(args: IssueCommentArgs) -> Result<()> {
173
+
let session = crate::util::load_session_with_refresh().await?;
174
+
let (did, rkey) = parse_record_id(&args.id, &session.did)?;
175
+
let pds = session
176
+
.pds
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
+
}
204
Ok(())
205
}
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
+175
-23
crates/tangled-cli/src/commands/knot.rs
···
1
use anyhow::Result;
2
-
use crate::cli::{Cli, KnotCommand, KnotListArgs, KnotAddArgs, KnotVerifyArgs, KnotRefArgs};
3
4
pub async fn run(_cli: &Cli, cmd: KnotCommand) -> Result<()> {
5
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,
11
}
12
}
13
14
-
async fn list(args: KnotListArgs) -> Result<()> {
15
-
println!("Knot list (stub) public={} owned={}", args.public, args.owned);
16
-
Ok(())
17
-
}
18
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
-
}
23
24
-
async fn verify(args: KnotVerifyArgs) -> Result<()> {
25
-
println!("Knot verify (stub) url={}", args.url);
26
-
Ok(())
27
-
}
28
29
-
async fn set_default(args: KnotRefArgs) -> Result<()> {
30
-
println!("Knot set-default (stub) url={}", args.url);
31
Ok(())
32
}
33
34
-
async fn remove(args: KnotRefArgs) -> Result<()> {
35
-
println!("Knot remove (stub) url={}", args.url);
36
-
Ok(())
37
}
38
···
1
+
use crate::cli::{Cli, KnotCommand, KnotMigrateArgs};
2
+
use anyhow::anyhow;
3
use anyhow::Result;
4
+
use git2::{Direction, Repository as GitRepository, StatusOptions};
5
+
use std::path::Path;
6
7
pub async fn run(_cli: &Cli, cmd: KnotCommand) -> Result<()> {
8
match cmd {
9
+
KnotCommand::Migrate(args) => migrate(args).await,
10
}
11
}
12
13
+
async fn migrate(args: KnotMigrateArgs) -> Result<()> {
14
+
let session = crate::util::load_session_with_refresh().await?;
15
+
// 1) Ensure we're inside a git repository and working tree is clean
16
+
let repo = GitRepository::discover(Path::new("."))?;
17
+
let mut status_opts = StatusOptions::new();
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
+
})?;
50
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
+
}
74
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))?;
82
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
+
);
154
Ok(())
155
}
156
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
+
}
163
}
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
+10
-15
crates/tangled-cli/src/commands/mod.rs
···
1
pub mod auth;
2
-
pub mod repo;
3
pub mod issue;
4
-
pub mod pr;
5
pub mod knot;
6
pub mod spindle;
7
8
use anyhow::Result;
9
-
use colored::Colorize;
10
11
use crate::cli::{Cli, Command};
12
13
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,
21
}
22
}
23
24
-
fn not_implemented(feature: &str) -> Result<()> {
25
-
eprintln!("{} {}", "[todo]".yellow().bold(), feature);
26
-
Ok(())
27
-
}
28
-
···
1
pub mod auth;
2
pub mod issue;
3
pub mod knot;
4
+
pub mod pr;
5
+
pub mod repo;
6
pub mod spindle;
7
8
use anyhow::Result;
9
10
use crate::cli::{Cli, Command};
11
12
pub async fn dispatch(cli: Cli) -> Result<()> {
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,
20
}
21
}
22
23
+
// All subcommands are currently implemented with stubs where needed.
+268
-11
crates/tangled-cli/src/commands/pr.rs
+268
-11
crates/tangled-cli/src/commands/pr.rs
···
1
-
use anyhow::Result;
2
-
use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrShowArgs, PrReviewArgs, PrMergeArgs};
3
4
pub async fn run(_cli: &Cli, cmd: PrCommand) -> Result<()> {
5
match cmd {
···
12
}
13
14
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
Ok(())
18
}
19
20
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);
23
Ok(())
24
}
25
26
async fn show(args: PrShowArgs) -> Result<()> {
27
-
println!("PR show (stub) id={} diff={} comments={} checks={}", args.id, args.diff, args.comments, args.checks);
28
Ok(())
29
}
30
31
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);
34
Ok(())
35
}
36
37
async fn merge(args: PrMergeArgs) -> Result<()> {
38
-
println!("PR merge (stub) id={} squash={} rebase={} no_ff={}",
39
-
args.id, args.squash, args.rebase, args.no_ff);
40
Ok(())
41
}
42
···
1
+
use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrMergeArgs, PrReviewArgs, PrShowArgs};
2
+
use anyhow::{anyhow, Result};
3
+
use std::path::Path;
4
+
use std::process::Command;
5
6
pub async fn run(_cli: &Cli, cmd: PrCommand) -> Result<()> {
7
match cmd {
···
14
}
15
16
async fn list(args: PrListArgs) -> Result<()> {
17
+
let session = crate::util::load_session_with_refresh().await?;
18
+
let pds = session
19
+
.pds
20
+
.clone()
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
+
}
72
Ok(())
73
}
74
75
async fn create(args: PrCreateArgs) -> Result<()> {
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
+
);
142
Ok(())
143
}
144
145
async fn show(args: PrShowArgs) -> Result<()> {
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
+
}
179
Ok(())
180
}
181
182
async fn review(args: PrReviewArgs) -> Result<()> {
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");
208
Ok(())
209
}
210
211
async fn merge(args: PrMergeArgs) -> Result<()> {
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", ¶ms, 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);
273
Ok(())
274
}
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
+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};
3
4
-
pub async fn run(_cli: &Cli, cmd: RepoCommand) -> Result<()> {
5
match cmd {
6
-
RepoCommand::List(args) => list(args).await,
7
RepoCommand::Create(args) => create(args).await,
8
RepoCommand::Clone(args) => clone(args).await,
9
RepoCommand::Info(args) => info(args).await,
···
13
}
14
}
15
16
-
async fn list(args: RepoListArgs) -> Result<()> {
17
-
println!("Listing repositories (stub) knot={:?} user={:?} starred={}",
18
-
args.knot, args.user, args.starred);
19
Ok(())
20
}
21
22
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
-
);
27
Ok(())
28
}
29
30
async fn clone(args: RepoCloneArgs) -> Result<()> {
31
-
println!("Cloning repo '{}' (stub) https={} depth={:?}", args.repo, args.https, args.depth);
32
-
Ok(())
33
}
34
35
async fn info(args: RepoInfoArgs) -> Result<()> {
36
-
println!(
37
-
"Repository info '{}' (stub) stats={} contributors={}",
38
-
args.repo, args.stats, args.contributors
39
-
);
40
Ok(())
41
}
42
43
async fn delete(args: RepoDeleteArgs) -> Result<()> {
44
-
println!("Deleting repo '{}' (stub) force={}", args.repo, args.force);
45
Ok(())
46
}
47
48
async fn star(args: RepoRefArgs) -> Result<()> {
49
-
println!("Starring repo '{}' (stub)", args.repo);
50
Ok(())
51
}
52
53
async fn unstar(args: RepoRefArgs) -> Result<()> {
54
-
println!("Unstarring repo '{}' (stub)", args.repo);
55
Ok(())
56
}
57
···
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
+
};
10
11
+
pub async fn run(cli: &Cli, cmd: RepoCommand) -> Result<()> {
12
match cmd {
13
+
RepoCommand::List(args) => list(cli, args).await,
14
RepoCommand::Create(args) => create(args).await,
15
RepoCommand::Clone(args) => clone(args).await,
16
RepoCommand::Info(args) => info(args).await,
···
20
}
21
}
22
23
+
async fn list(cli: &Cli, args: RepoListArgs) -> Result<()> {
24
+
let session = crate::util::load_session_with_refresh().await?;
25
+
26
+
// Use the PDS to list repo records for the user
27
+
let pds = session
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
+
57
Ok(())
58
}
59
60
async fn create(args: RepoCreateArgs) -> Result<()> {
61
+
let session = crate::util::load_session_with_refresh().await?;
62
+
63
+
let base = std::env::var("TANGLED_API_BASE").unwrap_or_else(|_| "https://tngl.sh".into());
64
+
let client = tangled_api::TangledClient::new(base);
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);
87
Ok(())
88
}
89
90
async fn clone(args: RepoCloneArgs) -> Result<()> {
91
+
let session = crate::util::load_session_with_refresh().await?;
92
+
93
+
let (owner, name) = parse_repo_ref(&args.repo, &session.handle);
94
+
let pds = session
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
+
}
152
}
153
154
async fn info(args: RepoInfoArgs) -> Result<()> {
155
+
let session = crate::util::load_session_with_refresh().await?;
156
+
let (owner, name) = parse_repo_ref(&args.repo, &session.handle);
157
+
let pds = session
158
+
.pds
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
+
}
219
Ok(())
220
}
221
222
async fn delete(args: RepoDeleteArgs) -> Result<()> {
223
+
let session = crate::util::load_session_with_refresh().await?;
224
+
let (owner, name) = parse_repo_ref(&args.repo, &session.handle);
225
+
let pds = session
226
+
.pds
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);
239
Ok(())
240
}
241
242
async fn star(args: RepoRefArgs) -> Result<()> {
243
+
let session = crate::util::load_session_with_refresh().await?;
244
+
let (owner, name) = parse_repo_ref(&args.repo, &session.handle);
245
+
let pds = session
246
+
.pds
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);
259
Ok(())
260
}
261
262
async fn unstar(args: RepoRefArgs) -> Result<()> {
263
+
let session = crate::util::load_session_with_refresh().await?;
264
+
let (owner, name) = parse_repo_ref(&args.repo, &session.handle);
265
+
let pds = session
266
+
.pds
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);
279
Ok(())
280
}
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
+290
-8
crates/tangled-cli/src/commands/spindle.rs
···
1
-
use anyhow::Result;
2
-
use crate::cli::{Cli, SpindleCommand, SpindleListArgs, SpindleConfigArgs, SpindleRunArgs, SpindleLogsArgs};
3
4
pub async fn run(_cli: &Cli, cmd: SpindleCommand) -> Result<()> {
5
match cmd {
···
7
SpindleCommand::Config(args) => config(args).await,
8
SpindleCommand::Run(args) => run_pipeline(args).await,
9
SpindleCommand::Logs(args) => logs(args).await,
10
}
11
}
12
13
async fn list(args: SpindleListArgs) -> Result<()> {
14
-
println!("Spindle list (stub) repo={:?}", args.repo);
15
Ok(())
16
}
17
18
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
22
);
23
Ok(())
24
}
25
26
async fn run_pipeline(args: SpindleRunArgs) -> Result<()> {
27
-
println!("Spindle run (stub) repo={:?} branch={:?} wait={}", args.repo, args.branch, args.wait);
28
Ok(())
29
}
30
31
async fn logs(args: SpindleLogsArgs) -> Result<()> {
32
-
println!("Spindle logs (stub) job_id={} follow={} lines={:?}", args.job_id, args.follow, args.lines);
33
Ok(())
34
}
35
···
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};
8
9
pub async fn run(_cli: &Cli, cmd: SpindleCommand) -> Result<()> {
10
match cmd {
···
12
SpindleCommand::Config(args) => config(args).await,
13
SpindleCommand::Run(args) => run_pipeline(args).await,
14
SpindleCommand::Logs(args) => logs(args).await,
15
+
SpindleCommand::Secret(cmd) => secret(cmd).await,
16
}
17
}
18
19
async fn list(args: SpindleListArgs) -> Result<()> {
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
+
}
60
Ok(())
61
}
62
63
async fn config(args: SpindleConfigArgs) -> Result<()> {
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
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
+
}
116
Ok(())
117
}
118
119
async fn run_pipeline(args: SpindleRunArgs) -> Result<()> {
120
+
println!(
121
+
"Spindle run (stub) repo={:?} branch={:?} wait={}",
122
+
args.repo, args.branch, args.wait
123
+
);
124
Ok(())
125
}
126
127
async fn logs(args: SpindleLogsArgs) -> Result<()> {
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
+
186
Ok(())
187
}
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
+2
-1
crates/tangled-cli/src/main.rs
+55
crates/tangled-cli/src/util.rs
+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
+7
-4
crates/tangled-config/src/config.rs
···
22
pub knot: Option<String>,
23
pub editor: Option<String>,
24
pub pager: Option<String>,
25
-
#[serde(default = "default_format")]
26
pub format: String,
27
}
28
29
-
fn default_format() -> String { "table".to_string() }
30
31
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32
pub struct AuthSection {
···
74
let path = path
75
.map(|p| p.to_path_buf())
76
.unwrap_or(default_config_path()?);
77
-
if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; }
78
let toml = toml::to_string_pretty(cfg)?;
79
fs::write(&path, toml)
80
.with_context(|| format!("Failed writing config file: {}", path.display()))?;
81
Ok(())
82
}
83
-
···
22
pub knot: Option<String>,
23
pub editor: Option<String>,
24
pub pager: Option<String>,
25
+
#[serde(default = "default_format")]
26
pub format: String,
27
}
28
29
+
fn default_format() -> String {
30
+
"table".to_string()
31
+
}
32
33
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
34
pub struct AuthSection {
···
76
let path = path
77
.map(|p| p.to_path_buf())
78
.unwrap_or(default_config_path()?);
79
+
if let Some(parent) = path.parent() {
80
+
std::fs::create_dir_all(parent)?;
81
+
}
82
let toml = toml::to_string_pretty(cfg)?;
83
fs::write(&path, toml)
84
.with_context(|| format!("Failed writing config file: {}", path.display()))?;
85
Ok(())
86
}
+13
-5
crates/tangled-config/src/keychain.rs
+13
-5
crates/tangled-config/src/keychain.rs
···
8
9
impl Keychain {
10
pub fn new(service: &str, account: &str) -> Self {
11
-
Self { service: service.into(), account: account.into() }
12
}
13
14
fn entry(&self) -> Result<Entry> {
···
16
}
17
18
pub fn set_password(&self, secret: &str) -> Result<()> {
19
-
self.entry()?.set_password(secret).map_err(|e| anyhow!("keyring error: {e}"))
20
}
21
22
pub fn get_password(&self) -> Result<String> {
23
-
self.entry()?.get_password().map_err(|e| anyhow!("keyring error: {e}"))
24
}
25
26
pub fn delete_password(&self) -> Result<()> {
27
-
self.entry()?.delete_password().map_err(|e| anyhow!("keyring error: {e}"))
28
}
29
}
30
-
···
8
9
impl Keychain {
10
pub fn new(service: &str, account: &str) -> Self {
11
+
Self {
12
+
service: service.into(),
13
+
account: account.into(),
14
+
}
15
}
16
17
fn entry(&self) -> Result<Entry> {
···
19
}
20
21
pub fn set_password(&self, secret: &str) -> Result<()> {
22
+
self.entry()?
23
+
.set_password(secret)
24
+
.map_err(|e| anyhow!("keyring error: {e}"))
25
}
26
27
pub fn get_password(&self) -> Result<String> {
28
+
self.entry()?
29
+
.get_password()
30
+
.map_err(|e| anyhow!("keyring error: {e}"))
31
}
32
33
pub fn delete_password(&self) -> Result<()> {
34
+
self.entry()?
35
+
.delete_credential()
36
+
.map_err(|e| anyhow!("keyring error: {e}"))
37
}
38
}
+1
-2
crates/tangled-config/src/lib.rs
+1
-2
crates/tangled-config/src/lib.rs
+13
-3
crates/tangled-config/src/session.rs
+13
-3
crates/tangled-config/src/session.rs
···
11
pub did: String,
12
pub handle: String,
13
#[serde(default)]
14
pub created_at: DateTime<Utc>,
15
}
16
···
21
refresh_jwt: String::new(),
22
did: String::new(),
23
handle: String::new(),
24
created_at: Utc::now(),
25
}
26
}
···
33
34
impl Default for SessionManager {
35
fn default() -> Self {
36
-
Self { service: "tangled-cli".into(), account: "default".into() }
37
}
38
}
39
40
impl SessionManager {
41
-
pub fn new(service: &str, account: &str) -> Self { Self { service: service.into(), account: account.into() } }
42
43
pub fn save(&self, session: &Session) -> Result<()> {
44
let keychain = Keychain::new(&self.service, &self.account);
···
59
keychain.delete_password()
60
}
61
}
62
-
···
11
pub did: String,
12
pub handle: String,
13
#[serde(default)]
14
+
pub pds: Option<String>,
15
+
#[serde(default)]
16
pub created_at: DateTime<Utc>,
17
}
18
···
23
refresh_jwt: String::new(),
24
did: String::new(),
25
handle: String::new(),
26
+
pds: None,
27
created_at: Utc::now(),
28
}
29
}
···
36
37
impl Default for SessionManager {
38
fn default() -> Self {
39
+
Self {
40
+
service: "tangled-cli".into(),
41
+
account: "default".into(),
42
+
}
43
}
44
}
45
46
impl SessionManager {
47
+
pub fn new(service: &str, account: &str) -> Self {
48
+
Self {
49
+
service: service.into(),
50
+
account: account.into(),
51
+
}
52
+
}
53
54
pub fn save(&self, session: &Session) -> Result<()> {
55
let keychain = Keychain::new(&self.service, &self.account);
···
70
keychain.delete_password()
71
}
72
}
-1
crates/tangled-git/src/operations.rs
-1
crates/tangled-git/src/operations.rs
+303
-7
docs/getting-started.md
+303
-7
docs/getting-started.md
···
1
-
# Getting Started
2
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.
4
5
-
## Build
6
7
-
Requires Rust toolchain and network access to fetch dependencies.
8
9
```
10
-
cargo build
11
```
12
13
-
## Run
14
15
```
16
-
cargo run -p tangled-cli -- --help
17
```
18
···
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
+
```
120
121
+
### View Issue Details
122
123
+
```sh
124
+
tangled issue show <issue-id>
125
+
```
126
127
+
### Comment on an Issue
128
129
+
```sh
130
+
tangled issue comment <issue-id> --body "I'm working on this!"
131
```
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"
139
```
140
141
+
The CLI will use `git format-patch` to create a patch from your branch.
142
143
+
### List Pull Requests
144
+
145
+
```sh
146
+
tangled pr list --repo my-project
147
```
148
+
149
+
### Review a Pull Request
150
+
151
+
```sh
152
+
tangled pr review <pr-id> --approve --comment "Looks good!"
153
```
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! 🧶