unifier-cli 0.5.0

Filesystem postbox for inter-process communication via a Unix tree
Documentation
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "unifier")]
#[command(
    about = "Filesystem postbox: programs communicate by writing values to files in a Unix tree",
    version
)]
pub struct Cli {
    /// Override state root (default: $UNIFIER_HOME or ~/.local/unifier)
    #[arg(long, global = true, env = "UNIFIER_HOME")]
    pub home: Option<std::path::PathBuf>,

    /// Scope operations to chroots/<name>/ (only that subtree is visible)
    #[arg(long, global = true, env = "UNIFIER_CHROOT")]
    pub chroot: Option<String>,

    /// Force direct filesystem access even when a hot daemon is running
    #[arg(long, global = true)]
    pub no_daemon: bool,

    /// Prefix keys with this namespace (overrides a bound process namespace)
    #[arg(long, global = true, env = "UNIFIER_NAMESPACE")]
    pub namespace: Option<String>,

    #[command(subcommand)]
    pub cmd: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Write a persistent key value under keys/
    Put {
        /// Key path (e.g. myapp/config/theme)
        key: String,
        /// Value to store
        value: String,
    },
    /// Read a key value
    Get { key: String },
    /// Delete a key
    Del { key: String },
    /// Drop a message into a recipient mailbox
    Send {
        /// Sender agent name (default: unifier)
        #[arg(long)]
        from: Option<String>,
        recipient: String,
        message: String,
    },
    /// Schedule a cron message (directory name: min_hour_dom_mon_dow, use * for any)
    Cron { schedule: String, message: String },
    /// Collect pending messages for a mailbox recipient
    Poll {
        recipient: String,
        /// Remove messages after printing
        #[arg(long)]
        ack: bool,
    },
    /// Collect cron messages whose schedule matches the current time
    PollCron {
        /// Remove messages after printing
        #[arg(long)]
        ack: bool,
    },
    /// List message files in a subtree (e.g. cron/0_0_*_*_*)
    List { path: String },
    /// Remove a message by UUID or relative path under the state root
    Ack { id_or_path: String },
    /// Print the effective state root path
    Root,
    /// Post a named JSON event for agent pickup
    Event {
        /// JSON payload (must include a name field or be a JSON object)
        payload: String,
        /// Seconds until the event is deleted (default 86400 / 24h). 0 = never expire.
        /// Overrides payload `ttl` / `expires_at` when set.
        #[arg(long)]
        ttl: Option<u64>,
    },
    /// Send a structured JSON message to an agent mailbox
    Message {
        #[arg(long)]
        from: String,
        recipient: String,
        payload: String,
    },
    /// ACID tick staging (read previous tick, write current, commit at end)
    #[command(subcommand)]
    Tick(TickCommands),
    /// Hot in-memory daemon (persists state in RAM, flushes to disk on demand)
    #[command(subcommand)]
    Daemon(DaemonCommands),
    /// Manage isolated chroot subtrees
    #[command(subcommand)]
    Chroot(ChrootCommands),
    /// Bind a key prefix to the calling process (and descendants)
    #[command(subcommand)]
    Namespace(NamespaceCommands),
    /// Managed SQLite databases under sqlite/
    #[command(subcommand)]
    Sql(SqlCommands),
    /// RDF-style triples in the reserved `triples` database
    #[command(subcommand)]
    Triple(TripleCommands),
    /// Span-tree logging for Jan cron scripts and agent pipelines
    #[command(subcommand)]
    Log(LogCommands),
    /// Pipe a temp file into the daemon HTTP server and print its URL
    Serve {
        /// Published name (default: random UUID). Path segment only: [A-Za-z0-9._-]
        name: Option<String>,
        /// Read body from this file instead of stdin
        #[arg(long)]
        file: Option<std::path::PathBuf>,
        /// Content-Type header (default: text/html; charset=utf-8)
        #[arg(long = "content-type", short = 't')]
        content_type: Option<String>,
        /// Auto-delete after this many seconds
        #[arg(long)]
        ttl: Option<u64>,
        /// Wrap body in a Unifier HTML chrome (for Jan report export)
        #[arg(long)]
        wrap: bool,
        /// Page title used with --wrap
        #[arg(long, default_value = "Unifier report")]
        title: String,
    },
    /// Manage temp files on the daemon web server
    #[command(subcommand)]
    Web(WebCommands),
}

#[derive(Subcommand)]
pub enum TickCommands {
    /// Begin a new tick (reads frozen snapshot of previous committed state)
    Start {
        #[arg(default_value = "default")]
        label: String,
    },
    /// Commit current tick to disk with versioning
    End,
    /// Show committed tick, active tick, queue depth, and locks
    Status,
    /// Set the active tick's phase name (broadcast on events.sock)
    Phase { phase: String },
    /// Lock a key for the duration of the active tick
    Lock { key: String },
    /// Release a tick lock
    Unlock { key: String },
}

#[derive(Subcommand)]
pub enum DaemonCommands {
    /// Start the hot daemon in the background
    Start,
    /// Run the daemon in the foreground (internal / debugging)
    Run,
    /// Stop the daemon (flushes dirty state first)
    Stop,
    /// Show whether the daemon is running
    Status,
    /// Flush in-memory dirty state to disk
    Flush,
    /// Print mailbox/event wakeup notices from the event socket (for Jan cron)
    Watch,
    /// Kill orphan daemons whose `--home` directory no longer exists
    Gc {
        /// Print victims without signaling them
        #[arg(long)]
        dry_run: bool,
    },
}

#[derive(Subcommand)]
pub enum ChrootCommands {
    /// Create chroots/<name>/ with keys/, mailbox/, and cron/
    Init { name: String },
    /// List chroot names under the global store
    List,
}

#[derive(Subcommand)]
pub enum NamespaceCommands {
    /// Bind a key prefix until this process exits or a new namespace is set
    Set { name: String },
    /// Print the effective namespace for this process
    #[command(visible_alias = "show")]
    Get,
    /// Remove this process's namespace binding
    Clear,
}

#[derive(Subcommand)]
pub enum SqlCommands {
    /// List managed database names under sqlite/
    List,
    /// Create an empty managed database (triples also gets its schema)
    Create {
        /// Database name (stored as sqlite/<name>.sqlite)
        name: String,
    },
    /// List tables (and columns) in a database
    Tables {
        database: String,
        /// Include column names and declared types
        #[arg(long)]
        schema: bool,
    },
    /// Run SQL against a managed database (creates the file if missing)
    Exec {
        database: String,
        /// SQL statement(s)
        sql: String,
    },
}

#[derive(Subcommand)]
pub enum TripleCommands {
    /// Insert a subject/predicate/object triple (duplicates ignored)
    Add {
        subject: String,
        predicate: String,
        object: String,
    },
    /// Query triples with optional filters
    Query {
        #[arg(long, short = 's')]
        subject: Option<String>,
        #[arg(long, short = 'p')]
        predicate: Option<String>,
        #[arg(long, short = 'o')]
        object: Option<String>,
    },
}

#[derive(Subcommand)]
pub enum LogCommands {
    /// Start a new span and print its UUID
    Start {
        /// Human-readable span name
        name: String,
        /// Parent span UUID (nest this span under an existing one)
        #[arg(long)]
        parent: Option<String>,
        /// Initial key=value fields (repeatable)
        #[arg(long = "field", short = 'f')]
        fields: Vec<String>,
    },
    /// End a span (record ended_at)
    End {
        /// Span UUID
        id: String,
    },
    /// Append a log event to a span
    Event {
        /// Span UUID
        span: String,
        /// Log message
        message: String,
        /// key=value fields to attach (repeatable)
        #[arg(long = "field", short = 'f')]
        fields: Vec<String>,
    },
    /// Set key=value fields on an existing span
    Field {
        /// Span UUID
        span: String,
        /// key=value pairs (repeatable)
        #[arg(required = true)]
        fields: Vec<String>,
    },
    /// Print the span tree (all spans, or rooted at a given span)
    Tree {
        /// Root span UUID (optional; show all roots if omitted)
        #[arg(long)]
        span: Option<String>,
    },
    /// List all span UUIDs with name and status
    List,
}

#[derive(Subcommand)]
pub enum WebCommands {
    /// List published temp files (name, type, bytes, url)
    List,
    /// Print the URL for a published name
    Url { name: String },
    /// Print the HTTP URL for a persistent key (`/keys/<key>`)
    KeyUrl { key: String },
    /// Remove a published temp file
    Rm { name: String },
    /// Print the daemon HTTP base URL
    Status,
}