memstead-cli 0.7.0

Command-line interface for Memstead — query and mutate typed entity graphs from the shell. Default build produces the full `memstead` binary (multi-mem, git-backed); `--no-default-features` builds the lean folder-only surface.
Documentation
//! `memstead` — command-line interface for the Memstead graph engine.
//!
//! Subcommands mirror the MCP tool surface. Output defaults to markdown
//! (same text MCP returns); `--json` emits structured content matching
//! the MCP `structured_content` payload.
//!
//! One crate, two build configs. The default (`mem-repo`) build is
//! the full `memstead`: every subcommand, including the multi-mem /
//! mem-repo lifecycle (`mem`, `mem-repo`, `workspace`, `install`,
//! `batch-update`, `recover`). `--no-default-features` drops the
//! git-branch backend and those subcommands — a CI / wasm-adjacent
//! config, not shipped.

use std::process::ExitCode;

use clap::Parser;

use memstead_cli::CliError;
use memstead_cli::cli::{Cli, Command};
use memstead_cli::commands;
use memstead_cli::output::{ExitKind, print_cli_error};
use memstead_cli::setup;

fn main() -> ExitCode {
    let cli = Cli::parse();
    let json_mode = cli.json;
    let verb = cli.command.verb();

    match run(cli) {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            let cli_err = e.downcast_ref::<CliError>();
            let kind = cli_err.map(|c| c.kind).unwrap_or(ExitKind::Generic);
            let code = cli_err.map(|c| c.effective_code()).unwrap_or("INTERNAL");
            let details = cli_err.and_then(|c| c.details.as_ref());
            // Friction ledger (best-effort, before rendering): every
            // typed refusal this surface returns appends one entry to
            // the workspace-local ledger, every value drawn from a
            // closed engine-defined vocabulary (the module's privacy
            // hard line). Unresolvable workspace (pre-boot refusals
            // outside any workspace) degrades to not-recording; the
            // refusal below is unaffected either way.
            if let Ok(cwd) = std::env::current_dir()
                && let Some(root) = setup::find_workspace_root(&cwd)
            {
                // The reason rides only when `closed_reason` matches
                // the details' discriminator against its per-code
                // closed vocabulary — free-form details never land.
                memstead_base::friction::FrictionLedger::for_workspace(&root).record(
                    "cli",
                    verb,
                    code,
                    memstead_base::friction::closed_reason(code, details),
                );
            }
            // `{e:#}` renders the whole anyhow chain (`context: cause`),
            // not just the outermost context line — an engine refusal
            // like `SchemaNotFound` stays visible through a
            // `.with_context(...)` wrapper instead of degrading to a
            // bare "init filesystem-mem engine at <path>".
            print_cli_error(code, &format!("{e:#}"), kind, json_mode, details);
            ExitCode::from(kind as u8)
        }
    }
}

fn run(cli: Cli) -> anyhow::Result<()> {
    // Workspace override — `--workspace` beats `MEMSTEAD_WORKSPACE`,
    // and either beats the upward walk. Applied by verifying the
    // marker and then chdir-ing (git `-C` semantics), so every
    // subcommand's resolution — the walk in `setup.rs`, the
    // per-command walkers — honours the override through one
    // mechanism. The marker check is NOT weakened: an override
    // without `.memstead/workspace.toml` refuses here, naming the
    // tried path, and never falls back to the walk (a typo must not
    // silently target the wrong graph).
    let override_path = cli
        .workspace
        .clone()
        .or_else(|| std::env::var_os("MEMSTEAD_WORKSPACE").map(std::path::PathBuf::from));
    if let Some(root) = override_path {
        if !memstead_base::is_workspace_root(&root) {
            return Err(setup::workspace_not_initialised_error(&format!(
                "workspace override `{}` (from --workspace or MEMSTEAD_WORKSPACE) does not \
                 carry `.memstead/workspace.toml` — refusing rather than falling back to \
                 the directory walk",
                root.display()
            ))
            .into());
        }
        std::env::set_current_dir(&root).map_err(|e| {
            CliError::new(
                ExitKind::Generic,
                "INVALID_INPUT",
                format!("cannot enter workspace override {}: {e}", root.display()),
            )
        })?;
    }

    let role = match cli.role.as_deref() {
        None => memstead_base::vcs::Role::Unspecified,
        Some(s) => match memstead_base::vcs::Role::from_wire(s) {
            Some(r) => r,
            None => {
                return Err(CliError::new(
                    ExitKind::Validation,
                    "INVALID_ROLE",
                    format!(
                        "unknown role {s:?} — declarable roles: {}",
                        memstead_base::vcs::Role::DECLARABLE.join(", ")
                    ),
                )
                .into());
            }
        },
    };
    let ctx = setup::CliContext {
        json: cli.json,
        quiet: cli.quiet,
        role,
    };

    match cli.command {
        Command::Status => commands::status::run(&ctx),
        Command::Entity(args) => commands::entity::run(&ctx, args),
        Command::Relations(args) => commands::relations::run(&ctx, args),
        Command::Search(args) => commands::search::run(&ctx, args),
        Command::List(args) => commands::list::run(&ctx, args),
        Command::Context(args) => commands::context::run(&ctx, args),
        Command::Overview(args) => commands::overview::run(&ctx, args),
        Command::Type(args) => commands::type_cmd::run(&ctx, args),
        Command::Health(args) => commands::health::run(&ctx, args),
        Command::Due(args) => commands::due::run(&ctx, args),
        Command::Export(args) => commands::export::run(&ctx, args),
        Command::Init(args) => commands::init::run(&ctx, args),
        Command::Quickstart(args) => commands::quickstart::run(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::Install(args) => commands::install::run(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::Uninstall(args) => commands::uninstall::run(&ctx, args),
        Command::VerifyAnchors(args) => commands::verify_anchors::run(&ctx, args),
        Command::Link(args) => commands::link::run(&ctx, args),
        Command::Publish(args) => commands::publish::run(&ctx, args),
        Command::Unpublish(args) => commands::unpublish::run(&ctx, args),
        Command::Domain { action } => commands::domain::run(&ctx, action),
        Command::Admin { action } => match action {
            commands::admin::AdminAction::Takedown(args) => {
                commands::admin::run_takedown(&ctx, args)
            }
            commands::admin::AdminAction::Denylist(args) => {
                commands::admin::run_denylist(&ctx, args)
            }
        },
        Command::Login(args) => commands::login::run(&ctx, args),
        Command::Logout(args) => commands::logout::run(&ctx, args),
        Command::Create(args) => commands::create::run(&ctx, args),
        Command::Update(args) => commands::update::run(&ctx, args),
        Command::Relate(args) => commands::relate::run(&ctx, args),
        Command::Delete(args) => commands::delete::run(&ctx, args),
        Command::Rename(args) => commands::rename::run(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::BatchUpdate(args) => commands::batch_update::run(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::BatchCreate(args) => commands::batch_create::run(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::BatchRelate(args) => commands::batch_relate::run(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::Recover(args) => commands::recover::run(&ctx, args),
        Command::Anchors(args) => commands::anchors::run(&ctx, args),
        Command::Changes(args) => commands::changes::run(&ctx, args),
        Command::Check(args) => commands::check::run(&ctx, args),
        Command::ReviewMark(args) => commands::review_mark::run(&ctx, args),
        Command::Reload(args) => commands::reload::run(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::Fetch(args) => commands::transport::run_fetch(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::Pull(args) => commands::transport::run_pull(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::Push(args) => commands::transport::run_push(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::BranchReset(args) => commands::branch_reset::run(&ctx, args),
        #[cfg(feature = "mem-repo")]
        Command::Mem { action } => match action {
            commands::mem::MemAction::Init(args) => commands::mem::run(&ctx, args),
            commands::mem::MemAction::Unregister(args) => commands::mem::run_unregister(&ctx, args),
            commands::mem::MemAction::Delete(args) => commands::mem::run_delete(&ctx, args),
            commands::mem::MemAction::Rename(args) => commands::mem::run_rename(&ctx, args),
            commands::mem::MemAction::SetVersion(args) => {
                commands::mem::run_set_version(&ctx, args)
            }
            commands::mem::MemAction::SetSchema(args) => commands::mem::run_set_schema(&ctx, args),
            commands::mem::MemAction::SetDescription(args) => {
                commands::mem::run_set_description(&ctx, args)
            }
            commands::mem::MemAction::SetTitle(args) => commands::mem::run_set_title(&ctx, args),
            commands::mem::MemAction::SetSubject(args) => {
                commands::mem::run_set_subject(&ctx, args)
            }
            commands::mem::MemAction::SetInternal(args) => {
                commands::mem::run_set_internal(&ctx, args)
            }
            commands::mem::MemAction::SetSyncState(args) => {
                commands::mem::run_set_sync_state(&ctx, args)
            }
            commands::mem::MemAction::List(args) => commands::mem::run_list(&ctx, args),
        },
        #[cfg(feature = "mem-repo")]
        Command::MemRepo { action } => commands::mem_repo::run(&ctx, action),
        #[cfg(feature = "mem-repo")]
        Command::Workspace { action } => commands::workspace::run(&ctx, action),
        Command::Schema(args) => commands::schema::run(&ctx, args),
        Command::Projection(args) => commands::projection::run(&ctx, args),
    }
}