frostx 0.1.0

frostx monitors project directories for inactivity. Once a configured inactivity threshold elapses (e.g. "90 days since any file was modified"), frostx executes a pipeline of **actions** - e.g., checking git state, creating archives, uploading backups, deleting local copies. Automating the lifecycle of projects, frostx helps users manage disk space and maintain a clean workspace.
Documentation
//! `frostx run` - execute the inactivity pipeline.

use crate::config::state::ProjectState;
use crate::error::FrostxError;
use crate::pipeline::{self, ActionCallback, ActionStatus, RunOptions};
use crate::scanner;
use std::path::PathBuf;

use super::FrostxOpts;

/// Arguments for the `run` operation.
pub struct RunArgs {
    /// Project directory to run.
    pub path: PathBuf,
    /// Run only this rule number (1-indexed).
    pub rule_filter: Option<usize>,
    /// Run only this named action, bypassing threshold checks.
    pub action_filter: Option<String>,
    /// Re-execute completed mutation actions.
    pub force: bool,
}

/// Execute the inactivity pipeline for a project.
///
/// `on_action` is called after each action completes, enabling real-time
/// streaming of results. Returns `true` if any action failed.
///
/// # Errors
///
/// Returns an error if the config or state cannot be loaded, the scan fails,
/// or an action returns a hard error (distinct from a failed action outcome).
pub fn execute(
    args: &RunArgs,
    opts: &FrostxOpts,
    on_action: &ActionCallback<'_>,
) -> Result<bool, FrostxError> {
    let path = &args.path;
    let config = super::init::load_config(path, opts)?;
    super::init::check_uuid_collision(&config, path, &opts.state_dir)?;

    let mut state = ProjectState::load(&opts.state_dir, config.id)?;
    state.project_path = path.canonicalize().unwrap_or_else(|_| path.clone());

    let scan = scanner::scan(path)?;
    let last_modified = opts
        .pretend_inactive
        .as_ref()
        .map_or(scan.last_modified, |d| d.subtract_from(chrono::Utc::now()));

    let run_opts = RunOptions {
        dry_run: opts.dry_run,
        force: args.force,
        yes: opts.yes,
        rule_filter: args.rule_filter,
        action_filter: args.action_filter.clone(),
    };

    let outcomes = pipeline::run(
        &config,
        &mut state,
        path,
        last_modified,
        &run_opts,
        on_action,
    )?;

    if !opts.dry_run {
        state.last_scan = Some(chrono::Utc::now());
        state.save(&opts.state_dir, config.id)?;
    }

    let had_failures = outcomes.iter().any(|r| {
        r.action_outcomes
            .iter()
            .any(|ao| ao.status == ActionStatus::Failed)
    });

    Ok(had_failures)
}