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