netsky 0.1.4

netsky CLI: the viable system launcher and subcommand dispatcher
Documentation
//! `netsky attach <target>` (alias: `netsky a <target>`) — attach to an
//! agent's tmux session. Fails gracefully if the session is not up.
//!
//! Target syntax:
//! - `0`..`N`      → `agent0`..`agentN`
//! - `infinity`    → `agentinfinity`
//! - `inf`         → shorthand for `infinity`
//! - `ticker`      → the `netsky-ticker` session
//!
//! When invoked from outside tmux the process execs `tmux attach`; from
//! inside tmux it execs `tmux switch-client` so the caller's outer session
//! stays intact. Execution replaces the current process, handing control
//! of the terminal to tmux.

use std::os::unix::process::CommandExt;
use std::process::Command;

use netsky_core::agent::AgentId;
use netsky_core::consts::{AGENTINFINITY_NAME, TICKER_SESSION, TMUX_BIN};
use netsky_sh::tmux;

pub fn run(target: &str) -> netsky_core::Result<()> {
    let session = resolve_target(target)?;

    if !tmux::has_session(&session) {
        netsky_core::bail!(
            "tmux session '{session}' is not up.\n\
             hint: `netsky up` brings the constellation up, or `netsky agent <N>` spawns one."
        );
    }

    let inside_tmux = std::env::var_os("TMUX").is_some();
    let subcmd = if inside_tmux {
        "switch-client"
    } else {
        "attach-session"
    };

    let err = Command::new(TMUX_BIN).args([subcmd, "-t", &session]).exec();
    Err(netsky_core::anyhow!("failed to exec tmux: {err}"))
}

/// Map a user-supplied target string to a canonical tmux session name.
fn resolve_target(target: &str) -> netsky_core::Result<String> {
    let t = target.trim();
    if t.is_empty() {
        netsky_core::bail!("missing target — try `netsky attach 0` or `netsky attach infinity`");
    }
    match t {
        "infinity" | "inf" => Ok(AGENTINFINITY_NAME.to_string()),
        "ticker" => Ok(TICKER_SESSION.to_string()),
        s => match s.parse::<u32>() {
            Ok(n) => Ok(AgentId::from_number(n).name()),
            Err(_) => netsky_core::bail!(
                "unrecognized target '{target}' — expected 0..N, `infinity`, `inf`, or `ticker`"
            ),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn resolves_numbers() {
        assert_eq!(resolve_target("0").unwrap(), "agent0");
        assert_eq!(resolve_target("7").unwrap(), "agent7");
        assert_eq!(resolve_target(" 3 ").unwrap(), "agent3");
    }

    #[test]
    fn resolves_aliases() {
        assert_eq!(resolve_target("infinity").unwrap(), "agentinfinity");
        assert_eq!(resolve_target("inf").unwrap(), "agentinfinity");
        assert_eq!(resolve_target("ticker").unwrap(), "netsky-ticker");
    }

    #[test]
    fn rejects_nonsense() {
        assert!(resolve_target("").is_err());
        assert!(resolve_target("   ").is_err());
        assert!(resolve_target("agent0").is_err());
        assert!(resolve_target("-1").is_err());
        assert!(resolve_target("xyz").is_err());
    }
}