···
SpindleSecretAddArgs, SpindleSecretCommand, SpindleSecretListArgs, SpindleSecretRemoveArgs,
use anyhow::{anyhow, Result};
+
use futures_util::StreamExt;
use tangled_config::session::SessionManager;
+
use tokio_tungstenite::{connect_async, tungstenite::Message};
pub async fn run(_cli: &Cli, cmd: SpindleCommand) -> Result<()> {
···
async fn list(args: SpindleListArgs) -> Result<()> {
+
let mgr = SessionManager::default();
+
.ok_or_else(|| anyhow!("Please login first: tangled auth login"))?;
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
+
.unwrap_or_else(|| "https://bsky.social".into());
+
let pds_client = tangled_api::TangledClient::new(&pds);
+
let (owner, name) = parse_repo_ref(
+
args.repo.as_deref().unwrap_or(&session.handle),
+
.get_repo_info(owner, name, Some(session.access_jwt.as_str()))
+
let pipelines = pds_client
+
.list_pipelines(&info.did, Some(session.access_jwt.as_str()))
+
if pipelines.is_empty() {
+
println!("No pipelines found for {}/{}", owner, name);
+
println!("RKEY\tKIND\tREPO\tWORKFLOWS");
+
let workflows = p.pipeline.workflows
+
.map(|w| w.name.as_str())
+
p.pipeline.trigger_metadata.kind,
+
p.pipeline.trigger_metadata.repo.repo,
async fn config(args: SpindleConfigArgs) -> Result<()> {
+
let mgr = SessionManager::default();
+
.ok_or_else(|| anyhow!("Please login first: tangled auth login"))?;
+
if args.enable && args.disable {
+
return Err(anyhow!("Cannot use --enable and --disable together"));
+
if !args.enable && !args.disable && args.url.is_none() {
+
"Must provide --enable, --disable, or --url"
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
+
.unwrap_or_else(|| "https://bsky.social".into());
+
let pds_client = tangled_api::TangledClient::new(&pds);
+
let (owner, name) = parse_repo_ref(
+
args.repo.as_deref().unwrap_or(&session.handle),
+
.get_repo_info(owner, name, Some(session.access_jwt.as_str()))
+
let new_spindle = if args.disable {
+
} else if let Some(url) = args.url.as_deref() {
+
} else if args.enable {
+
Some("https://spindle.tangled.sh")
+
return Err(anyhow!("Invalid flags combination"));
+
.update_repo_spindle(&info.did, &info.rkey, new_spindle, &pds, &session.access_jwt)
+
println!("Disabled spindle for {}/{}", owner, name);
+
"Enabled spindle for {}/{} ({})",
+
new_spindle.unwrap_or_default()
···
async fn logs(args: SpindleLogsArgs) -> Result<()> {
+
// Parse job_id: format is "knot:rkey:name" or just "name" (use repo context)
+
let parts: Vec<&str> = args.job_id.split(':').collect();
+
let (knot, rkey, name) = if parts.len() == 3 {
+
(parts[0].to_string(), parts[1].to_string(), parts[2].to_string())
+
} else if parts.len() == 1 {
+
// Use repo context - need to get repo info
+
let mgr = SessionManager::default();
+
.ok_or_else(|| anyhow!("Please login first: tangled auth login"))?;
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
+
.unwrap_or_else(|| "https://bsky.social".into());
+
let pds_client = tangled_api::TangledClient::new(&pds);
+
// Get repo info from current directory context or default to user's handle
+
.get_repo_info(&session.handle, &session.handle, Some(session.access_jwt.as_str()))
+
(info.knot, info.rkey, parts[0].to_string())
+
return Err(anyhow!("Invalid job_id format. Expected 'knot:rkey:name' or 'name'"));
+
// Build WebSocket URL - spindle base is typically https://spindle.tangled.sh
+
let spindle_base = std::env::var("TANGLED_SPINDLE_BASE")
+
.unwrap_or_else(|_| "wss://spindle.tangled.sh".to_string());
+
let ws_url = format!("{}/spindle/logs/{}/{}/{}", spindle_base, knot, rkey, name);
+
println!("Connecting to logs stream for {}:{}:{}...", knot, rkey, name);
+
// Connect to WebSocket
+
let (ws_stream, _) = connect_async(&ws_url).await
+
.map_err(|e| anyhow!("Failed to connect to log stream: {}", e))?;
+
let (mut _write, mut read) = ws_stream.split();
+
let mut line_count = 0;
+
let max_lines = args.lines.unwrap_or(usize::MAX);
+
while let Some(msg) = read.next().await {
+
Ok(Message::Text(text)) => {
+
if line_count >= max_lines {
+
Ok(Message::Close(_)) => {
+
return Err(anyhow!("WebSocket error: {}", e));