Skip to main content

memstead_cli/
cli.rs

1//! Clap derive for the `memstead` binary, lifted out of `main.rs` so
2//! the xtask doc generator can call `Cli::command()` against the same
3//! tree the binary exposes — no duplicated declarations, no drift.
4//!
5//! One crate, two build configs: the default (`mem-repo`) build
6//! exposes the full command set including the multi-mem / mem-repo
7//! lifecycle subcommands; `--no-default-features` drops those, leaving
8//! the engine-agnostic surface.
9
10use clap::{Parser, Subcommand};
11
12use crate::commands;
13
14/// Top-level `--help` epilog describing the exit-code posture. The
15/// taxonomy is intentionally coarse — success vs failure — because
16/// agents read JSON, not exit codes, and shell scripts can lift the
17/// granular `code` from `--json | jq .code`.
18pub const EXIT_CODES_HELP: &str = "\
19Exit codes:
20  0  success
21  1  generic failure (catch-all for non-classified errors)
22  2  usage error (clap argument-parse failure — unknown flag, bad value)
23  3  not found (entity / mem / resource missing)
24  4  hash mismatch (optimistic-locking failure on a mutation)
25  5  validation / schema / policy refusal
26
27  For programmatic branching, prefer `--json` over the exit code:
28    memstead <subcommand> ... --json | jq -r .code
29  The JSON envelope's `code` field carries the typed token
30  (e.g. INVALID_TITLE, HAS_INCOMING_REFS, CROSS_MEM_LINK_NOT_ALLOWED)
31  with structured recovery details under `.details`.";
32
33/// Query and mutate Memstead knowledge graphs from the shell.
34#[derive(Parser, Debug)]
35#[command(name = "memstead", version, about, long_about = None, after_long_help = EXIT_CODES_HELP)]
36pub struct Cli {
37    /// Emit JSON instead of markdown. Matches MCP `structured_content` shape.
38    #[arg(long, global = true)]
39    pub json: bool,
40
41    /// Suppress engine startup logs on stderr.
42    #[arg(long, global = true)]
43    pub quiet: bool,
44
45    #[command(subcommand)]
46    pub command: Command,
47}
48
49#[derive(Subcommand, Debug)]
50pub enum Command {
51    /// Node / edge counts and schema distribution.
52    Stats,
53
54    /// Read one entity as markdown.
55    Entity(commands::entity::Args),
56
57    /// List typed edges for an entity.
58    Relations(commands::relations::Args),
59
60    /// Find entities by text or graph proximity.
61    Search(commands::search::Args),
62
63    /// Filter entities by metadata (no text match — use `search` for that).
64    List(commands::list::Args),
65
66    /// Read an entity's community cluster.
67    Context(commands::context::Args),
68
69    /// All clusters with summaries and member lists. The full build
70    /// renders the same rich content the MCP `memstead_overview` tool
71    /// emits — both surfaces share the engine composer in `memstead-engine`.
72    Overview(commands::overview::Args),
73
74    /// Describe one type, or list all types when no name given.
75    Type(commands::type_cmd::Args),
76
77    /// Health summary (orphans, stubs, stale entities, missing fields).
78    Health(commands::health::Args),
79
80    /// Export the write mem as markdown (in place) or as a portable `.mem` archive.
81    Export(commands::export::Args),
82
83    /// Initialise a filesystem mem in the current (or named) folder.
84    /// Strict: errors out when the target is not empty.
85    Init(commands::init::InitArgs),
86
87    /// One-command cold start: workspace + default-schema mem + seed
88    /// entity + MCP wiring for your agent(s), in the current (or named)
89    /// folder. Tolerates dotfiles and README-grade files; derives the
90    /// mem name from the folder. For the strict, script-safe variant
91    /// use `memstead init`.
92    Quickstart(commands::quickstart::Args),
93
94    /// Install a sealed `.mem` mem — either a local file, or `<scope>/<name>`
95    /// from the memstead.io registry.
96    #[cfg(feature = "mem-repo")]
97    Install(commands::install::Args),
98
99    /// Link a filesystem mem to a registry-published dependency.
100    /// `memstead link <scope/name>` fetches the archive into
101    /// `.memstead/memstead-io/` and records the dep in `.memstead/config.json`.
102    Link(commands::link::LinkArgs),
103
104    /// Publish a `.mem` archive to the registry. Triggers GitHub
105    /// Device Flow on first use; subsequent runs are silent.
106    Publish(commands::publish::Args),
107
108    /// Unpublish (hard-delete) `<scope>/<name>` from the registry.
109    /// Permitted to the original uploader and to admins. The same
110    /// `<scope>/<name>` becomes immediately re-publishable.
111    Unpublish(commands::unpublish::Args),
112
113    /// Domain-authority publishing: generate the signing key for a domain you
114    /// control and print the `.well-known` manifest to host. `publish --scope
115    /// <domain>:<handle>` then signs with that key — no GitHub account needed.
116    Domain {
117        #[command(subcommand)]
118        action: commands::domain::DomainAction,
119    },
120
121    /// Admin-only registry moderation: take a mem down or deny-list
122    /// bytes. Gated server-side by the `MEMSTEAD_ADMINS` allowlist; every
123    /// action is recorded in the registry's append-only audit log.
124    Admin {
125        #[command(subcommand)]
126        action: commands::admin::AdminAction,
127    },
128
129    /// Authenticate with a registry via GitHub Device Flow. Optional —
130    /// `publish` auto-triggers the same flow on first use.
131    Login(commands::login::Args),
132
133    /// Remove stored credentials for a registry.
134    Logout(commands::logout::Args),
135
136    /// Create a new entity. Provide `--title`, `--type`, and the required
137    /// section fields, or pass `--from <file.json>` with the full payload.
138    Create(commands::create::Args),
139
140    /// Modify an existing entity. `--expected-hash` is required unless
141    /// `--auto-hash` (refetch before write) or `--force` (skip check) is given.
142    Update(commands::update::Args),
143
144    /// Add or remove a typed relationship between two entities.
145    Relate(commands::relate::Args),
146
147    /// Delete an entity. Use `--dry-run` to preview impact first.
148    /// Delete is hashless by design (no post-state to race on); race
149    /// protection comes from `HAS_INCOMING_REFS` — and
150    /// `RESIDUAL_STUB_FOR_READONLY_REFERRERS` for read-only-referrer cases.
151    Delete(commands::delete::Args),
152
153    /// Rename an entity (changes ID, file path, and every incoming wiki-link).
154    Rename(commands::rename::Args),
155
156    /// Update many entities in one atomic call. Input is a JSON file
157    /// with a top-level `updates: [...]` array (one entry per entity,
158    /// each with its own hash mode and mutation fields). All-or-nothing:
159    /// if any entry fails (validation, hash mismatch, missing entity)
160    /// the whole batch is refused and NOTHING is committed — fix the
161    /// named entry and resubmit. On success the batch lands as one
162    /// commit. Mirrors `memstead update` per entry.
163    #[cfg(feature = "mem-repo")]
164    #[command(name = "batch-update")]
165    BatchUpdate(commands::batch_update::Args),
166
167    /// Apply parse-time-drift recovery across writable mems. Walks
168    /// `PARSED_RELATION_INVALID` warnings, re-renders affected
169    /// source entities to drop the stale rows, and reports per-entry
170    /// outcomes. Read-only-origin drops surface as skipped.
171    #[cfg(feature = "mem-repo")]
172    Recover(commands::recover::Args),
173
174    /// Diff a mem's HEAD against a commit SHA. Pass `--since` = a
175    /// prior `commit_sha` from a mutation, or the canonical empty-tree
176    /// hash `4b825dc642cb6eb9a060e54bf8d69288fbee4904` for a first sync.
177    Changes(commands::changes::Args),
178
179    /// Reload one writable mem's slice of the in-memory store from
180    /// its on-disk branch tip — or every writable mem when
181    /// `--mem` is omitted. CLI parity with the MCP `memstead_reload`
182    /// tool.
183    Reload(commands::reload::Args),
184
185    /// Fetch a mem's branch refs from a git remote into the mem-repo
186    /// (no local branch moves — inspect first, then `pull`). Requires a
187    /// git-branch-backed mem (`INVALID_INPUT` on folder mounts);
188    /// refuses `UNKNOWN_REMOTE` when the remote is not configured.
189    #[cfg(feature = "mem-repo")]
190    Fetch(commands::transport::FetchArgs),
191
192    /// Fast-forward a mem's branch to its fetched remote counterpart
193    /// and reload the in-memory store. Refuses `LOCAL_DIVERGENCE` when
194    /// the local branch is not an ancestor of the remote — reconcile
195    /// via `branch-reset`, or resolve on another clone and push.
196    #[cfg(feature = "mem-repo")]
197    Pull(commands::transport::PullArgs),
198
199    /// Push a mem's branch to a git remote. `--force` uses
200    /// force-with-lease semantics; without it, non-fast-forward pushes
201    /// refuse (`NON_FAST_FORWARD`). Refuses `UNKNOWN_REMOTE` when the
202    /// remote is not configured.
203    #[cfg(feature = "mem-repo")]
204    Push(commands::transport::PushArgs),
205
206    /// Reset a mem's branch pointer to a target ref/SHA. Refuses to
207    /// discard commits reachable from any remote ref
208    /// (`PUSHED_COMMITS_PROTECTED`).
209    #[cfg(feature = "mem-repo")]
210    #[command(name = "branch-reset")]
211    BranchReset(commands::branch_reset::BranchResetArgs),
212
213    /// Mem lifecycle commands.
214    #[cfg(feature = "mem-repo")]
215    Mem {
216        #[command(subcommand)]
217        action: commands::mem::MemAction,
218    },
219
220    /// Mem-repo-git lifecycle commands.
221    #[cfg(feature = "mem-repo")]
222    #[command(name = "mem-repo")]
223    MemRepo {
224        #[command(subcommand)]
225        action: commands::mem_repo::MemRepoAction,
226    },
227
228    /// Introspect and configure workspace policy — `dump` reads the
229    /// effective config; `allow-create`/`revoke-create`/`allow-delete`/
230    /// `revoke-delete`/`grant-cross-link`/`revoke-cross-link`/`set-mutations`
231    /// write the mem-lifecycle allowlist, cross-mem link grants, and
232    /// mutation policy.
233    #[cfg(feature = "mem-repo")]
234    Workspace {
235        #[command(subcommand)]
236        action: commands::workspace::WorkspaceAction,
237    },
238
239    /// Author-time schema tooling. `memstead schema validate <path>`
240    /// checks a schema package directory against the engine's loader
241    /// without touching a workspace.
242    Schema(commands::schema::Args),
243
244    /// Pipeline-config tooling. `memstead pipeline migrate` converts the
245    /// legacy `scopes|projections|ingests/` JSON folders into the
246    /// `.memstead/` workspace store's four-primitive shape.
247    Pipeline(commands::pipeline::Args),
248}