···
SpindleSecretAddArgs, SpindleSecretCommand, SpindleSecretListArgs, SpindleSecretRemoveArgs,
use anyhow::{anyhow, Result};
6
+
use futures_util::StreamExt;
use tangled_config::session::SessionManager;
8
+
use tokio_tungstenite::{connect_async, tungstenite::Message};
pub async fn run(_cli: &Cli, cmd: SpindleCommand) -> Result<()> {
···
async fn list(args: SpindleListArgs) -> Result<()> {
19
-
println!("Spindle list (stub) repo={:?}", args.repo);
21
+
let mgr = SessionManager::default();
24
+
.ok_or_else(|| anyhow!("Please login first: tangled auth login"))?;
29
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
30
+
.unwrap_or_else(|| "https://bsky.social".into());
31
+
let pds_client = tangled_api::TangledClient::new(&pds);
33
+
let (owner, name) = parse_repo_ref(
34
+
args.repo.as_deref().unwrap_or(&session.handle),
37
+
let info = pds_client
38
+
.get_repo_info(owner, name, Some(session.access_jwt.as_str()))
41
+
let pipelines = pds_client
42
+
.list_pipelines(&info.did, Some(session.access_jwt.as_str()))
45
+
if pipelines.is_empty() {
46
+
println!("No pipelines found for {}/{}", owner, name);
48
+
println!("RKEY\tKIND\tREPO\tWORKFLOWS");
49
+
for p in pipelines {
50
+
let workflows = p.pipeline.workflows
52
+
.map(|w| w.name.as_str())
53
+
.collect::<Vec<_>>()
58
+
p.pipeline.trigger_metadata.kind,
59
+
p.pipeline.trigger_metadata.repo.repo,
async fn config(args: SpindleConfigArgs) -> Result<()> {
25
-
"Spindle config (stub) repo={:?} url={:?} enable={} disable={}",
26
-
args.repo, args.url, args.enable, args.disable
68
+
let mgr = SessionManager::default();
71
+
.ok_or_else(|| anyhow!("Please login first: tangled auth login"))?;
73
+
if args.enable && args.disable {
74
+
return Err(anyhow!("Cannot use --enable and --disable together"));
77
+
if !args.enable && !args.disable && args.url.is_none() {
79
+
"Must provide --enable, --disable, or --url"
86
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
87
+
.unwrap_or_else(|| "https://bsky.social".into());
88
+
let pds_client = tangled_api::TangledClient::new(&pds);
90
+
let (owner, name) = parse_repo_ref(
91
+
args.repo.as_deref().unwrap_or(&session.handle),
94
+
let info = pds_client
95
+
.get_repo_info(owner, name, Some(session.access_jwt.as_str()))
98
+
let new_spindle = if args.disable {
100
+
} else if let Some(url) = args.url.as_deref() {
102
+
} else if args.enable {
103
+
// Default spindle URL
104
+
Some("https://spindle.tangled.sh")
106
+
return Err(anyhow!("Invalid flags combination"));
110
+
.update_repo_spindle(&info.did, &info.rkey, new_spindle, &pds, &session.access_jwt)
114
+
println!("Disabled spindle for {}/{}", owner, name);
117
+
"Enabled spindle for {}/{} ({})",
120
+
new_spindle.unwrap_or_default()
···
async fn logs(args: SpindleLogsArgs) -> Result<()> {
41
-
"Spindle logs (stub) job_id={} follow={} lines={:?}",
42
-
args.job_id, args.follow, args.lines
135
+
// Parse job_id: format is "knot:rkey:name" or just "name" (use repo context)
136
+
let parts: Vec<&str> = args.job_id.split(':').collect();
137
+
let (knot, rkey, name) = if parts.len() == 3 {
138
+
(parts[0].to_string(), parts[1].to_string(), parts[2].to_string())
139
+
} else if parts.len() == 1 {
140
+
// Use repo context - need to get repo info
141
+
let mgr = SessionManager::default();
144
+
.ok_or_else(|| anyhow!("Please login first: tangled auth login"))?;
148
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
149
+
.unwrap_or_else(|| "https://bsky.social".into());
150
+
let pds_client = tangled_api::TangledClient::new(&pds);
151
+
// Get repo info from current directory context or default to user's handle
152
+
let info = pds_client
153
+
.get_repo_info(&session.handle, &session.handle, Some(session.access_jwt.as_str()))
155
+
(info.knot, info.rkey, parts[0].to_string())
157
+
return Err(anyhow!("Invalid job_id format. Expected 'knot:rkey:name' or 'name'"));
160
+
// Build WebSocket URL - spindle base is typically https://spindle.tangled.sh
161
+
let spindle_base = std::env::var("TANGLED_SPINDLE_BASE")
162
+
.unwrap_or_else(|_| "wss://spindle.tangled.sh".to_string());
163
+
let ws_url = format!("{}/spindle/logs/{}/{}/{}", spindle_base, knot, rkey, name);
165
+
println!("Connecting to logs stream for {}:{}:{}...", knot, rkey, name);
167
+
// Connect to WebSocket
168
+
let (ws_stream, _) = connect_async(&ws_url).await
169
+
.map_err(|e| anyhow!("Failed to connect to log stream: {}", e))?;
171
+
let (mut _write, mut read) = ws_stream.split();
173
+
// Stream log messages
174
+
let mut line_count = 0;
175
+
let max_lines = args.lines.unwrap_or(usize::MAX);
177
+
while let Some(msg) = read.next().await {
179
+
Ok(Message::Text(text)) => {
180
+
println!("{}", text);
182
+
if line_count >= max_lines {
186
+
Ok(Message::Close(_)) => {
190
+
return Err(anyhow!("WebSocket error: {}", e));