use crate::config;
use crate::config::state::{list_state_files, ProjectState};
use crate::error::FrostxError;
use crate::output::{
CheckOutput, ProjectAddOutput, ProjectAddSkip, ProjectEntry, ProjectRmOutput,
ProjectsListOutput, FROSTX_VERSION,
};
use crate::pipeline;
use std::path::{Path, PathBuf};
use uuid::Uuid;
use super::FrostxOpts;
pub struct ProjectsAddArgs {
pub paths: Vec<PathBuf>,
pub scan_dir: Option<PathBuf>,
}
pub struct ProjectsRunArgs {
pub force: bool,
pub rule_filter: Option<usize>,
pub action_filter: Option<String>,
}
pub fn list(opts: &FrostxOpts) -> Result<ProjectsListOutput, FrostxError> {
let entries = list_state_files(&opts.state_dir)?;
let mut projects = Vec::new();
for (uuid, _) in entries {
let state = ProjectState::load(&opts.state_dir, uuid)?;
let (name, description) = config::load(&state.project_path, &opts.library_dir)
.ok()
.map_or((None, None), |cfg| (cfg.name, cfg.description));
projects.push(ProjectEntry {
uuid: uuid.to_string(),
path: state.project_path.display().to_string(),
name,
description,
last_scan: state.last_scan.map(|t| t.to_rfc3339()),
});
}
Ok(ProjectsListOutput {
frostx_version: FROSTX_VERSION,
projects,
})
}
#[must_use]
pub fn add(args: &ProjectsAddArgs, opts: &FrostxOpts) -> ProjectAddOutput {
let mut all_paths: Vec<PathBuf> = args.paths.clone();
if let Some(ref dir) = args.scan_dir {
all_paths.extend(super::scan::find_projects(dir, None));
}
let mut added = Vec::new();
let mut skipped = Vec::new();
for path in &all_paths {
match add_single(path, opts) {
Ok(entry) => added.push(entry),
Err(e) => skipped.push(ProjectAddSkip {
path: path.display().to_string(),
reason: e.to_string(),
}),
}
}
ProjectAddOutput {
frostx_version: FROSTX_VERSION,
added,
skipped,
}
}
fn add_single(path: &Path, opts: &FrostxOpts) -> Result<ProjectEntry, FrostxError> {
let cfg = config::load(path, &opts.library_dir)?;
let canonical = path.canonicalize()?;
let mut state = ProjectState::load(&opts.state_dir, cfg.id)?;
if !state.project_path.as_os_str().is_empty() && state.project_path != canonical {
return Err(FrostxError::UuidCollision {
current: canonical,
recorded: state.project_path.clone(),
});
}
let last_scan = state.last_scan.map(|t| t.to_rfc3339());
state.project_path.clone_from(&canonical);
state.save(&opts.state_dir, cfg.id)?;
Ok(ProjectEntry {
uuid: cfg.id.to_string(),
path: canonical.display().to_string(),
name: cfg.name.clone(),
description: cfg.description.clone(),
last_scan,
})
}
pub fn rm(path: &Path, opts: &FrostxOpts) -> Result<ProjectRmOutput, FrostxError> {
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let uuid = if let Ok(cfg) = config::load(path, &opts.library_dir) {
cfg.id
} else {
find_uuid_by_path(&canonical, opts)?
};
ProjectState::delete(&opts.state_dir, uuid)?;
Ok(ProjectRmOutput {
frostx_version: FROSTX_VERSION,
uuid: uuid.to_string(),
path: canonical.display().to_string(),
})
}
fn find_uuid_by_path(canonical: &Path, opts: &FrostxOpts) -> Result<Uuid, FrostxError> {
let entries = list_state_files(&opts.state_dir)?;
for (uuid, _) in entries {
let state = ProjectState::load(&opts.state_dir, uuid)?;
if state.project_path == canonical {
return Ok(uuid);
}
}
Err(FrostxError::NotInitialized(canonical.to_path_buf()))
}
#[must_use]
pub fn check_all(opts: &FrostxOpts) -> (Vec<CheckOutput>, Vec<(PathBuf, FrostxError)>) {
let entries = match list_state_files(&opts.state_dir) {
Ok(e) => e,
Err(e) => return (vec![], vec![(PathBuf::new(), e)]),
};
let mut results = Vec::new();
let mut errors = Vec::new();
for (uuid, _) in entries {
let Some(path) = tracked_path(&opts.state_dir, uuid) else {
continue;
};
match super::check::gather(&path, opts) {
Ok(out) => results.push(out),
Err(e) => errors.push((path, e)),
}
}
(results, errors)
}
#[allow(clippy::type_complexity)]
pub fn run_all(
args: &ProjectsRunArgs,
opts: &FrostxOpts,
on_action: &dyn Fn(&Path, usize, Option<&str>, &pipeline::ActionOutcome),
) -> (bool, Vec<(PathBuf, FrostxError)>) {
let entries = match list_state_files(&opts.state_dir) {
Ok(e) => e,
Err(e) => return (false, vec![(PathBuf::new(), e)]),
};
let mut had_failures = false;
let mut errors = Vec::new();
for (uuid, _) in entries {
let Some(path) = tracked_path(&opts.state_dir, uuid) else {
continue;
};
let run_args = super::run::RunArgs {
path: path.clone(),
rule_filter: args.rule_filter,
action_filter: args.action_filter.clone(),
force: args.force,
};
let path_ref = path.clone();
let cb: pipeline::ActionCallback<'_> =
Box::new(move |rule_idx, rule_name, ao| on_action(&path_ref, rule_idx, rule_name, ao));
match super::run::execute(&run_args, opts, &cb) {
Ok(failed) => {
if failed {
had_failures = true;
}
}
Err(e) => errors.push((path, e)),
}
}
(had_failures, errors)
}
fn tracked_path(state_dir: &Path, uuid: Uuid) -> Option<PathBuf> {
let state = ProjectState::load(state_dir, uuid).ok()?;
if state.project_path.as_os_str().is_empty() || !state.project_path.exists() {
return None;
}
Some(state.project_path)
}