+1
Cargo.lock
+1
Cargo.lock
+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
-
+184
-32
crates/tangled-api/src/client.rs
+184
-32
crates/tangled-api/src/client.rs
···
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() }
13
-
}
14
-
15
-
pub fn default() -> Self {
16
-
Self::new("https://tangled.org")
20
+
Self {
21
+
base_url: base_url.into(),
22
+
}
17
23
}
18
24
19
25
fn xrpc_url(&self, method: &str) -> String {
20
-
format!(
21
-
"{}/xrpc/{}",
22
-
self.base_url.trim_end_matches('/'),
23
-
method
24
-
)
26
+
format!("{}/xrpc/{}", self.base_url.trim_end_matches('/'), method)
25
27
}
26
28
27
29
async fn post_json<TReq: Serialize, TRes: DeserializeOwned>(
···
36
38
.post(url)
37
39
.header(reqwest::header::CONTENT_TYPE, "application/json");
38
40
if let Some(token) = bearer {
39
-
reqb = reqb.header(
40
-
reqwest::header::AUTHORIZATION,
41
-
format!("Bearer {}", token),
42
-
);
41
+
reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token));
43
42
}
44
43
let res = reqb.json(req).send().await?;
45
44
let status = res.status();
···
58
57
) -> Result<TRes> {
59
58
let url = self.xrpc_url(method);
60
59
let client = reqwest::Client::new();
61
-
let mut reqb = client.get(url).query(¶ms);
60
+
let mut reqb = client
61
+
.get(&url)
62
+
.query(¶ms)
63
+
.header(reqwest::header::ACCEPT, "application/json");
62
64
if let Some(token) = bearer {
63
-
reqb = reqb.header(
64
-
reqwest::header::AUTHORIZATION,
65
-
format!("Bearer {}", token),
66
-
);
65
+
reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token));
67
66
}
68
67
let res = reqb.send().await?;
69
68
let status = res.status();
69
+
let body = res.text().await.unwrap_or_default();
70
70
if !status.is_success() {
71
-
let body = res.text().await.unwrap_or_default();
72
-
return Err(anyhow!("{}: {}", status, body));
71
+
return Err(anyhow!("GET {} -> {}: {}", url, status, body));
73
72
}
74
-
Ok(res.json::<TRes>().await?)
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
+
})
75
82
}
76
83
77
84
pub async fn login_with_password(
···
119
126
starred: bool,
120
127
bearer: Option<&str>,
121
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
+
122
153
#[derive(Deserialize)]
123
-
struct Envelope {
124
-
repos: Vec<Repository>,
154
+
struct RecordItem {
155
+
value: Repository,
125
156
}
126
-
let mut q = vec![];
127
-
if let Some(u) = user {
128
-
q.push(("user", u.to_string()));
157
+
#[derive(Deserialize)]
158
+
struct ListRes {
159
+
#[serde(default)]
160
+
records: Vec<RecordItem>,
129
161
}
162
+
163
+
let params = vec![
164
+
("repo", did),
165
+
("collection", "sh.tangled.repo".to_string()),
166
+
("limit", "100".to_string()),
167
+
];
168
+
169
+
let res: ListRes = self
170
+
.get_json("com.atproto.repo.listRecords", ¶ms, bearer)
171
+
.await?;
172
+
let mut repos: Vec<Repository> = res.records.into_iter().map(|r| r.value).collect();
173
+
// Apply optional filters client-side
130
174
if let Some(k) = knot {
131
-
q.push(("knot", k.to_string()));
175
+
repos.retain(|r| r.knot.as_deref().unwrap_or("") == k);
132
176
}
133
177
if starred {
134
-
q.push(("starred", true.to_string()));
178
+
// TODO: implement starred filtering when API is available. For now, no-op.
135
179
}
136
-
let env: Envelope = self
137
-
.get_json("sh.tangled.repo.list", &q, bearer)
180
+
Ok(repos)
181
+
}
182
+
183
+
pub async fn create_repo(&self, opts: CreateRepoOptions<'_>) -> Result<()> {
184
+
// 1) Create the sh.tangled.repo record on the user's PDS
185
+
#[derive(Serialize)]
186
+
struct Record<'a> {
187
+
name: &'a str,
188
+
knot: &'a str,
189
+
#[serde(skip_serializing_if = "Option::is_none")]
190
+
description: Option<&'a str>,
191
+
#[serde(rename = "createdAt")]
192
+
created_at: String,
193
+
}
194
+
#[derive(Serialize)]
195
+
struct CreateRecordReq<'a> {
196
+
repo: &'a str,
197
+
collection: &'a str,
198
+
validate: bool,
199
+
record: Record<'a>,
200
+
}
201
+
#[derive(Deserialize)]
202
+
struct CreateRecordRes {
203
+
uri: String,
204
+
}
205
+
206
+
let now = chrono::Utc::now().to_rfc3339();
207
+
let rec = Record {
208
+
name: opts.name,
209
+
knot: opts.knot,
210
+
description: opts.description,
211
+
created_at: now,
212
+
};
213
+
let create_req = CreateRecordReq {
214
+
repo: opts.did,
215
+
collection: "sh.tangled.repo",
216
+
validate: true,
217
+
record: rec,
218
+
};
219
+
220
+
let pds_client = TangledClient::new(opts.pds_base);
221
+
let created: CreateRecordRes = pds_client
222
+
.post_json(
223
+
"com.atproto.repo.createRecord",
224
+
&create_req,
225
+
Some(opts.access_jwt),
226
+
)
138
227
.await?;
139
-
Ok(env.repos)
228
+
229
+
// Extract rkey from at-uri: at://did/collection/rkey
230
+
let rkey = created
231
+
.uri
232
+
.rsplit('/')
233
+
.next()
234
+
.ok_or_else(|| anyhow!("failed to parse rkey from uri"))?;
235
+
236
+
// 2) Obtain a service auth token for the Tangled server (aud = did:web:<host>)
237
+
let host = self
238
+
.base_url
239
+
.trim_end_matches('/')
240
+
.strip_prefix("https://")
241
+
.or_else(|| self.base_url.trim_end_matches('/').strip_prefix("http://"))
242
+
.ok_or_else(|| anyhow!("invalid base_url"))?;
243
+
let audience = format!("did:web:{}", host);
244
+
245
+
#[derive(Deserialize)]
246
+
struct GetSARes {
247
+
token: String,
248
+
}
249
+
let params = [
250
+
("aud", audience),
251
+
("exp", (chrono::Utc::now().timestamp() + 600).to_string()),
252
+
];
253
+
let sa: GetSARes = pds_client
254
+
.get_json(
255
+
"com.atproto.server.getServiceAuth",
256
+
¶ms,
257
+
Some(opts.access_jwt),
258
+
)
259
+
.await?;
260
+
261
+
// 3) Call sh.tangled.repo.create with the rkey
262
+
#[derive(Serialize)]
263
+
struct CreateRepoReq<'a> {
264
+
rkey: &'a str,
265
+
#[serde(skip_serializing_if = "Option::is_none")]
266
+
#[serde(rename = "defaultBranch")]
267
+
default_branch: Option<&'a str>,
268
+
#[serde(skip_serializing_if = "Option::is_none")]
269
+
source: Option<&'a str>,
270
+
}
271
+
let req = CreateRepoReq {
272
+
rkey,
273
+
default_branch: opts.default_branch,
274
+
source: opts.source,
275
+
};
276
+
// No output expected on success
277
+
let _: serde_json::Value = self.post_json(REPO_CREATE, &req, Some(&sa.token)).await?;
278
+
Ok(())
140
279
}
141
280
}
142
281
···
147
286
pub name: String,
148
287
pub knot: Option<String>,
149
288
pub description: Option<String>,
289
+
#[serde(default)]
150
290
pub private: bool,
151
291
}
292
+
293
+
#[derive(Debug, Clone)]
294
+
pub struct CreateRepoOptions<'a> {
295
+
pub did: &'a str,
296
+
pub name: &'a str,
297
+
pub knot: &'a str,
298
+
pub description: Option<&'a str>,
299
+
pub default_branch: Option<&'a str>,
300
+
pub source: Option<&'a str>,
301
+
pub pds_base: &'a str,
302
+
pub access_jwt: &'a str,
303
+
}
+3
crates/tangled-cli/src/cli.rs
+3
crates/tangled-cli/src/cli.rs
+5
-4
crates/tangled-cli/src/commands/auth.rs
+5
-4
crates/tangled-cli/src/commands/auth.rs
···
21
21
Some(p) => p,
22
22
None => Password::new().with_prompt("Password").interact()?,
23
23
};
24
-
let pds = args.pds.unwrap_or_else(|| "https://bsky.social".to_string());
24
+
let pds = args
25
+
.pds
26
+
.unwrap_or_else(|| "https://bsky.social".to_string());
25
27
26
28
let client = tangled_api::TangledClient::new(&pds);
27
-
let session = client
28
-
.login_with_password(&handle, &password, &pds)
29
-
.await?;
29
+
let mut session = client.login_with_password(&handle, &password, &pds).await?;
30
+
session.pds = Some(pds.clone());
30
31
SessionManager::default().save(&session)?;
31
32
println!("Logged in as '{}' ({})", session.handle, session.did);
32
33
Ok(())
+24
-10
crates/tangled-cli/src/commands/issue.rs
+24
-10
crates/tangled-cli/src/commands/issue.rs
···
1
+
use crate::cli::{
2
+
Cli, IssueCommand, IssueCommentArgs, IssueCreateArgs, IssueEditArgs, IssueListArgs,
3
+
IssueShowArgs,
4
+
};
1
5
use anyhow::Result;
2
-
use crate::cli::{Cli, IssueCommand, IssueListArgs, IssueCreateArgs, IssueShowArgs, IssueEditArgs, IssueCommentArgs};
3
6
4
7
pub async fn run(_cli: &Cli, cmd: IssueCommand) -> Result<()> {
5
8
match cmd {
···
12
15
}
13
16
14
17
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);
18
+
println!(
19
+
"Issue list (stub) repo={:?} state={:?} author={:?} label={:?} assigned={:?}",
20
+
args.repo, args.state, args.author, args.label, args.assigned
21
+
);
17
22
Ok(())
18
23
}
19
24
20
25
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);
26
+
println!(
27
+
"Issue create (stub) repo={:?} title={:?} body={:?} labels={:?} assign={:?}",
28
+
args.repo, args.title, args.body, args.label, args.assign
29
+
);
23
30
Ok(())
24
31
}
25
32
26
33
async fn show(args: IssueShowArgs) -> Result<()> {
27
-
println!("Issue show (stub) id={} comments={} json={}", args.id, args.comments, args.json);
34
+
println!(
35
+
"Issue show (stub) id={} comments={} json={}",
36
+
args.id, args.comments, args.json
37
+
);
28
38
Ok(())
29
39
}
30
40
31
41
async fn edit(args: IssueEditArgs) -> Result<()> {
32
-
println!("Issue edit (stub) id={} title={:?} body={:?} state={:?}",
33
-
args.id, args.title, args.body, args.state);
42
+
println!(
43
+
"Issue edit (stub) id={} title={:?} body={:?} state={:?}",
44
+
args.id, args.title, args.body, args.state
45
+
);
34
46
Ok(())
35
47
}
36
48
37
49
async fn comment(args: IssueCommentArgs) -> Result<()> {
38
-
println!("Issue comment (stub) id={} close={} body={:?}", args.id, args.close, args.body);
50
+
println!(
51
+
"Issue comment (stub) id={} close={} body={:?}",
52
+
args.id, args.close, args.body
53
+
);
39
54
Ok(())
40
55
}
41
-
+9
-4
crates/tangled-cli/src/commands/knot.rs
+9
-4
crates/tangled-cli/src/commands/knot.rs
···
1
+
use crate::cli::{Cli, KnotAddArgs, KnotCommand, KnotListArgs, KnotRefArgs, KnotVerifyArgs};
1
2
use anyhow::Result;
2
-
use crate::cli::{Cli, KnotCommand, KnotListArgs, KnotAddArgs, KnotVerifyArgs, KnotRefArgs};
3
3
4
4
pub async fn run(_cli: &Cli, cmd: KnotCommand) -> Result<()> {
5
5
match cmd {
···
12
12
}
13
13
14
14
async fn list(args: KnotListArgs) -> Result<()> {
15
-
println!("Knot list (stub) public={} owned={}", args.public, args.owned);
15
+
println!(
16
+
"Knot list (stub) public={} owned={}",
17
+
args.public, args.owned
18
+
);
16
19
Ok(())
17
20
}
18
21
19
22
async fn add(args: KnotAddArgs) -> Result<()> {
20
-
println!("Knot add (stub) url={} did={:?} name={:?} verify={}", args.url, args.did, args.name, args.verify);
23
+
println!(
24
+
"Knot add (stub) url={} did={:?} name={:?} verify={}",
25
+
args.url, args.did, args.name, args.verify
26
+
);
21
27
Ok(())
22
28
}
23
29
···
35
41
println!("Knot remove (stub) url={}", args.url);
36
42
Ok(())
37
43
}
38
-
+3
-7
crates/tangled-cli/src/commands/mod.rs
+3
-7
crates/tangled-cli/src/commands/mod.rs
···
1
1
pub mod auth;
2
-
pub mod repo;
3
2
pub mod issue;
3
+
pub mod knot;
4
4
pub mod pr;
5
-
pub mod knot;
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
···
21
20
}
22
21
}
23
22
24
-
fn not_implemented(feature: &str) -> Result<()> {
25
-
eprintln!("{} {}", "[todo]".yellow().bold(), feature);
26
-
Ok(())
27
-
}
23
+
// All subcommands are currently implemented with stubs where needed.
+21
-11
crates/tangled-cli/src/commands/pr.rs
+21
-11
crates/tangled-cli/src/commands/pr.rs
···
1
+
use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrMergeArgs, PrReviewArgs, PrShowArgs};
1
2
use anyhow::Result;
2
-
use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrShowArgs, PrReviewArgs, PrMergeArgs};
3
3
4
4
pub async fn run(_cli: &Cli, cmd: PrCommand) -> Result<()> {
5
5
match cmd {
···
12
12
}
13
13
14
14
async fn list(args: PrListArgs) -> Result<()> {
15
-
println!("PR list (stub) repo={:?} state={:?} author={:?} reviewer={:?}",
16
-
args.repo, args.state, args.author, args.reviewer);
15
+
println!(
16
+
"PR list (stub) repo={:?} state={:?} author={:?} reviewer={:?}",
17
+
args.repo, args.state, args.author, args.reviewer
18
+
);
17
19
Ok(())
18
20
}
19
21
20
22
async fn create(args: PrCreateArgs) -> Result<()> {
21
-
println!("PR create (stub) repo={:?} base={:?} head={:?} title={:?} draft={}",
22
-
args.repo, args.base, args.head, args.title, args.draft);
23
+
println!(
24
+
"PR create (stub) repo={:?} base={:?} head={:?} title={:?} draft={}",
25
+
args.repo, args.base, args.head, args.title, args.draft
26
+
);
23
27
Ok(())
24
28
}
25
29
26
30
async fn show(args: PrShowArgs) -> Result<()> {
27
-
println!("PR show (stub) id={} diff={} comments={} checks={}", args.id, args.diff, args.comments, args.checks);
31
+
println!(
32
+
"PR show (stub) id={} diff={} comments={} checks={}",
33
+
args.id, args.diff, args.comments, args.checks
34
+
);
28
35
Ok(())
29
36
}
30
37
31
38
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);
39
+
println!(
40
+
"PR review (stub) id={} approve={} request_changes={} comment={:?}",
41
+
args.id, args.approve, args.request_changes, args.comment
42
+
);
34
43
Ok(())
35
44
}
36
45
37
46
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);
47
+
println!(
48
+
"PR merge (stub) id={} squash={} rebase={} no_ff={}",
49
+
args.id, args.squash, args.rebase, args.no_ff
50
+
);
40
51
Ok(())
41
52
}
42
-
+82
-13
crates/tangled-cli/src/commands/repo.rs
+82
-13
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 serde_json;
3
+
use tangled_config::session::SessionManager;
3
4
4
-
pub async fn run(_cli: &Cli, cmd: RepoCommand) -> Result<()> {
5
+
use crate::cli::{
6
+
Cli, OutputFormat, RepoCloneArgs, RepoCommand, RepoCreateArgs, RepoDeleteArgs, RepoInfoArgs,
7
+
RepoListArgs, RepoRefArgs,
8
+
};
9
+
10
+
pub async fn run(cli: &Cli, cmd: RepoCommand) -> Result<()> {
5
11
match cmd {
6
-
RepoCommand::List(args) => list(args).await,
12
+
RepoCommand::List(args) => list(cli, args).await,
7
13
RepoCommand::Create(args) => create(args).await,
8
14
RepoCommand::Clone(args) => clone(args).await,
9
15
RepoCommand::Info(args) => info(args).await,
···
13
19
}
14
20
}
15
21
16
-
async fn list(args: RepoListArgs) -> Result<()> {
17
-
println!("Listing repositories (stub) knot={:?} user={:?} starred={}",
18
-
args.knot, args.user, args.starred);
22
+
async fn list(cli: &Cli, args: RepoListArgs) -> Result<()> {
23
+
let mgr = SessionManager::default();
24
+
let session = match mgr.load()? {
25
+
Some(s) => s,
26
+
None => return Err(anyhow!("Please login first: tangled auth login")),
27
+
};
28
+
29
+
// Use the PDS to list repo records for the user
30
+
let pds = session
31
+
.pds
32
+
.clone()
33
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
34
+
.unwrap_or_else(|| "https://bsky.social".into());
35
+
let pds_client = tangled_api::TangledClient::new(pds);
36
+
// Default to the logged-in user handle if --user is not provided
37
+
let effective_user = args.user.as_deref().unwrap_or(session.handle.as_str());
38
+
let repos = pds_client
39
+
.list_repos(
40
+
Some(effective_user),
41
+
args.knot.as_deref(),
42
+
args.starred,
43
+
Some(session.access_jwt.as_str()),
44
+
)
45
+
.await?;
46
+
47
+
match cli.format {
48
+
OutputFormat::Json => {
49
+
let json = serde_json::to_string_pretty(&repos)?;
50
+
println!("{}", json);
51
+
}
52
+
OutputFormat::Table => {
53
+
println!("NAME\tKNOT\tPRIVATE");
54
+
for r in repos {
55
+
println!("{}\t{}\t{}", r.name, r.knot.unwrap_or_default(), r.private);
56
+
}
57
+
}
58
+
}
59
+
19
60
Ok(())
20
61
}
21
62
22
63
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
-
);
64
+
let mgr = SessionManager::default();
65
+
let session = match mgr.load()? {
66
+
Some(s) => s,
67
+
None => return Err(anyhow!("Please login first: tangled auth login")),
68
+
};
69
+
70
+
let base = std::env::var("TANGLED_API_BASE").unwrap_or_else(|_| "https://tngl.sh".into());
71
+
let client = tangled_api::TangledClient::new(base);
72
+
73
+
// Determine PDS base and target knot hostname
74
+
let pds = session
75
+
.pds
76
+
.clone()
77
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
78
+
.unwrap_or_else(|| "https://bsky.social".into());
79
+
let knot = args.knot.unwrap_or_else(|| "tngl.sh".to_string());
80
+
81
+
let opts = tangled_api::client::CreateRepoOptions {
82
+
did: &session.did,
83
+
name: &args.name,
84
+
knot: &knot,
85
+
description: args.description.as_deref(),
86
+
default_branch: None,
87
+
source: None,
88
+
pds_base: &pds,
89
+
access_jwt: &session.access_jwt,
90
+
};
91
+
client.create_repo(opts).await?;
92
+
93
+
println!("Created repo '{}' (knot: {})", args.name, knot);
27
94
Ok(())
28
95
}
29
96
30
97
async fn clone(args: RepoCloneArgs) -> Result<()> {
31
-
println!("Cloning repo '{}' (stub) https={} depth={:?}", args.repo, args.https, args.depth);
98
+
println!(
99
+
"Cloning repo '{}' (stub) https={} depth={:?}",
100
+
args.repo, args.https, args.depth
101
+
);
32
102
Ok(())
33
103
}
34
104
···
54
124
println!("Unstarring repo '{}' (stub)", args.repo);
55
125
Ok(())
56
126
}
57
-
+11
-4
crates/tangled-cli/src/commands/spindle.rs
+11
-4
crates/tangled-cli/src/commands/spindle.rs
···
1
+
use crate::cli::{
2
+
Cli, SpindleCommand, SpindleConfigArgs, SpindleListArgs, SpindleLogsArgs, SpindleRunArgs,
3
+
};
1
4
use anyhow::Result;
2
-
use crate::cli::{Cli, SpindleCommand, SpindleListArgs, SpindleConfigArgs, SpindleRunArgs, SpindleLogsArgs};
3
5
4
6
pub async fn run(_cli: &Cli, cmd: SpindleCommand) -> Result<()> {
5
7
match cmd {
···
24
26
}
25
27
26
28
async fn run_pipeline(args: SpindleRunArgs) -> Result<()> {
27
-
println!("Spindle run (stub) repo={:?} branch={:?} wait={}", args.repo, args.branch, args.wait);
29
+
println!(
30
+
"Spindle run (stub) repo={:?} branch={:?} wait={}",
31
+
args.repo, args.branch, args.wait
32
+
);
28
33
Ok(())
29
34
}
30
35
31
36
async fn logs(args: SpindleLogsArgs) -> Result<()> {
32
-
println!("Spindle logs (stub) job_id={} follow={} lines={:?}", args.job_id, args.follow, args.lines);
37
+
println!(
38
+
"Spindle logs (stub) job_id={} follow={} lines={:?}",
39
+
args.job_id, args.follow, args.lines
40
+
);
33
41
Ok(())
34
42
}
35
-
+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
-4
crates/tangled-config/src/keychain.rs
+13
-4
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_credential().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
}
+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
-