workon 0.25.1

Development workspace launcher with Zellij, Claude CLI, and branchdiff
Documentation
use std::process::ExitCode;

use anyhow::Result;
use clap::{CommandFactory, Parser};

use workon::workspace::{self, WorkspaceOptions};
use workon::{cli, deps, layout, not_ready, resolve, session, vcs};

fn main() -> Result<ExitCode> {
    // Serve shell-completion requests (COMPLETE=<shell> workon …) and exit;
    // a no-op on a normal invocation. Enables dynamic ws_id/nickname candidates.
    clap_complete::CompleteEnv::with_factory(cli::Cli::command).complete();

    let cli = cli::Cli::parse();
    match cli.command {
        Some(command) => run_subcommand(command),
        None => run_session(cli.session),
    }
}

/// `true` (ready) is success; `false` means a setup step failed and the workspace is not ready.
fn exit_code(ready: bool) -> ExitCode {
    if ready {
        ExitCode::SUCCESS
    } else {
        ExitCode::from(not_ready::EXIT_NOT_READY)
    }
}

fn run_subcommand(command: cli::Command) -> Result<ExitCode> {
    match command {
        cli::Command::Create { name, config, skip_copy_ignored, json } => {
            let project = resolve::resolve()?;
            let vcs = vcs::detect(&project.dir)?;
            let args = workspace::CreateArgs {
                skip_copy_ignored,
                name: cli::given_name(name.as_deref()),
                config: config.as_deref(),
                json,
            };
            workspace::cmd_create(&project.dir, &project.name, args, &*vcs).map(exit_code)
        }
        cli::Command::Attach { reference, config } => {
            workspace::cmd_attach(reference.as_deref(), config.as_deref()).map(|()| ExitCode::SUCCESS)
        }
        cli::Command::Destroy { reference, no_save, json } => {
            workspace::cmd_destroy(reference.as_deref(), no_save, json).map(|()| ExitCode::SUCCESS)
        }
        cli::Command::List { json } => workspace::cmd_list(json).map(|()| ExitCode::SUCCESS),
        cli::Command::Path { reference } => workspace::cmd_path(reference.as_deref()).map(|()| ExitCode::SUCCESS),
    }
}

/// The default (no-subcommand) flow: a session in cwd, or the ephemeral `-w`
/// workspace. Unchanged from before subcommands existed.
fn run_session(session: cli::SessionArgs) -> Result<ExitCode> {
    let project = resolve::resolve()?;
    let config = session.config.as_deref();

    // Resolve the layout first so deps::check_all knows which binaries to require
    // and we can fail fast on resume + configs that can't resume.
    let cfg = layout::read_config(config)?;
    layout::validate_layout(&cfg.layout)?;
    deps::check_all(&cfg.layout)?;

    if session.resume.is_some() {
        cfg.ensure_resume_compatible(config.unwrap_or("default"))?;
    }

    // Only the workspace flow injects, and it provisions a worktree first — so
    // reject an un-injectable layout here rather than halfway through setup.
    if session.workspace {
        cfg.ensure_single_agent_pane()?;
    }

    let name = cli::given_name(session.name.as_deref());

    if session.workspace {
        let vcs = vcs::detect(&project.dir)?;
        let opts = WorkspaceOptions {
            skip_copy_ignored: session.skip_copy_ignored,
            label: name,
            resume: session.resume.as_deref(),
            config,
            cfg: &cfg,
        };
        return workspace::run_workspace(&project.dir, &project.name, opts, &*vcs).map(exit_code);
    }
    let layout = cfg.resolve()?;
    session::run(
        name.unwrap_or(&project.name),
        layout.path(),
        &project.dir,
        session.new_session,
        &cfg.layout,
        config.unwrap_or("default"),
    )?;
    Ok(ExitCode::SUCCESS)
}