morph-cli 0.1.0

AST-based codebase migration and codemod tool for JavaScript and TypeScript projects.
Documentation
mod ai;
mod benchmark;
mod dashboard;
mod deps;
mod doctor;
mod docs;
mod explain;
mod graph;
mod history;
mod init;
mod list;
mod magic;
mod plan;
mod plugins;
mod preset;
mod run;
mod scan;
mod score;
mod simulate;
mod search;
mod sessions;
mod validate;
mod verify;
mod version;
mod watch;
mod completions;
mod manifest;
mod ignored;
mod dry_run;

use std::path::Path;

use anyhow::{Result, bail};

use crate::cli::{Cli, Command, PluginAction, PresetAction, AiAction, ScanAction};
use crate::commands::sessions::SessionsArgs;

pub fn execute(cli: Cli, project_root: &Path) -> Result<()> {
    match cli.command {
        Command::Version => version::execute(),
        Command::List { path, category, tag } => list::execute(&path, category.as_deref(), tag.as_deref()),
        Command::Search { query, path, category, maturity } => {
            search::execute(&query, &path, category.as_deref(), maturity.as_deref())
        }
        Command::History { limit } => history::execute(limit, project_root),
        Command::Init { path } => init::execute(&path),
        Command::Dashboard { port } => dashboard::execute(port, project_root),
        Command::Magic { path } => magic::execute(&path, project_root),
        Command::Benchmark { path } => benchmark::execute(&path, project_root),
        Command::ValidateRepo { path } => validate::execute(&path, project_root),
        Command::Ai { action } => match action {
            AiAction::Suggest { file } => ai::suggest(&file),
        },
        Command::Sessions => sessions::execute(project_root, &SessionsArgs::List),
        Command::Session { id } => sessions::execute(project_root, &SessionsArgs::Show { id }),
        Command::Plan { path, tag } => plan::execute(&path, tag.as_deref()),
        Command::Explain { file } => explain::execute(&file),
        Command::Scans => scan::execute_list(project_root),
        Command::Scan { action, path, tag, verbose } => {
            if let Some(act) = action {
                match act {
                    ScanAction::Show { id } => scan::execute_show(&id, project_root),
                }
            } else {
                scan::execute(&path, tag.as_deref(), verbose, project_root)
            }
        }
        Command::Deps { path } => deps::execute(&path),
        Command::Score { path, format } => score::execute(&path, &format),
        Command::Simulate { recipes, path } => simulate::execute(&recipes, &path),
        Command::Graph { graph_type, format, path } => graph::execute(&graph_type, &format, &path),
        Command::Preset { action } => match action {
            PresetAction::List => preset::list(),
            PresetAction::Run { name, path, write } => preset::run(&name, &path, write, project_root),
        },
        Command::Watch {
            path,
            recipes,
            debounce_ms,
        } => watch::execute(&path, &recipes, debounce_ms),
        Command::Rollback {
            session_id,
            preview,
            force,
        } => sessions::execute(
            project_root,
            &SessionsArgs::Rollback {
                session_id,
                preview,
                force,
            },
        ),
        Command::Verify { path } => verify::execute(&path),
        Command::Replay { session_id, write } => sessions::execute(
            project_root,
            &SessionsArgs::Replay {
                session_id,
                write,
            },
        ),
        Command::Resume { checkpoint } => run::resume(&checkpoint, project_root),
        Command::Completions { shell } => completions::execute(shell),
        Command::Manifest { action } => manifest::execute(action, project_root),
        Command::Ignored { path, detailed } => ignored::execute(&path, detailed),
        Command::DryRuns => dry_run::execute_list(project_root),
        Command::DryRun { action } => dry_run::execute_show(action, project_root),
        Command::Run {
            args,
            write,
            dry_run,
            review,
            verbose,
            summary_only,
            max_preview_lines,
            allow_risky,
            strict,
            report_json,
            report_md,
            report_dir,
            format,
            prettier,
            no_format,
            jobs,
            sequential,
            autofix,
            package,
            profile,
            output_style,
            tag,
        } => {
            let (path, recipes) = split_run_args(args)?;
            run::execute(
            &recipes,
            &path,
            dry_run,
            write,
            review,
            autofix,
            verbose,
            summary_only,
            max_preview_lines,
            allow_risky,
            strict,
            report_json,
            report_md,
            &report_dir,
            format,
            prettier,
            no_format,
            jobs,
            sequential,
            project_root,
            package.as_deref(),
            profile.as_deref(),
            output_style.as_deref(),
            tag.as_deref(),
            )
        }
        Command::Plugins { action } => match action {
            PluginAction::List => plugins::list::execute(project_root),
            PluginAction::Info { name } => plugins::info::execute(&name),
        },
        Command::Docs => docs::execute(),
        Command::Doctor { path } => doctor::execute(&path),
    }
}

fn split_run_args(mut args: Vec<String>) -> Result<(std::path::PathBuf, Vec<String>)> {
    let Some(path) = args.pop() else {
        bail!("Usage: morph run <recipe...> <path>");
    };

    if args.is_empty() {
        bail!("Usage: morph run <recipe...> <path>");
    }

    Ok((std::path::PathBuf::from(path), args))
}