yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The two world escape hatches (DESIGN §8.4, §16.4) — the human counterpart to
//! the embedded-crate agent tools. Both are multi-call subcommands of the yog
//! binary, beside `--editor-apply` (§9.3), and both are **pure argv → plan**
//! here; `main.rs` does the thin dispatch (print, or spawn-and-exit).
//!
//! - `yog env` prints the world's `export` lines ([`env_script`]) so
//!   `eval "$(yog env)"` drops the caller's shell *into* the world (§16.2),
//!   where the ambient `bl`/`lernie`/`bz` then operate on yog's nested state.
//!   Values are shell-quoted ([`shell_quote`]) so a path with a space or a
//!   quote survives `eval` intact.
//! - `yog exec <cmd…>` runs one command inside the world: [`parse_exec`] turns
//!   `[--cwd DIR] <cmd> [args…]` into an [`ExecPlan`], which `main.rs` runs
//!   through [`Cli::exec_in_world`](crate::cli_outbound::Cli::exec_in_world)
//!   (world overrides layered over the inherited env, stdio inherited, the
//!   child's exit faithfully yog's).

use std::path::PathBuf;

/// The `yog env` subcommand token (argv\[1\]).
pub const ENV_SUBCMD: &str = "env";
/// The `yog exec` subcommand token (argv\[1\]).
pub const EXEC_SUBCMD: &str = "exec";
/// The optional leading `--cwd <dir>` flag of `yog exec` (§8.4).
pub const CWD_FLAG: &str = "--cwd";

/// POSIX single-quote escaping: wrap `s` in `'…'` and rewrite each embedded
/// `'` as `'\''` (close-quote, an escaped literal quote, reopen-quote). The
/// result is one shell word that `eval` reproduces byte-for-byte — empty
/// strings, spaces, and quotes included — so `export VAR=<shell_quote(v)>` is
/// safe for any value.
pub fn shell_quote(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('\'');
    for ch in s.chars() {
        if ch == '\'' {
            out.push_str("'\\''");
        } else {
            out.push(ch);
        }
    }
    out.push('\'');
    out
}

/// The `yog env` product: one `export <VAR>='<value>'` line per world override
/// (§16.2 — `LERNIE_HOME`, `XDG_STATE_HOME`, in that order),
/// each value [`shell_quote`]d, newline-terminated. Fed the same
/// [`world::overrides`](crate::world::overrides) every spawn layers, so the
/// dir a human's `eval`'d shell writes is the dir yog watches.
pub fn env_script(overrides: &[(String, String)]) -> String {
    let mut out = String::new();
    for (key, value) in overrides {
        out.push_str("export ");
        out.push_str(key);
        out.push('=');
        out.push_str(&shell_quote(value));
        out.push('\n');
    }
    out
}

/// A parsed `yog exec` invocation: the command, its arguments, and the optional
/// working directory the child runs in.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecPlan {
    /// `--cwd <dir>` when given; the child otherwise inherits yog's cwd.
    pub cwd: Option<PathBuf>,
    /// The command to run — an arbitrary binary, taken verbatim.
    pub cmd: String,
    /// The command's own arguments (everything after `<cmd>`).
    pub args: Vec<String>,
}

/// Why a `yog exec` argv could not be turned into an [`ExecPlan`].
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ExecError {
    /// `--cwd` was given with no directory following it.
    #[error("--cwd requires a directory argument")]
    MissingCwdValue,
    /// No command was given (an empty argv, or only `--cwd <dir>`).
    #[error("no command given (usage: yog exec [--cwd DIR] <cmd> [args...])")]
    MissingCommand,
}

/// Parse the argv **after** `exec` into an [`ExecPlan`]: an optional leading
/// `--cwd <dir>` (only a *leading* one is yog's — a later `--cwd` belongs to the
/// command and passes through), then `<cmd> [args…]`. Pure; `main.rs` runs the
/// plan.
pub fn parse_exec(args: &[String]) -> Result<ExecPlan, ExecError> {
    let (cwd, rest) = match args.split_first() {
        Some((flag, tail)) if flag.as_str() == CWD_FLAG => {
            let (dir, more) = tail.split_first().ok_or(ExecError::MissingCwdValue)?;
            (Some(PathBuf::from(dir)), more)
        }
        _ => (None, args),
    };
    let (cmd, cmd_args) = rest.split_first().ok_or(ExecError::MissingCommand)?;
    Ok(ExecPlan {
        cwd,
        cmd: cmd.clone(),
        args: cmd_args.to_vec(),
    })
}

#[cfg(test)]
mod tests;