coven 0.1.0

A minimal streaming display and workflow runner for Claude Code's -p mode
Documentation
mod cli;

use std::path::PathBuf;

use anyhow::Result;
use clap::Parser;
use coven::commands;
use coven::vcr::{Io, VcrContext};

use cli::{Cli, Command};

#[tokio::main]
async fn main() -> Result<()> {
    // Panic hook to restore terminal state
    let default_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        crossterm::terminal::disable_raw_mode().ok();
        default_hook(info);
    }));

    let cli = Cli::parse();

    match cli.command {
        Some(Command::Init) => {
            commands::init::init()?;
        }
        Some(Command::Status) => {
            commands::status::status()?;
        }
        Some(Command::Gc) => {
            commands::gc::gc()?;
        }
        Some(Command::Ralph {
            prompt,
            iterations,
            break_tag,
            no_break,
            show_thinking,
            fork,
            claude_args,
        }) => {
            if no_break && iterations == 0 {
                anyhow::bail!("--no-break requires --iterations to prevent infinite looping");
            }
            let (mut io, vcr) = create_live_io();
            commands::ralph::ralph(
                commands::ralph::RalphConfig {
                    prompt,
                    iterations,
                    break_tag,
                    no_break,
                    show_thinking,
                    fork,
                    extra_args: claude_args,
                    working_dir: None,
                },
                &mut io,
                &vcr,
                std::io::stdout(),
            )
            .await?;
        }
        Some(Command::Worker {
            branch,
            worktree_base,
            show_thinking,
            fork,
            claude_args,
        }) => {
            let base = match worktree_base {
                Some(b) => b,
                None => default_worktree_base()?,
            };
            let (mut io, vcr) = create_live_io();
            commands::worker::worker(
                commands::worker::WorkerConfig {
                    show_thinking,
                    branch,
                    worktree_base: base,
                    extra_args: claude_args,
                    working_dir: None,
                    fork,
                },
                &mut io,
                &vcr,
                std::io::stdout(),
            )
            .await?;
        }
        None => {
            let (mut io, vcr) = create_live_io();
            commands::run::run(
                commands::run::RunConfig {
                    prompt: cli.prompt,
                    extra_args: cli.claude_args,
                    show_thinking: cli.show_thinking,
                    fork: cli.fork,
                    working_dir: None,
                },
                &mut io,
                &vcr,
                std::io::stdout(),
            )
            .await?;
        }
    }

    Ok(())
}

/// Create a live `Io` and `VcrContext` for production use.
///
/// Spawns a background task that reads crossterm events and forwards them
/// to the terminal event channel. The event channel starts empty — the first
/// `SessionRunner::spawn` should provide claude events via `io.replace_event_channel()`.
fn create_live_io() -> (Io, VcrContext) {
    use crossterm::event::EventStream;
    use futures::StreamExt;
    use tokio::sync::mpsc;

    let (term_tx, term_rx) = mpsc::unbounded_channel();
    let (_event_tx, event_rx) = mpsc::unbounded_channel();

    // Background task: forward crossterm events to the channel
    tokio::spawn(async move {
        let mut stream = EventStream::new();
        while let Some(Ok(event)) = stream.next().await {
            if term_tx.send(event).is_err() {
                break;
            }
        }
    });

    let io = Io::new(event_rx, term_rx);
    let vcr = VcrContext::live();
    (io, vcr)
}

fn default_worktree_base() -> Result<PathBuf> {
    let home = std::env::var("HOME").map_err(|_| {
        anyhow::anyhow!("HOME not set; use --worktree-base to specify worktree location")
    })?;
    Ok(PathBuf::from(home).join(".coven").join("worktrees"))
}