workon 0.24.4

Development workspace launcher with Zellij, Claude CLI, and branchdiff
Documentation
use anyhow::Result;
use clap::{CommandFactory, Parser};

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

fn main() -> Result<()> {
    // 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),
    }
}

fn run_subcommand(command: cli::Command) -> Result<()> {
    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)
        }
        cli::Command::Attach { reference, config } => {
            workspace::cmd_attach(reference.as_deref(), config.as_deref())
        }
        cli::Command::Destroy { reference, no_save, json } => {
            workspace::cmd_destroy(reference.as_deref(), no_save, json)
        }
        cli::Command::List { json } => workspace::cmd_list(json),
        cli::Command::Path { reference } => workspace::cmd_path(reference.as_deref()),
    }
}

/// 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<()> {
    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,
        };
        workspace::run_workspace(&project.dir, &project.name, opts, &*vcs)?;
    } else {
        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(())
}