use std::fs;
use std::io;
use std::path::Path;
use crate::error::CliError;
#[cfg(windows)]
const ERROR_PRIVILEGE_NOT_HELD: i32 = 1314;
pub fn preflight() -> Result<(), CliError> {
#[cfg(windows)]
{
use std::os::windows::fs::symlink_dir;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let tmp = std::env::temp_dir();
let pid = std::process::id();
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let src = tmp.join(format!("omne-preflight-src-{pid}-{nanos}-{seq}"));
let dst = tmp.join(format!("omne-preflight-dst-{pid}-{nanos}-{seq}"));
fs::create_dir_all(&src)?;
let result = symlink_dir(&src, &dst);
let _ = fs::remove_dir(&dst);
let _ = fs::remove_dir_all(&src);
match result {
Ok(()) => Ok(()),
Err(e) if e.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD) => {
Err(CliError::SymlinkPrivilegeRequired)
}
Err(e) => Err(CliError::Io(format!("symlink preflight failed: {e}"))),
}
}
#[cfg(not(windows))]
{
Ok(())
}
}
pub fn link_skills(root: &Path) -> Result<(), CliError> {
let omne = root.join(".omne");
let claude_skills = root.join(".claude").join("skills");
fs::create_dir_all(&claude_skills)?;
for layer in ["core", "image"] {
let layer_skills = omne.join(layer).join("skills");
if !layer_skills.is_dir() {
continue;
}
link_layer(&layer_skills, &claude_skills)?;
}
Ok(())
}
fn link_layer(src_skills: &Path, dst_skills: &Path) -> Result<(), CliError> {
for entry in fs::read_dir(src_skills)? {
let entry = entry?;
let src = entry.path();
if !src.is_dir() {
continue;
}
let name = entry.file_name();
let dst = dst_skills.join(&name);
replace_symlink(&src, &dst)?;
}
Ok(())
}
fn replace_symlink(src: &Path, dst: &Path) -> Result<(), CliError> {
if let Ok(meta) = fs::symlink_metadata(dst) {
if meta.file_type().is_symlink() {
remove_symlink(dst)?;
} else {
return Err(CliError::Io(format!(
".claude/skills/{} exists and is not a symlink — refusing to overwrite",
dst.file_name().unwrap_or_default().to_string_lossy()
)));
}
}
symlink_dir(src, dst).map_err(|e| {
#[cfg(windows)]
if e.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD) {
return CliError::SymlinkPrivilegeRequired;
}
CliError::Io(format!(
"failed to symlink {} -> {}: {e}",
dst.display(),
src.display()
))
})
}
#[cfg(windows)]
fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
std::os::windows::fs::symlink_dir(src, dst)
}
#[cfg(unix)]
fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(src, dst)
}
#[cfg(windows)]
fn remove_symlink(dst: &Path) -> io::Result<()> {
fs::remove_dir(dst)
}
#[cfg(unix)]
fn remove_symlink(dst: &Path) -> io::Result<()> {
fs::remove_file(dst)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn make_skill(root: &Path, layer: &str, name: &str) {
let dir = root.join(".omne").join(layer).join("skills").join(name);
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("SKILL.md"),
format!("---\nname: {name}\ndescription: test\n---\n"),
)
.unwrap();
}
#[test]
fn links_kernel_skill() {
let tmp = TempDir::new().unwrap();
make_skill(tmp.path(), "core", "query-installation");
link_skills(tmp.path()).unwrap();
let link = tmp.path().join(".claude/skills/query-installation");
let meta = fs::symlink_metadata(&link).unwrap();
assert!(meta.file_type().is_symlink());
}
#[test]
fn links_distro_skill() {
let tmp = TempDir::new().unwrap();
make_skill(tmp.path(), "image", "assess-domain");
link_skills(tmp.path()).unwrap();
let link = tmp.path().join(".claude/skills/assess-domain");
assert!(fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink());
}
#[test]
fn image_shadows_kernel_on_name_collision() {
let tmp = TempDir::new().unwrap();
make_skill(tmp.path(), "core", "dup");
make_skill(tmp.path(), "image", "dup");
link_skills(tmp.path()).unwrap();
let link = tmp.path().join(".claude/skills/dup");
let target = fs::read_link(&link).unwrap();
assert!(
target.to_string_lossy().contains("image"),
"expected image/ target, got {}",
target.display()
);
}
#[test]
fn links_multiple_skills() {
let tmp = TempDir::new().unwrap();
make_skill(tmp.path(), "image", "a");
make_skill(tmp.path(), "image", "b");
make_skill(tmp.path(), "image", "c");
link_skills(tmp.path()).unwrap();
for name in ["a", "b", "c"] {
let link = tmp.path().join(".claude/skills").join(name);
assert!(fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink());
}
}
#[test]
fn refuses_to_overwrite_real_directory() {
let tmp = TempDir::new().unwrap();
make_skill(tmp.path(), "image", "preexisting");
let real = tmp.path().join(".claude/skills/preexisting");
fs::create_dir_all(&real).unwrap();
fs::write(real.join("SKILL.md"), "user content").unwrap();
let err = link_skills(tmp.path()).unwrap_err();
assert!(
matches!(err, CliError::Io(ref m) if m.contains("refusing to overwrite")),
"expected Io refusal, got {err:?}"
);
}
#[test]
fn idempotent_when_symlink_already_present() {
let tmp = TempDir::new().unwrap();
make_skill(tmp.path(), "image", "repeat");
link_skills(tmp.path()).unwrap();
link_skills(tmp.path()).unwrap();
let link = tmp.path().join(".claude/skills/repeat");
assert!(fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink());
}
#[test]
fn preflight_succeeds_in_test_environment() {
preflight().unwrap();
}
}