hyperstack-cli 0.6.7

CLI tool for generating TypeScript SDKs from HyperStack stream specifications
use anyhow::{Context, Result};
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Input, Select};
use std::fs;
use std::path::Path;
use std::process::{Command, Stdio};

use crate::telemetry;
use crate::templates::{
    customize_project, detect_package_manager, dev_command, install_command, start_command,
    Template, TemplateManager,
};
use crate::ui;

pub fn create(
    name: Option<String>,
    template: Option<String>,
    offline: bool,
    force_refresh: bool,
    skip_install: bool,
) -> Result<()> {
    let start = std::time::Instant::now();
    let theme = ColorfulTheme::default();

    let project_name = match name {
        Some(n) => n,
        None => Input::with_theme(&theme)
            .with_prompt("Project name")
            .default("my-hyperstack-app".to_string())
            .interact_text()
            .context("Failed to read project name")?,
    };

    let selected_template = match template {
        Some(t) => Template::from_str(&t).ok_or_else(|| {
            anyhow::anyhow!(
                "Unknown template: {}. Available: react-ore, rust-ore, typescript-ore",
                t
            )
        })?,
        None => {
            let items: Vec<String> = Template::ALL
                .iter()
                .map(|t| format!("{} - {}", t.display_name(), t.description()))
                .collect();

            let selection = Select::with_theme(&theme)
                .with_prompt("Select a template")
                .items(&items)
                .default(0)
                .interact()
                .context("Failed to select template")?;

            Template::ALL[selection]
        }
    };

    telemetry::record_template_selected(selected_template.display_name());

    let project_dir = Path::new(&project_name);

    if project_dir.exists() {
        anyhow::bail!(
            "Directory '{}' already exists. Choose a different name or remove it first.",
            project_name
        );
    }

    let manager = TemplateManager::new()?;

    if force_refresh {
        ui::print_step("Clearing template cache...");
        manager.clear_cache()?;
    }

    if !manager.is_cached() {
        if offline {
            anyhow::bail!(
                "Templates not cached and --offline specified. Run without --offline first."
            );
        }

        ui::print_step("Downloading templates...");
        manager.fetch_templates()?;
        println!("  {} Templates cached", ui::symbols::SUCCESS.green());
    }

    ui::print_step(&format!(
        "Creating {} from {}...",
        project_name.bold(),
        selected_template.display_name().cyan()
    ));

    fs::create_dir_all(project_dir)
        .with_context(|| format!("Failed to create directory: {}", project_name))?;

    manager.copy_template(selected_template, project_dir)?;
    customize_project(project_dir, &project_name)?;

    println!("  {} Project scaffolded", ui::symbols::SUCCESS.green());

    if selected_template.is_rust() {
        println!();
        print_rust_next_steps(&project_name);
    } else if selected_template.is_typescript_cli() {
        let pm = detect_package_manager();
        let install_succeeded = if skip_install {
            false
        } else {
            run_npm_install(project_dir, pm)?
        };

        println!();
        print_ts_cli_next_steps(&project_name, pm, install_succeeded);
    } else {
        let pm = detect_package_manager();
        let install_succeeded = if skip_install {
            false
        } else {
            run_npm_install(project_dir, pm)?
        };

        println!();
        print_js_next_steps(&project_name, pm, install_succeeded);
    }

    telemetry::record_create_completed(selected_template.display_name(), start.elapsed());

    Ok(())
}

fn run_npm_install(project_dir: &Path, pm: &str) -> Result<bool> {
    ui::print_step("Installing dependencies...");

    let (cmd, args) = match pm {
        "yarn" => ("yarn", vec!["install"]),
        "pnpm" => ("pnpm", vec!["install"]),
        "bun" => ("bun", vec!["install"]),
        _ => ("npm", vec!["install"]),
    };

    let status = Command::new(cmd)
        .args(&args)
        .current_dir(project_dir)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .with_context(|| format!("Failed to run {}", install_command(pm)))?;

    if status.success() {
        println!("  {} Dependencies installed", ui::symbols::SUCCESS.green());
        Ok(true)
    } else {
        println!(
            "  {} Install failed (exit code: {})",
            ui::symbols::FAILURE.red(),
            status.code().unwrap_or(-1)
        );
        println!(
            "    You can retry manually with: {}",
            install_command(pm).dimmed()
        );
        Ok(false)
    }
}

fn print_js_next_steps(project_name: &str, pm: &str, install_succeeded: bool) {
    println!(
        "{} {}",
        ui::symbols::SUCCESS.green().bold(),
        "Ready!".bold()
    );
    println!();

    if install_succeeded {
        println!("Start the dev server:");
        println!();
        println!(
            "  {} {} && {}",
            "$".dimmed(),
            format!("cd {}", project_name).cyan(),
            dev_command(pm).cyan()
        );
    } else {
        println!("Install dependencies and start:");
        println!();
        println!(
            "  {} {} && {} && {}",
            "$".dimmed(),
            format!("cd {}", project_name).cyan(),
            install_command(pm).cyan(),
            dev_command(pm).cyan()
        );
    }

    println!();
}

fn print_rust_next_steps(project_name: &str) {
    println!(
        "{} {}",
        ui::symbols::SUCCESS.green().bold(),
        "Ready!".bold()
    );
    println!();
    println!("Build and run:");
    println!();
    println!(
        "  {} {} && {}",
        "$".dimmed(),
        format!("cd {}", project_name).cyan(),
        "cargo run".cyan()
    );
    println!();
}

fn print_ts_cli_next_steps(project_name: &str, pm: &str, install_succeeded: bool) {
    println!(
        "{} {}",
        ui::symbols::SUCCESS.green().bold(),
        "Ready!".bold()
    );
    println!();

    if install_succeeded {
        println!("Run the CLI:");
        println!();
        println!(
            "  {} {} && {}",
            "$".dimmed(),
            format!("cd {}", project_name).cyan(),
            start_command(pm).cyan()
        );
    } else {
        println!("Install dependencies and run:");
        println!();
        println!(
            "  {} {} && {} && {}",
            "$".dimmed(),
            format!("cd {}", project_name).cyan(),
            install_command(pm).cyan(),
            start_command(pm).cyan()
        );
    }

    println!();
}