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}