use std::path::PathBuf;
use netsky_core::config::{self, Config as RuntimeConfig, NetskyToml, netsky_toml_path};
use netsky_core::consts::{
ENV_DISK_MIN_MB, ENV_NETSKY_DIR, ENV_OWNER_IMESSAGE, ENV_OWNER_NAME, ENV_TICKER_INTERVAL,
};
use netsky_core::paths::resolve_netsky_dir;
use serde_json::{Value, json};
use crate::cli::ConfigCommand;
pub fn run(sub: ConfigCommand) -> netsky_core::Result<()> {
match sub {
ConfigCommand::Show { json } => show(json),
ConfigCommand::Check { json } => check(json),
}
}
fn show(json: bool) -> netsky_core::Result<()> {
let report = gather();
if json {
println!("{}", serde_json::to_string_pretty(&report.to_json())?);
return Ok(());
}
render_text(&report);
Ok(())
}
struct Report {
netsky_dir: PathBuf,
netsky_toml: TomlPlane,
runtime: ConfigPlane,
env: Vec<EnvVar>,
effective: Vec<EffectiveValue>,
}
struct TomlPlane {
path: PathBuf,
status: PlaneStatus,
fields: Vec<(&'static str, String)>,
}
struct ConfigPlane {
dir: PathBuf,
owner_path: PathBuf,
channels_path: PathBuf,
addendum_path: PathBuf,
status: PlaneStatus,
fields: Vec<(&'static str, String)>,
active_host: Option<String>,
}
enum PlaneStatus {
Present,
Absent,
Error(String),
}
struct EnvVar {
name: &'static str,
set: bool,
value: Option<String>,
}
struct EffectiveValue {
key: &'static str,
value: String,
source: &'static str,
}
fn gather() -> Report {
let netsky_dir = resolve_netsky_dir();
let toml_path = netsky_toml_path();
let netsky_toml = match NetskyToml::load_from(&toml_path) {
Ok(None) => TomlPlane {
path: toml_path,
status: PlaneStatus::Absent,
fields: Vec::new(),
},
Ok(Some(cfg)) => TomlPlane {
path: toml_path,
status: PlaneStatus::Present,
fields: netsky_toml_fields(&cfg),
},
Err(e) => TomlPlane {
path: toml_path,
status: PlaneStatus::Error(e.to_string()),
fields: Vec::new(),
},
};
let config_dir = config::config_dir();
let owner_path = config::owner_path();
let channels_path = config::channels_path();
let addendum_path = config::addendum_path();
let active_host = config::active_host_label().ok().flatten();
let (config_status, config_fields) = match RuntimeConfig::load() {
Ok(cfg) => (PlaneStatus::Present, runtime_fields(&cfg)),
Err(e) => (PlaneStatus::Error(e.to_string()), Vec::new()),
};
let any_present = owner_path.exists() || channels_path.exists() || addendum_path.exists();
let runtime = ConfigPlane {
dir: config_dir,
owner_path,
channels_path,
addendum_path,
status: if !any_present {
PlaneStatus::Absent
} else {
config_status
},
fields: config_fields,
active_host,
};
let env_names: &[&str] = &[
ENV_NETSKY_DIR,
ENV_OWNER_NAME,
ENV_OWNER_IMESSAGE,
ENV_TICKER_INTERVAL,
ENV_DISK_MIN_MB,
"MACHINE_TYPE",
"NETSKY_EMAIL_AUTO_SEND",
];
let env = env_names
.iter()
.map(|name| {
let raw = std::env::var(name).ok();
let is_set = raw.as_deref().is_some_and(|s| !s.is_empty());
EnvVar {
name,
set: is_set,
value: raw.filter(|v| !v.is_empty()).map(|v| redact(name, &v)),
}
})
.collect();
let effective = vec![
{
let env = std::env::var(ENV_OWNER_NAME).ok().filter(|s| !s.is_empty());
EffectiveValue {
key: "owner.name",
value: netsky_core::config::owner_name(),
source: if env.is_some() {
"env"
} else if has_owner_name_in_toml() {
"netsky.toml"
} else {
"default"
},
}
},
{
let env = std::env::var(ENV_OWNER_IMESSAGE)
.ok()
.filter(|s| !s.is_empty());
let resolved = netsky_core::config::owner_imessage();
EffectiveValue {
key: "owner.imessage",
value: match &resolved {
Some(_) => "<set>".to_string(),
None => "<unset>".to_string(),
},
source: if env.is_some() {
"env"
} else if resolved.is_some() {
"runtime owner.toml"
} else {
"unset"
},
}
},
{
let env = std::env::var(ENV_TICKER_INTERVAL)
.ok()
.filter(|s| !s.is_empty());
EffectiveValue {
key: "tuning.ticker_interval_s",
value: env
.clone()
.or_else(ticker_interval_toml)
.unwrap_or_else(|| netsky_core::consts::TICKER_INTERVAL_DEFAULT_S.to_string()),
source: if env.is_some() {
"env"
} else if ticker_interval_toml().is_some() {
"netsky.toml"
} else {
"default"
},
}
},
];
Report {
netsky_dir,
netsky_toml,
runtime,
env,
effective,
}
}
fn redact(name: &str, value: &str) -> String {
if name.contains("IMESSAGE") || name.contains("TOKEN") || name.contains("SECRET") {
"<set>".to_string()
} else {
value.to_string()
}
}
fn has_owner_name_in_toml() -> bool {
NetskyToml::load()
.ok()
.flatten()
.and_then(|c| c.owner)
.and_then(|o| o.name)
.is_some_and(|s| !s.is_empty())
}
fn ticker_interval_toml() -> Option<String> {
NetskyToml::load()
.ok()
.flatten()
.and_then(|c| c.tuning)
.and_then(|t| t.ticker_interval_s)
.map(|n| n.to_string())
}
fn netsky_toml_fields(cfg: &NetskyToml) -> Vec<(&'static str, String)> {
let mut out: Vec<(&'static str, String)> = Vec::new();
if let Some(v) = cfg.schema_version {
out.push(("schema_version", v.to_string()));
}
if let Some(o) = &cfg.owner {
if let Some(n) = &o.name {
out.push(("owner.name", n.clone()));
}
if o.imessage.is_some() {
out.push(("owner.imessage", "<set>".to_string()));
}
}
if let Some(a) = &cfg.addendum
&& let Some(p) = &a.agent0
{
out.push(("addendum.agent0", p.clone()));
}
if let Some(n) = &cfg.netsky
&& let Some(mid) = &n.machine_id
{
out.push(("netsky.machine_id", mid.clone()));
}
if let Some(t) = &cfg.tuning
&& let Some(i) = t.ticker_interval_s
{
out.push(("tuning.ticker_interval_s", i.to_string()));
}
out
}
fn runtime_fields(cfg: &RuntimeConfig) -> Vec<(&'static str, String)> {
let mut out: Vec<(&'static str, String)> = Vec::new();
if cfg.owner.imessage_handle.is_some() {
out.push(("owner.imessage_handle", "<set>".to_string()));
}
if !cfg.owner.email_addresses.is_empty() {
out.push((
"owner.email_addresses",
format!("{} account(s)", cfg.owner.email_addresses.len()),
));
}
if let Some(gh) = &cfg.owner.github_username {
out.push(("owner.github_username", gh.clone()));
}
if !cfg.owner.github_orgs.is_empty() {
out.push(("owner.github_orgs", cfg.owner.github_orgs.join(", ")));
}
if let Some(host) = &cfg.host {
out.push(("host.label", host.label.clone()));
}
out
}
fn render_text(r: &Report) {
println!("netsky_dir: {}", r.netsky_dir.display());
println!();
println!("netsky.toml");
println!(" path: {}", r.netsky_toml.path.display());
match &r.netsky_toml.status {
PlaneStatus::Present => println!(" status: present"),
PlaneStatus::Absent => println!(" status: absent (env + defaults active)"),
PlaneStatus::Error(e) => println!(" status: ERROR — {e}"),
}
for (k, v) in &r.netsky_toml.fields {
println!(" {k} = {v}");
}
println!();
println!("runtime ({})", r.runtime.dir.display());
println!(" owner.toml: {}", r.runtime.owner_path.display());
println!(" channels.toml: {}", r.runtime.channels_path.display());
println!(" addendum.md: {}", r.runtime.addendum_path.display());
if let Some(label) = &r.runtime.active_host {
println!(" active-host: {label}");
}
match &r.runtime.status {
PlaneStatus::Present => println!(" status: present"),
PlaneStatus::Absent => println!(" status: absent"),
PlaneStatus::Error(e) => println!(" status: ERROR — {e}"),
}
for (k, v) in &r.runtime.fields {
println!(" {k} = {v}");
}
println!();
println!("environment");
for ev in &r.env {
match (&ev.value, ev.set) {
(Some(v), true) => println!(" {} = {v}", ev.name),
(_, _) => println!(" {} <unset>", ev.name),
}
}
println!();
println!("effective");
for ev in &r.effective {
println!(" {} = {} (source: {})", ev.key, ev.value, ev.source);
}
}
impl Report {
fn to_json(&self) -> Value {
let toml_fields: Vec<Value> = self
.netsky_toml
.fields
.iter()
.map(|(k, v)| json!({"key": k, "value": v}))
.collect();
let cfg_fields: Vec<Value> = self
.runtime
.fields
.iter()
.map(|(k, v)| json!({"key": k, "value": v}))
.collect();
let env: Vec<Value> = self
.env
.iter()
.map(|e| {
json!({
"name": e.name,
"set": e.set,
"value": e.value,
})
})
.collect();
let effective: Vec<Value> = self
.effective
.iter()
.map(|e| json!({"key": e.key, "value": e.value, "source": e.source}))
.collect();
let overall = if matches!(self.netsky_toml.status, PlaneStatus::Error(_))
|| matches!(self.runtime.status, PlaneStatus::Error(_))
{
"red"
} else {
"green"
};
json!({
"command": "config show",
"status": overall,
"summary": format!(
"netsky_dir={} netsky.toml={} runtime={}",
self.netsky_dir.display(),
plane_label(&self.netsky_toml.status),
plane_label(&self.runtime.status),
),
"generated_at": chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(),
"data": {
"netsky_dir": self.netsky_dir.display().to_string(),
"netsky_toml": {
"path": self.netsky_toml.path.display().to_string(),
"status": plane_label(&self.netsky_toml.status),
"error": plane_error(&self.netsky_toml.status),
"fields": toml_fields,
},
"runtime": {
"dir": self.runtime.dir.display().to_string(),
"owner_path": self.runtime.owner_path.display().to_string(),
"channels_path": self.runtime.channels_path.display().to_string(),
"addendum_path": self.runtime.addendum_path.display().to_string(),
"active_host": self.runtime.active_host,
"status": plane_label(&self.runtime.status),
"error": plane_error(&self.runtime.status),
"fields": cfg_fields,
},
"env": env,
"effective": effective,
},
})
}
}
fn plane_label(status: &PlaneStatus) -> &'static str {
match status {
PlaneStatus::Present => "present",
PlaneStatus::Absent => "absent",
PlaneStatus::Error(_) => "error",
}
}
fn plane_error(status: &PlaneStatus) -> Option<String> {
if let PlaneStatus::Error(e) = status {
Some(e.clone())
} else {
None
}
}
fn check(json: bool) -> netsky_core::Result<()> {
let mut errors: Vec<(String, String)> = Vec::new();
let toml_path = netsky_toml_path();
if let Err(e) = NetskyToml::load_from(&toml_path) {
errors.push((
format!("netsky.toml at {}", toml_path.display()),
e.to_string(),
));
}
if let Err(e) = RuntimeConfig::load() {
errors.push((
"runtime config (owner/channels/host)".to_string(),
e.to_string(),
));
}
if json {
let envelope = json!({
"command": "config check",
"status": if errors.is_empty() { "green" } else { "red" },
"summary": if errors.is_empty() {
"all config files parse".to_string()
} else {
format!("{} parse error(s)", errors.len())
},
"generated_at": chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(),
"data": {
"errors": errors.iter().map(|(src, msg)| json!({"source": src, "error": msg})).collect::<Vec<_>>(),
},
});
println!("{}", serde_json::to_string_pretty(&envelope)?);
} else if errors.is_empty() {
println!("all config files parse");
} else {
for (src, msg) in &errors {
println!("ERROR {src}: {msg}");
}
}
if !errors.is_empty() {
netsky_core::bail!("config check: {} error(s)", errors.len());
}
Ok(())
}