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;
}
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"));
}
}