vkit 0.1.0

Fast Rust dev CLI: manage Node ports, run scripts, install & sync VS Code / Cursor extensions.
//! 运行选中的 scripts:暂时挂起 TUI,把终端完整交给子进程(继承 stdio),
//! 全部跑完后恢复 TUI。适配 `dev` 这类长驻/交互式进程(用户 Ctrl-C 退出后回到界面)。

use std::io::{self, stdout, Write};
use std::process::Command;

use anyhow::Result;
use ratatui::crossterm::{
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::DefaultTerminal;

use super::discover::{format_script_label, PackageManager, ScriptEntry};

/// 单个 script 的运行结果。
pub struct RunOutcome {
    pub label: String,
    pub exit_code: i32,
}

/// 离开备用屏 + 关 raw 模式,把终端还给子进程。
fn suspend() -> io::Result<()> {
    disable_raw_mode()?;
    execute!(stdout(), LeaveAlternateScreen)?;
    Ok(())
}

/// 重新进入备用屏 + 开 raw 模式,并清屏重绘。
fn resume(terminal: &mut DefaultTerminal) -> io::Result<()> {
    enable_raw_mode()?;
    execute!(stdout(), EnterAlternateScreen)?;
    terminal.clear()?;
    Ok(())
}

/// 顺序运行多个 scripts(继承 stdio)。整个过程挂起 TUI,结束后恢复。
///
/// 即使某个子进程非零退出也继续跑下一个;返回每个的退出码。
pub fn run_scripts(
    terminal: &mut DefaultTerminal,
    entries: &[ScriptEntry],
    pm: PackageManager,
) -> Result<Vec<RunOutcome>> {
    if entries.is_empty() {
        return Ok(Vec::new());
    }

    suspend()?;
    // suspend 之后确保先前 TUI 输出不会残留,从干净的一屏开始。
    let outcomes = run_all(entries, pm);
    resume(terminal)?;

    Ok(outcomes)
}

/// 挂起态下真正执行;不涉及 TUI,方便阅读。
fn run_all(entries: &[ScriptEntry], pm: PackageManager) -> Vec<RunOutcome> {
    let mut outcomes = Vec::with_capacity(entries.len());

    for (index, entry) in entries.iter().enumerate() {
        let label = format_script_label(entry, 28);
        print_banner(index, &label, pm, &entry.script);

        let exit_code = Command::new(pm.bin())
            .args(["run", &entry.script])
            .current_dir(&entry.package_path)
            .status()
            .map(|status| status.code().unwrap_or(1))
            .unwrap_or(1);

        outcomes.push(RunOutcome { label, exit_code });
    }

    outcomes
}

/// 每个任务前打印一行分隔横幅,方便在连续输出里辨认。
fn print_banner(index: usize, label: &str, pm: PackageManager, script: &str) {
    let mut out = stdout();
    if index > 0 {
        let _ = writeln!(out);
    }
    let _ = writeln!(out, "\n{label}  ({} run {script})\n", pm.label());
    let _ = out.flush();
}