mars-agents 0.4.3

Agent package manager for .agents/ directories
Documentation
//! Shared target-name validation for `link`, `unlink`, and `init` commands.

use crate::error::{ConfigError, MarsError};

/// Validate that a target is a simple directory name, not a path.
///
/// Returns an error if the target contains path separators, is empty, or is a
/// dot-only component. Used by `mars init` and `load_manifest` target validation.
pub fn validate_target(target: &str) -> Result<(), MarsError> {
    if target.contains('/') || target.contains('\\') {
        return Err(MarsError::Config(ConfigError::Invalid {
            message: format!(
                "`{target}` looks like a path — TARGET should be a directory name \
                 like `.claude` or `.codex`. Use `--root` to specify project root."
            ),
        }));
    }
    if target == "." || target == ".." || target.is_empty() {
        return Err(MarsError::Config(ConfigError::Invalid {
            message: format!(
                "`{target}` is not a valid target name — use a directory name like `.claude` or `.codex`."
            ),
        }));
    }
    Ok(())
}

/// Normalize and validate a target directory name.
///
/// Strips trailing slashes, rejects paths (containing `/` or `\`),
/// and rejects empty/dot names.
pub fn normalize_target_name(target: &str) -> Result<String, MarsError> {
    let normalized = target.trim_end_matches('/').trim_end_matches('\\');
    if normalized.contains('/') || normalized.contains('\\') {
        return Err(MarsError::Link {
            target: target.to_string(),
            message: "target must be a directory name, not a path".to_string(),
        });
    }
    if normalized.is_empty() || normalized == "." || normalized == ".." {
        return Err(MarsError::Link {
            target: target.to_string(),
            message: "invalid target name".to_string(),
        });
    }
    Ok(normalized.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn validate_target_accepts_simple_names() {
        assert!(validate_target(".agents").is_ok());
        assert!(validate_target(".claude").is_ok());
        assert!(validate_target("my-agents").is_ok());
    }

    #[test]
    fn validate_target_rejects_paths() {
        assert!(validate_target("./foo").is_err());
        assert!(validate_target("foo/bar").is_err());
        assert!(validate_target("/absolute/path").is_err());
    }

    #[test]
    fn validate_target_rejects_dots() {
        assert!(validate_target(".").is_err());
        assert!(validate_target("..").is_err());
    }

    #[test]
    fn validate_target_rejects_empty() {
        assert!(validate_target("").is_err());
    }

    #[test]
    fn normalize_strips_trailing_slash() {
        assert_eq!(normalize_target_name(".claude/").unwrap(), ".claude");
    }

    #[test]
    fn normalize_rejects_path() {
        assert!(normalize_target_name("foo/bar").is_err());
    }

    #[test]
    fn normalize_rejects_empty() {
        assert!(normalize_target_name("").is_err());
    }

    #[test]
    fn normalize_rejects_dots() {
        assert!(normalize_target_name(".").is_err());
        assert!(normalize_target_name("..").is_err());
    }
}