moadim 3.5.0

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
//! Data-plane CLI subcommands.
//!
//! These mirror the daemon's `/api/v1` REST routes (and the MCP tools) so most actions are
//! reachable from the command line too — routine flags and per-routine snooze are REST/MCP-only
//! for now (#1516 tracks closing that gap). The global routine lock (`lock`/`unlock`/
//! `lock-status`) has full CLI parity below. Each subcommand is a thin client: it serializes its flags into the
//! same JSON the REST API expects, sends it to the running server over the loopback HTTP client in
//! [`crate::cli`], and prints the server's response. The daemon must already be running
//! (`moadim` / `moadim -i`); when it is not, these commands report that and exit
//! [`crate::cli::EXIT_NOT_RUNNING`].

use clap::{Parser, Subcommand};
use serde_json::{Map, Value};

#[allow(
    clippy::missing_docs_in_private_items,
    reason = "split-out module keeps the file under the linecheck limit"
)]
#[path = "routine_cmd.rs"]
mod routine_cmd;
pub(crate) use routine_cmd::*;
#[allow(
    clippy::missing_docs_in_private_items,
    reason = "split-out module keeps the file under the linecheck limit"
)]
#[path = "dispatch_routine.rs"]
mod dispatch_routine;
pub(crate) use dispatch_routine::*;

/// Top-level parser for the data-plane subcommands, parsed from argv with the leading `moadim`
/// binary name already stripped (`no_binary_name`), so the first token is the subcommand keyword.
#[derive(Parser)]
#[command(
    name = "moadim",
    version,
    no_binary_name = true,
    about = "moadim data commands"
)]
struct DataCli {
    /// The selected data subcommand.
    #[command(subcommand)]
    command: DataCommand,
}

/// The data subcommand groups: routines and agents.
#[derive(Subcommand)]
pub(crate) enum DataCommand {
    /// Manage routines (create/list/get/update/replace/delete/trigger/logs/ical).
    ///
    /// Boxed because `RoutineCmd` (the largest variant by far now that the cron-job
    /// subcommand is gone) would otherwise blow up the size of every `DataCommand`
    /// value, including the trivial `Agents`/`Schedule` ones (`clippy::large_enum_variant`).
    #[command(subcommand, visible_alias = "routine")]
    Routines(Box<RoutineCmd>),
    /// Trigger a routine on its schedule by ID (invoked by the generated crontab line).
    #[command(subcommand, visible_alias = "sched")]
    Schedule(ScheduleCmd),
    /// Enable a routine (set `enabled = true`) by id or slug.
    Enable {
        /// Routine id or slug to enable.
        routine: String,
        /// Emit a machine-readable `{"routine","enabled"}` object instead of a status line.
        #[arg(long)]
        json: bool,
    },
    /// Disable a routine (set `enabled = false`) by id or slug.
    Disable {
        /// Routine id or slug to disable.
        routine: String,
        /// Optional reason to persist into disabled.json.
        #[arg(long)]
        reason: Option<String>,
        /// Emit a machine-readable `{"routine","enabled"}` object instead of a status line.
        #[arg(long)]
        json: bool,
    },
    /// List the available agent registry keys.
    Agents,
    /// Export every tracked routine, agent, and global config file as one portable JSON bundle.
    ///
    /// Runs offline against the config directory — no daemon needed. Gitignored runtime files
    /// (`*.local.*`, `*.pid`, `*.log`, compiled outputs) are excluded, so the bundle is safe to
    /// share or commit.
    Export {
        /// Write the bundle to this file (atomically) instead of stdout.
        #[arg(long)]
        out: Option<std::path::PathBuf>,
    },
    /// Restore a bundle produced by `moadim export` into the config directory.
    ///
    /// Runs offline; existing files are skipped unless `--force` is given. Run `moadim restart`
    /// afterwards so the imported routines are resynced into the scheduler.
    Import {
        /// Path of the bundle file to import.
        file: std::path::PathBuf,
        /// Print what would be created/overwritten/skipped without writing anything.
        #[arg(long)]
        dry_run: bool,
        /// Overwrite files that already exist (default: skip them).
        #[arg(long)]
        force: bool,
    },
    /// Create a lock sentinel, halting all routine scheduling and triggers without touching any
    /// individual routine's `enabled` state (the "pause everything" escape hatch, #683).
    Lock {
        /// Which sentinel to create: `shared` (committed `.lock`, shareable via git) or `local`
        /// (gitignored `.local.lock`, machine-local).
        #[arg(long, default_value = "shared")]
        scope: String,
    },
    /// Remove lock sentinel(s), restoring prior routine scheduling.
    Unlock {
        /// Which sentinel(s) to remove: `shared`, `local`, or `all` (both, the default).
        #[arg(long, default_value = "all")]
        scope: String,
    },
    /// Show the current global lock status (whether the shared and/or local sentinel is present).
    LockStatus,
}

/// Schedule operations driven by the OS crontab, keyed only by ID.
#[derive(Subcommand)]
pub(crate) enum ScheduleCmd {
    /// Run a routine on its schedule by ID.
    ///
    /// This is what the generated crontab line invokes at each fire time. It records a *scheduled*
    /// trigger (not a manual one), so it maps to the routine's `scheduled-trigger` route rather than
    /// the manual `trigger` route.
    Trigger {
        /// UUID of the routine to trigger.
        id: String,
    },
}

/// Build the full create/replace JSON body for a routine, validating optional `repositories` as a
/// JSON array. Returns the serialized body, or an exit code (`2`) when `repositories` is invalid.
#[allow(
    clippy::too_many_arguments,
    reason = "all parameters map to distinct CLI flags with no natural grouping"
)]
#[path = "commands_http.rs"]
mod commands_http;
use commands_http::{insert_json_opt, insert_opt, request, tags_value, to_body};

#[path = "commands_routine_actions.rs"]
mod commands_routine_actions;
use commands_routine_actions::set_routine_enabled;

#[path = "routes/move_routine/cli.rs"]
mod move_routine_cli;
use move_routine_cli::move_routine;

#[cfg(test)]
#[path = "commands_tests.rs"]
mod commands_tests;