netsky 0.2.0

netsky CLI: the viable system launcher and subcommand dispatcher
Documentation
//! netsky CLI entry point. Parses argv with clap, dispatches to
//! [`cmd`] submodules. Each subcommand owns a small file there.

pub mod cli;
pub mod clone_tools;
pub mod cmd;
pub mod observability;
pub mod ui;

use clap::Parser;

pub fn run<I, T>(args: I) -> netsky_core::Result<()>
where
    I: IntoIterator<Item = T>,
    T: Into<std::ffi::OsString> + Clone,
{
    load_home_dotenv();
    let argv: Vec<std::ffi::OsString> = args.into_iter().map(Into::into).collect();
    if let Some(message) = parse_hint(&argv) {
        return Err(netsky_core::anyhow!("{}", message));
    }
    let cli = cli::Cli::parse_from(argv);
    cli::dispatch(cli)
}

fn parse_hint(args: &[std::ffi::OsString]) -> Option<String> {
    let command = args.get(1)?.to_string_lossy();
    let subcommand = args
        .get(2)
        .map(|value| value.to_string_lossy().into_owned());
    match (command.as_ref(), subcommand.as_deref()) {
        ("tasks", _) => Some(
            "unknown top-level verb `tasks`. Use `netsky sync tasks ...` for the Google Tasks sync view or `netsky task ...` for the meta.db source of truth.".to_string(),
        ),
        ("codex", _) => Some(
            "unknown top-level verb `codex`. Use `netsky agent <N> --type codex` for one resident clone or `netsky up <N> --type codex` for a Codex constellation.".to_string(),
        ),
        ("quiet", _) => Some(
            "unknown top-level verb `quiet`. The operator-facing quiet sentinel verb was removed because no valid caller remains.".to_string(),
        ),
        ("nap", _) => Some(
            "unknown top-level verb `nap`. ScheduleWakeup is denied and `netsky nap` was removed. Drop the call instead of renaming it.".to_string(),
        ),
        ("task", Some("add")) => Some(
            "unknown `netsky task` verb `add`. Use `netsky task create \"<title>\"`.".to_string(),
        ),
        ("channel", Some("list")) => Some(
            "unknown `netsky channel` verb `list`. Use `netsky channel drain <agent>` to read inboxes, `netsky channel send <target> <text>` to write one envelope, or `netsky channel quarantine <agent> --list` to inspect quarantined envelopes.".to_string(),
        ),
        _ => None,
    }
}

fn load_home_dotenv() {
    let path = netsky_core::paths::home().join(".env");
    let Ok(contents) = std::fs::read_to_string(path) else {
        return;
    };
    for line in contents.lines() {
        let Some((key, value)) = parse_dotenv_line(line) else {
            continue;
        };
        if std::env::var_os(&key).is_some() {
            continue;
        }
        // SAFETY: netsky calls this at CLI startup before it spawns worker
        // threads. Existing environment values win, so the loader cannot
        // race with later code that intentionally set a variable.
        unsafe {
            std::env::set_var(key, value);
        }
    }
}

fn parse_dotenv_line(line: &str) -> Option<(String, String)> {
    let mut s = line.trim();
    if s.is_empty() || s.starts_with('#') {
        return None;
    }
    if let Some(rest) = s.strip_prefix("export ") {
        s = rest.trim_start();
    }
    let (key, raw_value) = s.split_once('=')?;
    let key = key.trim();
    if key.is_empty()
        || !key
            .chars()
            .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
        || key.chars().next().is_some_and(|ch| ch.is_ascii_digit())
    {
        return None;
    }
    let value = parse_dotenv_value(raw_value.trim());
    Some((key.to_string(), value))
}

fn parse_dotenv_value(value: &str) -> String {
    if value.len() >= 2 {
        let bytes = value.as_bytes();
        let quote = bytes[0] as char;
        if (quote == '"' || quote == '\'') && bytes[value.len() - 1] as char == quote {
            let inner = &value[1..value.len() - 1];
            if quote == '"' {
                return unescape_double_quoted(inner);
            }
            return inner.to_string();
        }
    }

    strip_inline_comment(value).trim_end().to_string()
}

fn strip_inline_comment(value: &str) -> &str {
    let mut escaped = false;
    for (idx, ch) in value.char_indices() {
        if escaped {
            escaped = false;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            continue;
        }
        if ch == '#'
            && value[..idx]
                .chars()
                .next_back()
                .is_some_and(char::is_whitespace)
        {
            return &value[..idx];
        }
    }
    value
}

fn unescape_double_quoted(value: &str) -> String {
    let mut out = String::with_capacity(value.len());
    let mut chars = value.chars();
    while let Some(ch) = chars.next() {
        if ch != '\\' {
            out.push(ch);
            continue;
        }
        match chars.next() {
            Some('n') => out.push('\n'),
            Some('r') => out.push('\r'),
            Some('t') => out.push('\t'),
            Some('"') => out.push('"'),
            Some('\\') => out.push('\\'),
            Some(other) => {
                out.push('\\');
                out.push(other);
            }
            None => out.push('\\'),
        }
    }
    out
}

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

    #[test]
    fn dotenv_line_parses_export_and_quotes() {
        assert_eq!(
            parse_dotenv_line("export NETSKY_IROH_PEER_WORK_NODEID=\"abc\""),
            Some((
                "NETSKY_IROH_PEER_WORK_NODEID".to_string(),
                "abc".to_string()
            ))
        );
        assert_eq!(
            parse_dotenv_line("NETSKY_EMAIL_AUTO_SEND='1'"),
            Some(("NETSKY_EMAIL_AUTO_SEND".to_string(), "1".to_string()))
        );
    }

    #[test]
    fn dotenv_line_ignores_comments_and_invalid_keys() {
        assert_eq!(parse_dotenv_line("# x"), None);
        assert_eq!(parse_dotenv_line("1BAD=x"), None);
        assert_eq!(
            parse_dotenv_line("NETSKY_IROH_LABEL=work # local peer"),
            Some(("NETSKY_IROH_LABEL".to_string(), "work".to_string()))
        );
    }

    #[test]
    fn parse_hint_catches_removed_and_misnamed_verbs() {
        let removed = parse_hint(&[
            "netsky".into(),
            "tasks".into(),
            "ls".into(),
            "cody@dkdc.dev".into(),
        ])
        .expect("expected removed tasks hint");
        assert!(removed.contains("netsky sync tasks"));

        let renamed = parse_hint(&["netsky".into(), "task".into(), "add".into()])
            .expect("expected task add hint");
        assert!(renamed.contains("netsky task create"));

        let channel = parse_hint(&["netsky".into(), "channel".into(), "list".into()])
            .expect("expected channel list hint");
        assert!(channel.contains("netsky channel drain"));
    }
}