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, schema distribution, and per-binding projection state.
52 Status,
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 the
101 /// workspace and records the dependency in the workspace config.
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 /// Read provenance anchors (E3a): `memstead anchors <id>` lists an
175 /// entity's anchors + composition; `memstead anchors --artifact <path>`
176 /// reverse-looks-up every entity whose anchor references that path
177 /// (the query the check-realization hook consumes).
178 Anchors(commands::anchors::Args),
179
180 /// Diff a mem's HEAD against a commit SHA. Pass `--since` = a
181 /// prior `commit_sha` from a mutation, or the canonical empty-tree
182 /// hash `4b825dc642cb6eb9a060e54bf8d69288fbee4904` for a first sync.
183 Changes(commands::changes::Args),
184
185 /// Reload one writable mem's slice of the in-memory store from
186 /// its on-disk branch tip — or every writable mem when
187 /// `--mem` is omitted. CLI parity with the MCP `memstead_reload`
188 /// tool.
189 Reload(commands::reload::Args),
190
191 /// Fetch a mem's branch refs from a git remote into the mem-repo
192 /// (no local branch moves — inspect first, then `pull`). Requires a
193 /// git-branch-backed mem (`INVALID_INPUT` on folder mounts);
194 /// refuses `UNKNOWN_REMOTE` when the remote is not configured.
195 #[cfg(feature = "mem-repo")]
196 Fetch(commands::transport::FetchArgs),
197
198 /// Fast-forward a mem's branch to its fetched remote counterpart
199 /// and reload the in-memory store. Refuses `LOCAL_DIVERGENCE` when
200 /// the local branch is not an ancestor of the remote — reconcile
201 /// via `branch-reset`, or resolve on another clone and push.
202 #[cfg(feature = "mem-repo")]
203 Pull(commands::transport::PullArgs),
204
205 /// Push a mem's branch to a git remote. `--force` uses
206 /// force-with-lease semantics; without it, non-fast-forward pushes
207 /// refuse (`NON_FAST_FORWARD`). Refuses `UNKNOWN_REMOTE` when the
208 /// remote is not configured.
209 #[cfg(feature = "mem-repo")]
210 Push(commands::transport::PushArgs),
211
212 /// Reset a mem's branch pointer to a target ref/SHA. Refuses to
213 /// discard commits reachable from any remote ref
214 /// (`PUSHED_COMMITS_PROTECTED`).
215 #[cfg(feature = "mem-repo")]
216 #[command(name = "branch-reset")]
217 BranchReset(commands::branch_reset::BranchResetArgs),
218
219 /// Mem lifecycle commands.
220 #[cfg(feature = "mem-repo")]
221 Mem {
222 #[command(subcommand)]
223 action: commands::mem::MemAction,
224 },
225
226 /// Mem-repo-git lifecycle commands.
227 #[cfg(feature = "mem-repo")]
228 #[command(name = "mem-repo")]
229 MemRepo {
230 #[command(subcommand)]
231 action: commands::mem_repo::MemRepoAction,
232 },
233
234 /// Introspect and configure workspace policy — `dump` reads the
235 /// effective config; `allow-create`/`revoke-create`/`allow-delete`/
236 /// `revoke-delete`/`grant-cross-link`/`revoke-cross-link`/`set-mutations`
237 /// write the mem-lifecycle allowlist, cross-mem link grants, and
238 /// mutation policy.
239 #[cfg(feature = "mem-repo")]
240 Workspace {
241 #[command(subcommand)]
242 action: commands::workspace::WorkspaceAction,
243 },
244
245 /// Author-time schema tooling. `memstead schema validate <path>`
246 /// checks a schema package directory against the engine's loader
247 /// without touching a workspace.
248 Schema(commands::schema::Args),
249
250 /// Binding (projection-promotion) tooling — the projection is the unit,
251 /// one versioned binding per source→mem obligation. `memstead projection
252 /// brief <binding>` renders a binding's run-brief (the Markdown prompt an
253 /// agent consumes); `memstead projection init` scaffolds a fresh v1 binding
254 /// non-interactively; `memstead projection migrate` promotes both legacy
255 /// generations (root-folder `scopes|projections|ingests/` and the gen-2
256 /// four-primitive store) into v1 bindings; `memstead projection advance`
257 /// records disposition-gated sync-baseline advances; `memstead projection
258 /// enable <build|sync|verify> <binding>` adds a missing operation block.
259 Projection(commands::projection::Args),
260}