pub mod cli;
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 cli = cli::Cli::parse_from(args);
cli::dispatch(cli)
}
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()))
);
}
}