xtask-todo-lib 0.1.21

Todo workspace library and cargo devshell subcommand
Documentation
//! Devshell REPL and VFS: same logic as the `cargo-devshell` binary, exposed so tests can cover it.

pub mod command;
pub mod completion;
pub mod host_text;
pub mod parser;
pub mod sandbox;
pub mod script;
pub mod serialization;
pub mod session_store;
pub mod todo_io;
pub mod vfs;
pub mod vm;
pub mod workspace;

mod repl;

use std::cell::RefCell;
use std::io::{self, BufReader, IsTerminal};
use std::path::Path;
use std::rc::Rc;

use vfs::Vfs;

/// Error from `run_with` (usage or REPL failure).
#[derive(Debug)]
pub enum RunWithError {
    Usage,
    ReplFailed,
}

impl std::fmt::Display for RunWithError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Usage => f.write_str("usage error"),
            Self::ReplFailed => f.write_str("repl failed"),
        }
    }
}

impl std::error::Error for RunWithError {}

/// Run the devshell using process args and standard I/O (for the binary).
///
/// # Errors
/// Returns an error if usage is wrong or I/O fails critically.
pub fn run_main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = std::env::args().collect();
    let is_tty = io::stdin().is_terminal();
    let mut stdin = BufReader::new(io::stdin());
    let mut stdout = io::stdout();
    let mut stderr = io::stderr();
    run_main_from_args(&args, is_tty, &mut stdin, &mut stdout, &mut stderr)
}

/// Same as `run_main` but takes args, `is_tty`, and streams (for tests and callers that supply I/O).
///
/// # Errors
/// Returns an error if usage is wrong or I/O fails critically.
#[allow(clippy::too_many_lines)] // script vs REPL branches; split would scatter entry-point logic
pub fn run_main_from_args<R, W1, W2>(
    args: &[String],
    is_tty: bool,
    stdin: &mut R,
    stdout: &mut W1,
    stderr: &mut W2,
) -> Result<(), Box<dyn std::error::Error>>
where
    R: std::io::BufRead + std::io::Read,
    W1: std::io::Write,
    W2: std::io::Write,
{
    let positionals: Vec<&str> = args
        .iter()
        .skip(1)
        .filter(|a| *a != "-e" && *a != "-f")
        .map(String::as_str)
        .collect();
    let set_e = args.iter().skip(1).any(|a| a == "-e");
    let run_script = args.iter().skip(1).any(|a| a == "-f");

    if run_script {
        if positionals.len() != 1 {
            writeln!(stderr, "usage: dev_shell [-e] -f script.dsh")?;
            return Err(Box::new(std::io::Error::other("usage")));
        }
        let script_path = positionals[0];
        let script_src = match host_text::read_host_text(Path::new(script_path)) {
            Ok(s) => s,
            Err(e) => {
                writeln!(stderr, "dev_shell: {script_path}: {e}")?;
                return Err(e.into());
            }
        };
        let bin_path = Path::new(".dev_shell.bin");
        #[cfg(unix)]
        vm::export_devshell_workspace_root_env();
        let vm_session = if cfg!(unix) {
            vm::try_session_rc_or_host(stderr)
        } else {
            match vm::try_session_rc(stderr) {
                Ok(s) => s,
                Err(()) => return Err(Box::new(std::io::Error::other("vm session"))),
            }
        };
        let vfs: Rc<RefCell<Vfs>> =
            if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
                let root = vm::vm_workspace_host_root();
                std::fs::create_dir_all(&root)?;
                Rc::new(RefCell::new(Vfs::new_host_root(root)?))
            } else {
                let v = match serialization::load_from_file(bin_path) {
                    Ok(v) => v,
                    Err(e) => {
                        if e.kind() != io::ErrorKind::NotFound {
                            let _ =
                                writeln!(stderr, "Failed to load {}: {}", bin_path.display(), e);
                        }
                        Vfs::new()
                    }
                };
                Rc::new(RefCell::new(v))
            };
        if vm_session.borrow().is_guest_primary() {
            if let Err(e) =
                session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), bin_path)
            {
                let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
            }
        }
        script::run_script(
            &vfs,
            &vm_session,
            &script_src,
            bin_path,
            set_e,
            stdin,
            stdout,
            stderr,
        )
        .map_err(|e| Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error>)
    } else {
        let path = match positionals.as_slice() {
            [] => Path::new(".dev_shell.bin"),
            [p] => Path::new(p),
            _ => {
                writeln!(stderr, "usage: dev_shell [options] [path]")?;
                return Err(Box::new(std::io::Error::other("usage")));
            }
        };

        #[cfg(unix)]
        vm::export_devshell_workspace_root_env();

        let vm_session = if cfg!(unix) {
            vm::try_session_rc_or_host(stderr)
        } else {
            match vm::try_session_rc(stderr) {
                Ok(s) => s,
                Err(()) => return Err(Box::new(std::io::Error::other("vm session"))),
            }
        };

        #[cfg(all(unix, not(test)))]
        if vm::should_delegate_lima_shell(&vm_session, is_tty, run_script) {
            let vfs = Rc::new(RefCell::new(Vfs::new()));
            vm_session
                .borrow_mut()
                .ensure_ready(&vfs.borrow(), vfs.borrow().cwd())
                .map_err(|e| {
                    Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error>
                })?;
            let err = vm_session.borrow().exec_lima_interactive_shell();
            return Err(Box::new(err));
        }

        let vfs: Rc<RefCell<Vfs>> =
            if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
                let root = vm::vm_workspace_host_root();
                std::fs::create_dir_all(&root)?;
                Rc::new(RefCell::new(Vfs::new_host_root(root)?))
            } else {
                let v = match serialization::load_from_file(path) {
                    Ok(v) => v,
                    Err(e) => {
                        if e.kind() == io::ErrorKind::NotFound {
                            if positionals.len() > 1 {
                                let _ = writeln!(stderr, "File not found, starting with empty VFS");
                            }
                        } else {
                            let _ = writeln!(stderr, "Failed to load {}: {}", path.display(), e);
                        }
                        Vfs::new()
                    }
                };
                Rc::new(RefCell::new(v))
            };
        if vm_session.borrow().is_guest_primary() {
            if let Err(e) = session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), path)
            {
                let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
            }
        }
        repl::run(&vfs, &vm_session, is_tty, path, stdin, stdout, stderr).map_err(|()| {
            Box::new(std::io::Error::other("repl error")) as Box<dyn std::error::Error>
        })?;
        Ok(())
    }
}

/// Run the devshell with given args and streams (for tests).
///
/// # Errors
/// Returns `RunWithError::Usage` on invalid args; `RunWithError::ReplFailed` if the REPL exits with error.
pub fn run_with<R, W1, W2>(
    args: &[String],
    stdin: &mut R,
    stdout: &mut W1,
    stderr: &mut W2,
) -> Result<(), RunWithError>
where
    R: std::io::BufRead + std::io::Read,
    W1: std::io::Write,
    W2: std::io::Write,
{
    let path = match args {
        [] | [_] => Path::new(".dev_shell.bin"),
        [_, path] => Path::new(path),
        _ => {
            let _ = writeln!(stderr, "usage: dev_shell [path]");
            return Err(RunWithError::Usage);
        }
    };
    #[cfg(unix)]
    vm::export_devshell_workspace_root_env();
    let vm_session = if cfg!(unix) {
        vm::try_session_rc_or_host(stderr)
    } else {
        vm::try_session_rc(stderr).map_err(|()| RunWithError::ReplFailed)?
    };
    let vfs: Rc<RefCell<Vfs>> = if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
        let root = vm::vm_workspace_host_root();
        std::fs::create_dir_all(&root).map_err(|_| RunWithError::ReplFailed)?;
        Rc::new(RefCell::new(
            Vfs::new_host_root(root).map_err(|_| RunWithError::ReplFailed)?,
        ))
    } else {
        Rc::new(RefCell::new(
            serialization::load_from_file(path).unwrap_or_default(),
        ))
    };
    if vm_session.borrow().is_guest_primary() {
        if let Err(e) = session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), path) {
            let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
        }
    }
    repl::run(&vfs, &vm_session, false, path, stdin, stdout, stderr)
        .map_err(|()| RunWithError::ReplFailed)
}

#[cfg(test)]
mod tests;