use super::config::Scope;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Artifact {
pub path: PathBuf,
pub scope: Scope,
pub kind: ArtifactKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArtifactKind {
ShellScript,
Makefile,
Dockerfile,
ShellConfig,
Workflow,
DevContainer,
}
impl std::fmt::Display for ArtifactKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArtifactKind::ShellScript => write!(f, "shell"),
ArtifactKind::Makefile => write!(f, "makefile"),
ArtifactKind::Dockerfile => write!(f, "dockerfile"),
ArtifactKind::ShellConfig => write!(f, "config"),
ArtifactKind::Workflow => write!(f, "workflow"),
ArtifactKind::DevContainer => write!(f, "devcontainer"),
}
}
}
impl Artifact {
pub fn new(path: PathBuf, scope: Scope, kind: ArtifactKind) -> Self {
Self { path, scope, kind }
}
pub fn display_name(&self) -> String {
match self.scope {
Scope::User => {
if let Some(home) = dirs_home() {
if let Ok(rel) = self.path.strip_prefix(&home) {
return format!("~/{}", rel.display());
}
}
self.path.display().to_string()
}
_ => self.path.display().to_string(),
}
}
}
const PROJECT_PATTERNS: &[(&str, ArtifactKind)] = &[
("*.sh", ArtifactKind::ShellScript),
("scripts/**/*.sh", ArtifactKind::ShellScript),
("bin/**/*.sh", ArtifactKind::ShellScript),
("hooks/**/*.sh", ArtifactKind::ShellScript),
("Makefile", ArtifactKind::Makefile),
("makefile", ArtifactKind::Makefile),
("GNUmakefile", ArtifactKind::Makefile),
("*.mk", ArtifactKind::Makefile),
("Dockerfile", ArtifactKind::Dockerfile),
("Dockerfile.*", ArtifactKind::Dockerfile),
("docker-compose.yml", ArtifactKind::Workflow),
("docker-compose.yaml", ArtifactKind::Workflow),
(".github/workflows/*.yml", ArtifactKind::Workflow),
(".github/workflows/*.yaml", ArtifactKind::Workflow),
(
".devcontainer/devcontainer.json",
ArtifactKind::DevContainer,
),
];
const USER_CONFIGS: &[&str] = &[
".zshrc",
".bashrc",
".bash_profile",
".profile",
".zprofile",
".zshenv",
".zlogout",
".bash_logout",
];
const SYSTEM_CONFIGS: &[&str] = &[
"/etc/profile",
"/etc/bash.bashrc",
"/etc/zsh/zshrc",
"/etc/zsh/zshenv",
"/etc/environment",
];
pub fn discover(project_path: &Path, scope: Scope) -> Vec<Artifact> {
match scope {
Scope::Project => discover_project(project_path),
Scope::User => discover_user(),
Scope::System => discover_system(),
}
}
pub fn discover_all(project_path: &Path) -> Vec<Artifact> {
let mut artifacts = discover_project(project_path);
artifacts.extend(discover_user());
artifacts.extend(discover_system());
artifacts
}
fn discover_project(project_path: &Path) -> Vec<Artifact> {
let mut artifacts = Vec::new();
for (pattern, kind) in PROJECT_PATTERNS {
let full_pattern = project_path.join(pattern);
let pattern_str = full_pattern.to_string_lossy();
if let Ok(paths) = glob::glob(&pattern_str) {
for entry in paths.flatten() {
if entry.is_file() {
let rel = entry
.strip_prefix(project_path)
.unwrap_or(&entry)
.to_path_buf();
let artifact = Artifact::new(rel, Scope::Project, *kind);
if !artifacts.iter().any(|a: &Artifact| a.path == artifact.path) {
artifacts.push(artifact);
}
}
}
}
}
artifacts.sort_by(|a, b| a.path.cmp(&b.path));
artifacts
}
fn discover_user() -> Vec<Artifact> {
let mut artifacts = Vec::new();
let home = match dirs_home() {
Some(h) => h,
None => return artifacts,
};
for config_name in USER_CONFIGS {
let path = home.join(config_name);
if path.exists() {
artifacts.push(Artifact::new(path, Scope::User, ArtifactKind::ShellConfig));
}
}
let pzsh_config = home.join(".config/pzsh/config.toml");
if pzsh_config.exists() {
artifacts.push(Artifact::new(
pzsh_config,
Scope::User,
ArtifactKind::ShellConfig,
));
}
artifacts
}
fn discover_system() -> Vec<Artifact> {
let mut artifacts = Vec::new();
for path_str in SYSTEM_CONFIGS {
let path = PathBuf::from(path_str);
if path.exists() {
artifacts.push(Artifact::new(
path,
Scope::System,
ArtifactKind::ShellConfig,
));
}
}
artifacts
}
pub fn classify(path: &Path) -> Option<ArtifactKind> {
let name = path.file_name()?.to_str()?;
let name_lower = name.to_lowercase();
classify_by_name(&name_lower, name, path).or_else(|| classify_by_shebang(path))
}
fn classify_by_name(name_lower: &str, name: &str, path: &Path) -> Option<ArtifactKind> {
if name_lower == "makefile" || name_lower == "gnumakefile" || name_lower.ends_with(".mk") {
return Some(ArtifactKind::Makefile);
}
if name_lower.starts_with("dockerfile") {
return Some(ArtifactKind::Dockerfile);
}
if name_lower == "devcontainer.json" {
return Some(ArtifactKind::DevContainer);
}
if is_workflow_yaml(name_lower, path) {
return Some(ArtifactKind::Workflow);
}
if name_lower.ends_with(".sh") || name_lower.ends_with(".bash") {
return Some(ArtifactKind::ShellScript);
}
const CONFIG_NAMES: &[&str] = &[
".zshrc",
".bashrc",
".bash_profile",
".profile",
".zprofile",
".zshenv",
];
if CONFIG_NAMES.contains(&name) {
return Some(ArtifactKind::ShellConfig);
}
None
}
fn is_workflow_yaml(name_lower: &str, path: &Path) -> bool {
(name_lower.ends_with(".yml") || name_lower.ends_with(".yaml"))
&& (path.to_string_lossy().contains(".github/workflows")
|| name_lower.starts_with("docker-compose"))
}
fn classify_by_shebang(path: &Path) -> Option<ArtifactKind> {
let content = std::fs::read_to_string(path).ok()?;
const SHELL_SHEBANGS: &[&str] = &[
"#!/bin/sh",
"#!/bin/bash",
"#!/usr/bin/env bash",
"#!/usr/bin/env sh",
];
if SHELL_SHEBANGS.iter().any(|s| content.starts_with(s)) {
return Some(ArtifactKind::ShellScript);
}
None
}
fn dirs_home() -> Option<PathBuf> {
std::env::var("HOME").ok().map(PathBuf::from)
}
pub fn detect_pzsh() -> Option<PzshInfo> {
let output = std::process::Command::new("pzsh")
.arg("--version")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let version_str = String::from_utf8_lossy(&output.stdout);
let version = version_str.trim().strip_prefix("pzsh ")?.to_string();
Some(PzshInfo { version })
}
#[derive(Clone, Debug)]
pub struct PzshInfo {
pub version: String,
}