crates/cli/git-ents/src/cli.rs
| 1 | //! `git ents`'s argument grammar — `figue` derive definitions only. |
| 2 | //! |
| 3 | //! Per this project's engineering conventions, this module carries no |
| 4 | //! logic: every doc comment here becomes `--help` text, and |
| 5 | //! [`crate::exe`] is the only place a [`Top`] variant is interpreted. |
| 6 | |
| 7 | use std::path::PathBuf; |
| 8 | |
| 9 | use facet::Facet; |
| 10 | use figue::{self as args, FigueBuiltins}; |
| 11 | |
| 12 | pub use ents_forge::comment::CommentAction; |
| 13 | pub use ents_forge::issue::IssueAction; |
| 14 | pub use ents_forge::review::ReviewAction; |
| 15 | pub use ents_kiln::toolchain::ToolchainAction; |
| 16 | |
| 17 | /// Local root wiring, subcommand surface, and the single-node hosted |
| 18 | /// root's git-hook plumbing (`docs/development-plan.adoc`, phase 6). |
| 19 | #[derive(Facet)] |
| 20 | pub struct Cli { |
| 21 | /// The subcommand to run. |
| 22 | #[facet(args::subcommand)] |
| 23 | pub command: Top, |
| 24 | /// `--help`/`--version`/`--completions` wiring `figue` provides for |
| 25 | /// every CLI built on it. |
| 26 | #[facet(flatten)] |
| 27 | pub builtins: FigueBuiltins, |
| 28 | } |
| 29 | |
| 30 | /// Every top-level `git ents` subcommand. |
| 31 | // @relation(roots.local, roots.worktree-update, roots.single-node-hosted, lens.serve, scope=file) |
| 32 | #[derive(Facet)] |
| 33 | #[repr(u8)] |
| 34 | pub enum Top { |
| 35 | /// Configure this repository for signed local writes: resolve or |
| 36 | /// generate a signing key, record it as `user.signingkey` with |
| 37 | /// `gpg.format=ssh`, and set `receive.denyCurrentBranch=updateInstead` |
| 38 | /// so the integration-test harness can push into this repository's |
| 39 | /// checked-out branch (`roots.worktree-update`). |
| 40 | /// |
| 41 | /// With `--hosted`, configures the single-node hosted root instead |
| 42 | /// (`roots.single-node-hosted`): a signing key for the hosted worker, |
| 43 | /// and this binary's own `pre-receive`/`post-receive` hooks installed |
| 44 | /// into a bare repository's `hooks/` directory. Without these hooks |
| 45 | /// installed, a hosted bare repository accepts every push ungated — |
| 46 | /// stock git's `receive-pack` has no gate of its own. |
| 47 | Setup { |
| 48 | /// Key to sign with; defaults to `user.signingkey`, else a new |
| 49 | /// `~/.ssh/id_ed25519` is generated. |
| 50 | #[facet(args::named)] |
| 51 | key: Option<PathBuf>, |
| 52 | /// Configure the single-node hosted root instead of the local |
| 53 | /// one: install this binary's `hook pre-receive`/`hook |
| 54 | /// post-receive` into a bare repository's own hooks, and a |
| 55 | /// signing key for the hosted worker. |
| 56 | #[facet(args::named, default)] |
| 57 | hosted: bool, |
| 58 | /// The bare repository to configure with `--hosted`; defaults to |
| 59 | /// the current directory. Ignored without `--hosted`. |
| 60 | #[facet(args::positional, default)] |
| 61 | path: Option<PathBuf>, |
| 62 | }, |
| 63 | /// Bootstrap a fresh hosted root from a clone of it: enroll yourself |
| 64 | /// as the self-admitting first member (`gate.bootstrap`), then vouch |
| 65 | /// for the server's own key (`roots.web-signing`) so its fail-closed |
| 66 | /// web UI can boot, pushing both enrollments to the remote. Run from |
| 67 | /// your clone, never on the server — enrolling server-side would |
| 68 | /// make the machine the trust root instead of the operator. |
| 69 | Bootstrap { |
| 70 | /// Your username to enroll (`refs/meta/member/<username>`). |
| 71 | #[facet(args::positional)] |
| 72 | username: String, |
| 73 | /// The server's public key to vouch for; defaults to fetching |
| 74 | /// `/.ents/server-key` from the remote's host — the hosted |
| 75 | /// root's front proxy publishes the key's public half there |
| 76 | /// while the web UI awaits this enrollment. Required when the |
| 77 | /// remote is not http(s). |
| 78 | #[facet(args::named)] |
| 79 | server_pubkey: Option<String>, |
| 80 | /// The username the server key is enrolled under; defaults to |
| 81 | /// `forge`. |
| 82 | #[facet(args::named)] |
| 83 | server_name: Option<String>, |
| 84 | /// The remote to push both enrollments to; defaults to `origin`. |
| 85 | #[facet(args::named)] |
| 86 | remote: Option<String>, |
| 87 | /// Key to sign both enrollments with; defaults to |
| 88 | /// `user.signingkey`. |
| 89 | #[facet(args::named)] |
| 90 | key: Option<PathBuf>, |
| 91 | }, |
| 92 | /// Manage the repository members at `refs/meta/member/<username>`. |
| 93 | Members { |
| 94 | /// The member action to run. |
| 95 | #[facet(args::subcommand)] |
| 96 | action: MembersAction, |
| 97 | }, |
| 98 | /// Manage this repository's account identity at `refs/meta/account`. |
| 99 | Account { |
| 100 | /// The account action to run. |
| 101 | #[facet(args::subcommand)] |
| 102 | action: AccountAction, |
| 103 | }, |
| 104 | /// Manage the configured effects at `refs/meta/effects/<name>` and run |
| 105 | /// them locally. |
| 106 | Effect { |
| 107 | /// The effect action to run. |
| 108 | #[facet(args::subcommand)] |
| 109 | action: EffectAction, |
| 110 | }, |
| 111 | /// Manage the toolchains stored as git trees at |
| 112 | /// `refs/meta/toolchains/<name>`. |
| 113 | Toolchain { |
| 114 | /// The toolchain action to run. |
| 115 | #[facet(args::subcommand)] |
| 116 | action: ToolchainAction, |
| 117 | }, |
| 118 | /// Comment on code: one comment per ref at `refs/meta/comments/<id>`, |
| 119 | /// anchored to a blob (and optionally lines) at a commit. |
| 120 | Comment { |
| 121 | /// The comment action to run. |
| 122 | #[facet(args::subcommand)] |
| 123 | action: CommentAction, |
| 124 | }, |
| 125 | /// Manage issues at `refs/meta/issues/<id>`. |
| 126 | Issue { |
| 127 | /// The issue action to run. |
| 128 | #[facet(args::subcommand)] |
| 129 | action: IssueAction, |
| 130 | }, |
| 131 | /// Review a commit: a verdict plus a body at |
| 132 | /// `refs/meta/reviews/<target>/<member>`, with a retention pin at |
| 133 | /// `refs/meta/pins/reviews/<target>/<member>` keeping the reviewed |
| 134 | /// commit reachable. |
| 135 | Review { |
| 136 | /// The review action to run. |
| 137 | #[facet(args::subcommand)] |
| 138 | action: ReviewAction, |
| 139 | }, |
| 140 | /// Work with entities awaiting adoption at |
| 141 | /// `refs/meta/inbox/<member>/<id>`. |
| 142 | Inbox { |
| 143 | /// The inbox action to run. |
| 144 | #[facet(args::subcommand)] |
| 145 | action: InboxAction, |
| 146 | }, |
| 147 | /// Manage redactions recorded at `refs/meta/redactions/<id>`. |
| 148 | Redact { |
| 149 | /// The redaction action to run. |
| 150 | #[facet(args::subcommand)] |
| 151 | action: RedactAction, |
| 152 | }, |
| 153 | /// Plumbing invoked by git's own hooks on the single-node hosted root |
| 154 | /// (`git.ents.cloud`) — not part of the porcelain surface a developer |
| 155 | /// runs directly. |
| 156 | Hook { |
| 157 | /// Which hook is running. |
| 158 | #[facet(args::subcommand)] |
| 159 | action: HookAction, |
| 160 | }, |
| 161 | /// Prove membership to a hosted web session (`roots.web-signin`): |
| 162 | /// fetch the one-time challenge the hosted `/login` page displayed, |
| 163 | /// sign it with your member key under the `git-ents-login` SSHSIG |
| 164 | /// namespace — locally, the key never leaves this machine — and post |
| 165 | /// the signature back, signing that browser session in. |
| 166 | Login { |
| 167 | /// The hosted root's base URL, e.g. `https://git.ents.cloud`. |
| 168 | #[facet(args::positional)] |
| 169 | url: String, |
| 170 | /// The one-time code the `/login` page displays (`XXXX-XXXX`). |
| 171 | #[facet(args::positional)] |
| 172 | code: String, |
| 173 | /// Key to prove membership with; defaults to `user.signingkey`, |
| 174 | /// else `~/.ssh/id_ed25519`. |
| 175 | #[facet(args::named)] |
| 176 | key: Option<PathBuf>, |
| 177 | }, |
| 178 | /// Start the local web UI (`roots.local`): reuses this repository's |
| 179 | /// existing local composition root (the same loose-ref `RefStore`, |
| 180 | /// odb, null `EventSink`, and advisory gate `git ents members`, |
| 181 | /// `git ents comment`, and every other porcelain command already use) |
| 182 | /// and adds only the `ents-web` HTTP frontend, bound to loopback — |
| 183 | /// never git's own smart-HTTP transport, which this command does not |
| 184 | /// expose in any form. With `--hosted`, serves the single-node hosted |
| 185 | /// root's web UI instead (`roots.single-node-hosted`). |
| 186 | Serve { |
| 187 | /// Port to bind on loopback (`127.0.0.1`); `0` picks any free |
| 188 | /// port. Defaults to 4880. |
| 189 | #[facet(args::named)] |
| 190 | port: Option<u16>, |
| 191 | /// Key to sign web edits with; defaults to `user.signingkey`. |
| 192 | #[facet(args::named)] |
| 193 | key: Option<PathBuf>, |
| 194 | /// Serve the single-node hosted root's web UI instead |
| 195 | /// (`roots.single-node-hosted`): mandatory gate, sign-in |
| 196 | /// required, member-attributed edits, the server's own key as |
| 197 | /// signing identity. Still binds loopback — the front proxy is |
| 198 | /// the only external listener. |
| 199 | #[facet(args::named, default)] |
| 200 | hosted: bool, |
| 201 | /// The canonical public host bound into sign-in challenges with |
| 202 | /// `--hosted` (`roots.web-signin`), e.g. `git.ents.cloud`. |
| 203 | /// Required with `--hosted`; ignored without it. |
| 204 | #[facet(args::named)] |
| 205 | public_host: Option<String>, |
| 206 | /// The bare repository to serve with `--hosted`; defaults to the |
| 207 | /// current directory. Ignored without `--hosted`. |
| 208 | #[facet(args::positional, default)] |
| 209 | path: Option<PathBuf>, |
| 210 | }, |
| 211 | /// Serve the editor lens (`lens.serve`): a Language Server Protocol |
| 212 | /// server over stdin/stdout that projects this repository's comments |
| 213 | /// (`refs/meta/comments/*`) into whatever buffer an editor has open, |
| 214 | /// and composes new ones through the same signed path `git ents |
| 215 | /// comment` uses (`lens.parity`). |
| 216 | /// |
| 217 | /// Speaks LSP over stdio only: it binds no network socket and adds no |
| 218 | /// git-serving transport. It reuses the very same local composition |
| 219 | /// root `git ents serve` and every other porcelain command use (the |
| 220 | /// same loose-ref `RefStore`, odb, null `EventSink`, and advisory |
| 221 | /// gate), adding only the LSP frontend and signing with the user's own |
| 222 | /// key. Meant to be launched by an editor extension (e.g. `ents-zed`), |
| 223 | /// not run interactively. |
| 224 | Lsp { |
| 225 | /// Key to sign composed comments with; defaults to |
| 226 | /// `user.signingkey`. |
| 227 | #[facet(args::named)] |
| 228 | key: Option<PathBuf>, |
| 229 | }, |
| 230 | } |
| 231 | |
| 232 | /// `git ents members` actions. |
| 233 | #[derive(Facet)] |
| 234 | #[repr(u8)] |
| 235 | pub enum MembersAction { |
| 236 | /// List the members recorded in this repository. |
| 237 | List, |
| 238 | /// Enroll a new member, or update an existing one's key. |
| 239 | Add { |
| 240 | /// The member's username (`refs/meta/member/<username>`). |
| 241 | #[facet(args::positional)] |
| 242 | username: String, |
| 243 | /// The public key to enroll (an OpenSSH single-line public key); |
| 244 | /// defaults to the signer's own public key. |
| 245 | #[facet(args::named)] |
| 246 | pubkey: Option<String>, |
| 247 | /// Key to sign the enrollment with; defaults to `user.signingkey`. |
| 248 | #[facet(args::named)] |
| 249 | key: Option<PathBuf>, |
| 250 | }, |
| 251 | /// Remove a member, deleting its ref. |
| 252 | Remove { |
| 253 | /// The member (username) to remove. |
| 254 | #[facet(args::positional)] |
| 255 | username: String, |
| 256 | /// Key to sign the removal with; defaults to `user.signingkey`. |
| 257 | #[facet(args::named)] |
| 258 | key: Option<PathBuf>, |
| 259 | }, |
| 260 | /// Revoke a member's key (`model.member-revocation`): the record |
| 261 | /// stays, but the key no longer authorizes new signatures. |
| 262 | Revoke { |
| 263 | /// The member (username) to revoke. |
| 264 | #[facet(args::positional)] |
| 265 | username: String, |
| 266 | /// Key to sign the revocation with; defaults to `user.signingkey`. |
| 267 | #[facet(args::named)] |
| 268 | key: Option<PathBuf>, |
| 269 | }, |
| 270 | /// Lift a revocation, restoring a member's key to active. |
| 271 | Unrevoke { |
| 272 | /// The member (username) to unrevoke. |
| 273 | #[facet(args::positional)] |
| 274 | username: String, |
| 275 | /// Key to sign the unrevocation with; defaults to |
| 276 | /// `user.signingkey`. |
| 277 | #[facet(args::named)] |
| 278 | key: Option<PathBuf>, |
| 279 | }, |
| 280 | /// Report whether a key is an active member. |
| 281 | Check { |
| 282 | /// Key to look for; defaults to `user.signingkey`. |
| 283 | #[facet(args::named)] |
| 284 | key: Option<PathBuf>, |
| 285 | }, |
| 286 | } |
| 287 | |
| 288 | /// `git ents account` actions. |
| 289 | #[derive(Facet)] |
| 290 | #[repr(u8)] |
| 291 | pub enum AccountAction { |
| 292 | /// Show this repository's account identity. |
| 293 | Show, |
| 294 | /// Create or update this repository's account identity. |
| 295 | Create { |
| 296 | /// The member this account belongs to; defaults to the signer's |
| 297 | /// own member (resolved by public key). |
| 298 | #[facet(args::named)] |
| 299 | member: Option<String>, |
| 300 | /// The login identity the member authenticates as. |
| 301 | #[facet(args::named)] |
| 302 | login: String, |
| 303 | /// Key to sign with; defaults to `user.signingkey`. |
| 304 | #[facet(args::named)] |
| 305 | key: Option<PathBuf>, |
| 306 | }, |
| 307 | } |
| 308 | |
| 309 | /// `git ents effect` actions. |
| 310 | #[derive(Facet)] |
| 311 | #[repr(u8)] |
| 312 | pub enum EffectAction { |
| 313 | /// List the effects configured in this repository. |
| 314 | /// |
| 315 | /// With --porcelain, emits a stable machine-readable form: |
| 316 | /// blank-line-separated records, each starting with a `<name>` line, |
| 317 | /// followed by `trigger <query>`, `toolchains <a, b>` (when |
| 318 | /// non-empty), and `run <command>` lines. |
| 319 | List { |
| 320 | /// Emit the stable machine-readable form described above. |
| 321 | #[facet(args::named, default)] |
| 322 | porcelain: bool, |
| 323 | }, |
| 324 | /// Show one effect's definition and, when a commit is given, its |
| 325 | /// result. |
| 326 | Show { |
| 327 | /// The effect's name. |
| 328 | #[facet(args::positional)] |
| 329 | name: String, |
| 330 | /// Commit to show the result for. |
| 331 | #[facet(args::named)] |
| 332 | at: Option<String>, |
| 333 | }, |
| 334 | /// Define (or replace) an effect and push the update. |
| 335 | Add { |
| 336 | /// Name to record the effect under (`refs/meta/effects/<name>`). |
| 337 | #[facet(args::positional)] |
| 338 | name: String, |
| 339 | /// The query this effect triggers on (`query.grammar`). |
| 340 | #[facet(args::named)] |
| 341 | on: String, |
| 342 | /// The command the effect runs. |
| 343 | #[facet(args::positional)] |
| 344 | run: String, |
| 345 | /// Toolchain (`refs/meta/toolchains/<name>`) to activate before |
| 346 | /// the command runs (repeatable). |
| 347 | #[facet(args::named, args::label = "TOOLCHAIN", default)] |
| 348 | toolchain: Vec<String>, |
| 349 | /// Key to sign with; defaults to `user.signingkey`. |
| 350 | #[facet(args::named)] |
| 351 | key: Option<PathBuf>, |
| 352 | }, |
| 353 | /// Run this repository's effects locally against every commit still |
| 354 | /// owed a result, or a single one with `--at` |
| 355 | /// (`effect.local-run`): identical toolchain materialization and |
| 356 | /// sandbox path to a hosted worker, the queue skipped entirely. |
| 357 | Run { |
| 358 | /// The effect's name. |
| 359 | #[facet(args::positional)] |
| 360 | name: String, |
| 361 | /// Commit to run against; omit to run every outstanding commit |
| 362 | /// (`query.workset`). |
| 363 | #[facet(args::named)] |
| 364 | at: Option<String>, |
| 365 | /// Key to sign the result with; defaults to `user.signingkey`. |
| 366 | #[facet(args::named)] |
| 367 | key: Option<PathBuf>, |
| 368 | }, |
| 369 | /// Show recorded results for an effect, one row per judged commit. |
| 370 | /// |
| 371 | /// With --porcelain, emits a stable machine-readable form: |
| 372 | /// blank-line-separated records of one line each, |
| 373 | /// `<commit> <status>` — the full oid of the judged commit and |
| 374 | /// pass, fail, or error. |
| 375 | Log { |
| 376 | /// The effect's name. |
| 377 | #[facet(args::positional)] |
| 378 | name: String, |
| 379 | /// Emit the stable machine-readable form described above. |
| 380 | #[facet(args::named, default)] |
| 381 | porcelain: bool, |
| 382 | }, |
| 383 | } |
| 384 | |
| 385 | /// `git ents inbox` actions. |
| 386 | #[derive(Facet)] |
| 387 | #[repr(u8)] |
| 388 | pub enum InboxAction { |
| 389 | /// List entities awaiting adoption. |
| 390 | List, |
| 391 | /// Adopt an inbox entity onto its canonical ref |
| 392 | /// (`sync.adoption-machinery`): a merge that keeps the author's |
| 393 | /// original signed commit in ancestry |
| 394 | /// (`sync.adoption-no-cherry-pick`). |
| 395 | Adopt { |
| 396 | /// The inbox entry to adopt, as `<member>/<id>`. |
| 397 | #[facet(args::positional)] |
| 398 | entry: String, |
| 399 | /// Key to sign the adoption merge with; defaults to |
| 400 | /// `user.signingkey`. |
| 401 | #[facet(args::named)] |
| 402 | key: Option<PathBuf>, |
| 403 | }, |
| 404 | } |
| 405 | |
| 406 | /// `git ents redact` actions. |
| 407 | #[derive(Facet)] |
| 408 | #[repr(u8)] |
| 409 | pub enum RedactAction { |
| 410 | /// List the redactions recorded in this repository. |
| 411 | List, |
| 412 | /// Record that `oid` was redacted (`refs/meta/redactions/<id>`), |
| 413 | /// refusing any future push that would refill it |
| 414 | /// (`receive.redaction-ingest`). Admin-only: the gate's default |
| 415 | /// namespace-authorization arm requires admin-registered provenance |
| 416 | /// for `refs/meta/redactions/*`. |
| 417 | Add { |
| 418 | /// The object id to redact. |
| 419 | #[facet(args::positional)] |
| 420 | oid: String, |
| 421 | /// A human-readable reason recorded alongside the redaction. |
| 422 | #[facet(args::named)] |
| 423 | reason: String, |
| 424 | /// Key to sign with; defaults to `user.signingkey`. |
| 425 | #[facet(args::named)] |
| 426 | key: Option<PathBuf>, |
| 427 | }, |
| 428 | } |
| 429 | |
| 430 | /// Plumbing subcommands the single-node hosted root's git hooks invoke; |
| 431 | /// see `crate::hook`'s own doc for what each does and why. |
| 432 | #[derive(Facet)] |
| 433 | #[repr(u8)] |
| 434 | pub enum HookAction { |
| 435 | /// Run as git's own `pre-receive` hook: evaluate the gate against |
| 436 | /// every proposed transition read from stdin, refusing the whole |
| 437 | /// push under the mandatory gate if any fails. |
| 438 | PreReceive, |
| 439 | /// Run as git's own `post-receive` hook: reconcile outstanding effect |
| 440 | /// obligations (`receive.reconstructible`) and run them. |
| 441 | PostReceive, |
| 442 | /// Reconcile outstanding effect obligations without running anything |
| 443 | /// — the boot-time scan on its own, for operational use and testing. |
| 444 | Reconcile, |
| 445 | } |