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, symlink_file};
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 nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir_src = tmp.join(format!("omne-preflight-dsrc-{pid}-{nanos}-{seq}"));
let dir_dst = tmp.join(format!("omne-preflight-ddst-{pid}-{nanos}-{seq}"));
fs::create_dir_all(&dir_src)?;
let dir_result = symlink_dir(&dir_src, &dir_dst);
let _ = fs::remove_dir(&dir_dst);
let _ = fs::remove_dir_all(&dir_src);
match dir_result {
Ok(()) => {}
Err(e) if e.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD) => {
return Err(CliError::SymlinkPrivilegeRequired);
}
Err(e) => return Err(CliError::Io(format!("dir symlink preflight failed: {e}"))),
}
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let file_src = tmp.join(format!("omne-preflight-fsrc-{pid}-{nanos}-{seq}.md"));
let file_dst = tmp.join(format!("omne-preflight-fdst-{pid}-{nanos}-{seq}.md"));
fs::write(&file_src, b"preflight")?;
let file_result = symlink_file(&file_src, &file_dst);
let _ = fs::remove_file(&file_dst);
let _ = fs::remove_file(&file_src);
match file_result {
Ok(()) => Ok(()),
Err(e) if e.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD) => {
Err(CliError::SymlinkPrivilegeRequired)
}
Err(e) => Err(CliError::Io(format!("file symlink preflight failed: {e}"))),
}
}
#[cfg(not(windows))]
{
Ok(())
}
}
pub fn link_layers(root: &Path) -> Result<(), CliError> {
let omne = root.join(".omne");
let claude_skills = root.join(".claude").join("skills");
let claude_commands = root.join(".claude").join("commands");
fs::create_dir_all(&claude_skills)?;
fs::create_dir_all(&claude_commands)?;
for layer in ["core", "dist"] {
let layer_skills = omne.join(layer).join("skills");
if layer_skills.is_dir() {
link_skills_layer(&layer_skills, &claude_skills)?;
}
let layer_cmds = omne.join(layer).join("cmds");
if layer_cmds.is_dir() {
link_cmds_layer(&layer_cmds, &claude_commands)?;
}
}
Ok(())
}
fn link_skills_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, LinkKind::Dir)?;
}
Ok(())
}
fn link_cmds_layer(src_cmds: &Path, dst_commands: &Path) -> Result<(), CliError> {
for entry in fs::read_dir(src_cmds)? {
let entry = entry?;
let src = entry.path();
let file_type = entry.file_type()?;
if !file_type.is_file() {
continue;
}
if src.extension().is_none_or(|ext| ext != "md") {
continue;
}
let name = entry.file_name();
let dst = dst_commands.join(&name);
replace_symlink(&src, &dst, LinkKind::File)?;
}
Ok(())
}
#[derive(Clone, Copy)]
enum LinkKind {
Dir,
File,
}
fn replace_symlink(src: &Path, dst: &Path, kind: LinkKind) -> Result<(), CliError> {
if let Ok(meta) = fs::symlink_metadata(dst) {
if meta.file_type().is_symlink() {
remove_symlink(dst, kind)?;
} else {
let namespace = match kind {
LinkKind::Dir => "skills",
LinkKind::File => "commands",
};
return Err(CliError::Io(format!(
".claude/{namespace}/{} exists and is not a symlink — refusing to overwrite",
dst.file_name().unwrap_or_default().to_string_lossy()
)));
}
}
let result = match kind {
LinkKind::Dir => symlink_dir(src, dst),
LinkKind::File => symlink_file(src, dst),
};
result.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 symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
std::os::windows::fs::symlink_file(src, dst)
}
#[cfg(unix)]
fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(src, dst)
}
fn remove_symlink(dst: &Path, kind: LinkKind) -> io::Result<()> {
#[cfg(windows)]
{
match kind {
LinkKind::Dir => fs::remove_dir(dst),
LinkKind::File => fs::remove_file(dst),
}
}
#[cfg(unix)]
{
let _ = kind;
fs::remove_file(dst)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn make_dir_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();
}
fn make_cmd(root: &Path, layer: &str, name: &str) {
let dir = root.join(".omne").join(layer).join("cmds");
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join(format!("{name}.md")),
format!("---\nname: {name}\n---\n# {name}\n"),
)
.unwrap();
}
#[test]
fn links_kernel_skill() {
let tmp = TempDir::new().unwrap();
make_dir_skill(tmp.path(), "core", "query-installation");
link_layers(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_dir_skill(tmp.path(), "dist", "assess-domain");
link_layers(tmp.path()).unwrap();
let link = tmp.path().join(".claude/skills/assess-domain");
assert!(fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink());
}
#[test]
fn dist_shadows_kernel_skill_on_name_collision() {
let tmp = TempDir::new().unwrap();
make_dir_skill(tmp.path(), "core", "dup");
make_dir_skill(tmp.path(), "dist", "dup");
link_layers(tmp.path()).unwrap();
let link = tmp.path().join(".claude/skills/dup");
let target = fs::read_link(&link).unwrap();
assert!(
target.to_string_lossy().contains("dist"),
"expected dist/ target, got {}",
target.display()
);
}
#[test]
fn links_multiple_skills() {
let tmp = TempDir::new().unwrap();
make_dir_skill(tmp.path(), "dist", "a");
make_dir_skill(tmp.path(), "dist", "b");
make_dir_skill(tmp.path(), "dist", "c");
link_layers(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_skill_directory() {
let tmp = TempDir::new().unwrap();
make_dir_skill(tmp.path(), "dist", "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_layers(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_skills() {
let tmp = TempDir::new().unwrap();
make_dir_skill(tmp.path(), "dist", "repeat");
link_layers(tmp.path()).unwrap();
link_layers(tmp.path()).unwrap();
let link = tmp.path().join(".claude/skills/repeat");
assert!(fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink());
}
#[test]
fn links_kernel_cmd() {
let tmp = TempDir::new().unwrap();
make_cmd(tmp.path(), "core", "plan");
link_layers(tmp.path()).unwrap();
let link = tmp.path().join(".claude/commands/plan.md");
let meta = fs::symlink_metadata(&link).unwrap();
assert!(meta.file_type().is_symlink());
}
#[test]
fn links_distro_cmd() {
let tmp = TempDir::new().unwrap();
make_cmd(tmp.path(), "dist", "implement");
link_layers(tmp.path()).unwrap();
let link = tmp.path().join(".claude/commands/implement.md");
assert!(fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink());
}
#[test]
fn dist_shadows_kernel_cmd_on_name_collision() {
let tmp = TempDir::new().unwrap();
make_cmd(tmp.path(), "core", "dup");
make_cmd(tmp.path(), "dist", "dup");
link_layers(tmp.path()).unwrap();
let link = tmp.path().join(".claude/commands/dup.md");
let target = fs::read_link(&link).unwrap();
assert!(
target.to_string_lossy().contains("dist"),
"expected dist/ target, got {}",
target.display()
);
}
#[test]
fn mixed_layer_both_namespaces() {
let tmp = TempDir::new().unwrap();
make_dir_skill(tmp.path(), "dist", "my-skill");
make_cmd(tmp.path(), "dist", "my-cmd");
link_layers(tmp.path()).unwrap();
assert!(
fs::symlink_metadata(tmp.path().join(".claude/skills/my-skill"))
.unwrap()
.file_type()
.is_symlink()
);
assert!(
fs::symlink_metadata(tmp.path().join(".claude/commands/my-cmd.md"))
.unwrap()
.file_type()
.is_symlink()
);
}
#[test]
fn non_md_file_in_cmds_is_skipped() {
let tmp = TempDir::new().unwrap();
let cmds_dir = tmp.path().join(".omne/dist/cmds");
fs::create_dir_all(&cmds_dir).unwrap();
fs::write(cmds_dir.join("notes.txt"), "not a cmd").unwrap();
fs::write(cmds_dir.join("plan.bak"), "backup").unwrap();
make_cmd(tmp.path(), "dist", "real");
link_layers(tmp.path()).unwrap();
assert!(fs::symlink_metadata(tmp.path().join(".claude/commands/real.md")).is_ok());
assert!(fs::symlink_metadata(tmp.path().join(".claude/commands/notes.txt")).is_err());
assert!(fs::symlink_metadata(tmp.path().join(".claude/commands/plan.bak")).is_err());
}
#[test]
fn subdirectory_in_cmds_is_skipped() {
let tmp = TempDir::new().unwrap();
let cmds_dir = tmp.path().join(".omne/dist/cmds");
fs::create_dir_all(cmds_dir.join("nested")).unwrap();
make_cmd(tmp.path(), "dist", "real");
link_layers(tmp.path()).unwrap();
assert!(fs::symlink_metadata(tmp.path().join(".claude/commands/real.md")).is_ok());
assert!(fs::symlink_metadata(tmp.path().join(".claude/commands/nested")).is_err());
}
#[test]
fn refuses_to_overwrite_real_command_file() {
let tmp = TempDir::new().unwrap();
make_cmd(tmp.path(), "dist", "plan");
let real_dir = tmp.path().join(".claude/commands");
fs::create_dir_all(&real_dir).unwrap();
fs::write(real_dir.join("plan.md"), "user content").unwrap();
let err = link_layers(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_cmds() {
let tmp = TempDir::new().unwrap();
make_cmd(tmp.path(), "dist", "plan");
link_layers(tmp.path()).unwrap();
link_layers(tmp.path()).unwrap();
let link = tmp.path().join(".claude/commands/plan.md");
assert!(fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink());
}
#[test]
fn preflight_succeeds_in_test_environment() {
preflight().unwrap();
}
}