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(())
}