netsky 0.1.5

netsky CLI: the viable system launcher and subcommand dispatcher
//! `netsky init [--update] [--path <dir>]` — bootstrap a netsky home dir
//! from the compiled binary.

use std::fs;
use std::path::{Path, PathBuf};

mod embedded {
    include!(concat!(env!("OUT_DIR"), "/embedded_files.rs"));
}

pub fn run(path: Option<PathBuf>, update: bool) -> netsky_core::Result<()> {
    let root = path.unwrap_or_else(default_root);
    fs::create_dir_all(&root)?;
    ensure_dir(&root.join("notes"))?;
    ensure_dir(&root.join("briefs"))?;
    ensure_dir(&root.join("workspaces"))?;
    ensure_agents_dir(&root)?;
    ensure_claude_symlink(&root)?;

    let mut wrote = 0usize;
    for &(rel, content) in embedded::EMBEDDED_FILES {
        let target = root.join(rel);
        if rel == ".claude/settings.json" && update {
            continue;
        }
        if target.exists() && !update {
            continue;
        }
        if let Some(parent) = target.parent() {
            fs::create_dir_all(parent)?;
        }
        atomic_write(&target, &render_content(rel, content))?;
        println!("  wrote {rel}");
        wrote += 1;
    }

    println!("netsky init: wrote {wrote} files to {}", root.display());
    Ok(())
}

fn default_root() -> PathBuf {
    netsky_core::paths::home().join("netsky")
}

fn ensure_agents_dir(root: &Path) -> netsky_core::Result<()> {
    fs::create_dir_all(root.join(".agents"))?;
    fs::create_dir_all(root.join(".agents/skills"))?;
    Ok(())
}

fn ensure_dir(path: &Path) -> netsky_core::Result<()> {
    fs::create_dir_all(path)?;
    Ok(())
}

fn ensure_claude_symlink(root: &Path) -> netsky_core::Result<()> {
    let link = root.join(".claude");
    match fs::symlink_metadata(&link) {
        Ok(meta) if meta.file_type().is_symlink() || meta.is_dir() => Ok(()),
        Ok(_) => netsky_core::bail!(
            ".claude exists but is not a directory or symlink: {}",
            link.display()
        ),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => create_claude_symlink(root),
        Err(err) => Err(err.into()),
    }
}

fn create_claude_symlink(root: &Path) -> netsky_core::Result<()> {
    let link = root.join(".claude");
    #[cfg(unix)]
    {
        std::os::unix::fs::symlink(".agents", &link)?;
    }
    #[cfg(windows)]
    {
        std::os::windows::fs::symlink_dir(".agents", &link)?;
    }
    Ok(())
}

fn render_content(rel: &str, content: &str) -> String {
    if rel == ".claude/settings.json" {
        let home = netsky_core::paths::home();
        let home_str = home.to_string_lossy().to_string();
        content.replace("/Users/cody/", &format!("{home_str}/"))
    } else {
        content.to_string()
    }
}

fn atomic_write(target: &Path, content: &str) -> netsky_core::Result<()> {
    use std::time::{SystemTime, UNIX_EPOCH};

    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    let tmp_name = format!(
        "{}.tmp.{}.{}",
        target
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("init-file"),
        std::process::id(),
        nanos
    );
    let tmp = target
        .parent()
        .map(|p| p.join(&tmp_name))
        .unwrap_or_else(|| PathBuf::from(tmp_name));
    fs::write(&tmp, content)?;
    fs::rename(&tmp, target)?;
    Ok(())
}