Skip to main content

bynk/
cli.rs

1//! The `bynk` driver command-line interface.
2//!
3//! The developer front-end: `doctor` / `new` / `dev`, plus the everyday
4//! `check` / `fmt` / `test` (v0.138, #487). `check` and `fmt` run the linked
5//! pipeline in-process; `test` delegates to the driver-resolved `bynkc`. The
6//! flag surfaces mirror `bynkc`'s so the two are drop-in equivalent.
7
8use std::path::PathBuf;
9
10use clap::{Args, Parser, Subcommand, ValueEnum};
11
12use crate::doctor::{Capability, DoctorOptions};
13use crate::report::Format;
14
15#[derive(Parser, Debug)]
16#[command(name = "bynk", version, about = "The Bynk driver", long_about = None)]
17pub struct Cli {
18    #[command(subcommand)]
19    pub command: Command,
20}
21
22#[derive(Subcommand, Debug)]
23pub enum Command {
24    /// Check whether your machine is ready to compile, test, and deploy Bynk —
25    /// and print the exact remedy for anything missing.
26    ///
27    /// Bare `bynk doctor` is informational: it surveys every capability and
28    /// exits 0 unless `bynkc` itself is unusable. `--only <capability>` gates on
29    /// one capability (exits non-zero if its tools are missing); `--strict`
30    /// turns every warning into a failure, for CI.
31    Doctor {
32        /// Project directory to inspect (for project-local `node_modules/.bin`
33        /// resolution). Defaults to the current directory.
34        #[arg(default_value = ".")]
35        input: PathBuf,
36        /// Scope the check — and the exit code — to one capability.
37        #[arg(long, value_enum)]
38        only: Option<CapabilityArg>,
39        /// Treat every warning (optional gaps, npx provisionability, minor
40        /// version skew) as a failure. For an all-green CI gate.
41        #[arg(long)]
42        strict: bool,
43        /// Output format. `human` (default) is a grouped table; `short` and
44        /// `json` are the stable scriptable surface.
45        #[arg(long, value_enum, default_value = "human")]
46        format: FormatArg,
47    },
48    /// Build the project and serve it locally with `wrangler dev`, rebuilding
49    /// on save — one step in place of the manual compile + `cd` + `wrangler
50    /// dev` recipe.
51    ///
52    /// Compiles into a managed `.bynk/dev/` build dir and runs one `wrangler
53    /// dev` per context from inside its worker dir, in local mode (Miniflare) —
54    /// no namespace provisioning needed. Every context is served by default and
55    /// the service bindings between them are wired (#552), so a cross-context
56    /// call resolves locally; `--context` narrows to a subset. While serving,
57    /// `.bynk` sources are watched (#524): saving a file rebuilds in place and
58    /// the running workers hot-reload; a failing rebuild reports errors and
59    /// keeps serving the last good build. Everything after `--` is forwarded to
60    /// `wrangler dev` verbatim.
61    Dev {
62        /// Project directory to serve from (anywhere inside the project; the
63        /// root is found by walking up for `bynk.toml`). Defaults to `.`.
64        #[arg(default_value = ".")]
65        path: PathBuf,
66        #[command(flatten)]
67        lock: LockArgs,
68        /// Which context to serve, repeatable. Omit to serve every context in
69        /// the project with the service bindings between them wired (#552);
70        /// pass one or more to narrow to a subset. Accepts the dotted name or
71        /// its dasherised worker-dir form.
72        #[arg(long = "context", value_name = "NAME")]
73        contexts: Vec<String>,
74        /// First port of the per-context allocation (context *i* gets
75        /// `--base-port` + *i*, in sorted order). Defaults to wrangler's 8787.
76        /// A single context left on the default keeps `-- --port N` working.
77        #[arg(long, value_name = "PORT")]
78        base_port: Option<u16>,
79        /// Serve with the V8 inspector enabled (slice 3, ADR 0104): `wrangler dev`
80        /// starts with `--inspector-port` so a JavaScript debugger can attach.
81        /// Breakpoints set in `.bynk` sources resolve through the emitted source
82        /// maps, composed into the worker bundle. Prints the inspector URL on start.
83        #[arg(long)]
84        inspect: bool,
85        /// First inspector port, allocated per context exactly as `--base-port`
86        /// is (default 9229). Used under `--inspect`, and also whenever several
87        /// contexts are served, so their always-on inspectors do not collide.
88        #[arg(long, default_value_t = 9229)]
89        inspect_port: u16,
90        /// Which `bynk.deploy.lock` environment `-- --remote` reads the KV id
91        /// from. `dev` never provisions, so this only selects among what
92        /// `bynk deploy --env NAME` already recorded — irrelevant without
93        /// `--remote`. Omit for today's single, unqualified default.
94        #[arg(long, default_value = "default", value_name = "NAME")]
95        env: String,
96        /// A Worker outside the project, named by a `Service` instance
97        /// (`@worker("NAME")`), to reach in your own locally running
98        /// `wrangler dev` of it rather than in the stand-in `dev` binds by
99        /// default. Repeatable.
100        #[arg(long = "local-worker", value_name = "NAME")]
101        local_workers: Vec<String>,
102        /// Output format. `human` (default) is the readable stream on stderr.
103        /// `json` adds one JSON event per line on stdout (`serving` with each
104        /// context's ports, `rebuilding`, `rebuild_ok`, `rebuild_failed`,
105        /// `stopped`), the stable scriptable surface for editors and CI.
106        #[arg(long, value_enum, default_value = "human")]
107        format: DevFormatArg,
108        /// Once every served context answers an HTTP probe, write
109        /// `{ "<worker>": "<url>" }` to FILE: the readiness signal for a test
110        /// runner or CI script. A stale FILE is removed at start, and FILE is
111        /// removed when the session stops.
112        #[arg(long, value_name = "FILE")]
113        ready_json: Option<PathBuf>,
114        /// Arguments after `--`, forwarded to `wrangler dev` (e.g. `-- --remote`).
115        /// Ports are the driver's to allocate: use `--base-port` / `--inspect-port`.
116        #[arg(last = true)]
117        wrangler_args: Vec<String>,
118    },
119    /// Provision each context's Cloudflare resources and deploy its Worker.
120    /// The whole project ships in one command, in Service-Binding dependency
121    /// order — Cloudflare rejects a Worker uploaded before a Worker it binds
122    /// to. The generated configuration remains disposable: Cloudflare ids live
123    /// in the committed `bynk.deploy.lock`.
124    Deploy {
125        /// Project directory to deploy from. Defaults to the current directory.
126        #[arg(default_value = ".")]
127        path: PathBuf,
128        #[command(flatten)]
129        lock: LockArgs,
130        /// Deploy this context alone, assuming the contexts it consumes are
131        /// already live; a dependency that has never been deployed is reported
132        /// rather than pushed into. Accepts the dotted name or its dasherised
133        /// worker-dir form. Omit to deploy the whole project in order.
134        #[arg(long, value_name = "NAME")]
135        context: Option<String>,
136        /// Target environment. Selects the `bynk.deploy.lock` section and,
137        /// for any value other than `default`, synthesises an environment-
138        /// scoped Wrangler config section (KV, queues, Service Bindings all
139        /// qualified) since Cloudflare does not inherit bindings into a named
140        /// environment. Omit to deploy today's single, unqualified default.
141        #[arg(long, default_value = "default", value_name = "NAME")]
142        env: String,
143        /// Print the provisioning and deploy plan without changing Cloudflare
144        /// or writing `bynk.deploy.lock`.
145        #[arg(long, visible_alias = "plan")]
146        dry_run: bool,
147        /// Plan output format. `short` is line-oriented; `json` is for CI.
148        #[arg(long, value_enum, default_value = "short")]
149        format: DeployFormatArg,
150        /// Skip the confirmation required before creating a namespace or
151        /// publishing a Worker. Required for non-interactive automation.
152        #[arg(long)]
153        yes: bool,
154        /// Read secret values from a dotenv-style `NAME=value` file. Supplies
155        /// both names and values; never committed, never persisted. Values move
156        /// to `wrangler secret put` and are dropped.
157        #[arg(long, value_name = "PATH")]
158        secrets_file: Option<PathBuf>,
159        /// Set this named secret, taking its value from the environment (or a
160        /// prompt). Repeatable. Use for a `bynk.Secrets` name, whose spelling
161        /// the compiler cannot know — an actor's declared `auth` secret needs no
162        /// flag. The environment is never scanned for names.
163        #[arg(long = "secret", value_name = "NAME")]
164        secrets: Vec<String>,
165        /// Overwrite a secret that is already set. The default sets only the
166        /// missing ones, so a re-deploy does not cut a fresh Cloudflare secret
167        /// version for every secret every time.
168        #[arg(long)]
169        force: bool,
170        /// Delete every reported orphan — a KV namespace or queue the ledger
171        /// remembers that the current build no longer declares. Never
172        /// deletes a Worker. Prompts separately from the creation
173        /// confirmation unless --yes is also given; omit to report only.
174        #[arg(long)]
175        prune: bool,
176        /// Confirm deleting this agent class's stored data, named as the plan
177        /// prints it (`<context>.<Class>`). Repeatable. A `removed agent`
178        /// declaration deletes data irreversibly, so `--yes` never stands in
179        /// for this.
180        #[arg(long = "confirm-delete", value_name = "CONTEXT.CLASS")]
181        confirm_delete: Vec<String>,
182        /// Arguments after `--`, forwarded to `wrangler deploy` verbatim.
183        #[arg(last = true)]
184        wrangler_args: Vec<String>,
185    },
186    /// Scaffold a new project: a complete, runnable single-context HTTP service
187    /// you can serve immediately with `bynk dev`.
188    ///
189    /// Writes a `bynk.toml`, a `.gitignore`, and `src/<name>.bynk` into a new
190    /// directory. Pure offline file-writing — it shells nothing and needs no
191    /// toolchain, so you can run it before `bynkc`, Node, or `wrangler` are
192    /// installed. The project name defaults to the target directory's final
193    /// component; `--name` overrides it and must be a legal Bynk identifier.
194    New {
195        /// Directory to create for the new project (e.g. `hello` or `./hello`).
196        path: PathBuf,
197        /// Project name / context identifier. Defaults to PATH's final
198        /// component; must be a legal Bynk identifier (a letter followed by
199        /// letters, digits, or underscores).
200        #[arg(long)]
201        name: Option<String>,
202    },
203    /// Type-check a `.bynk` file or project without writing output — the
204    /// `bynkc check` behaviour through the driver's compiler resolution (v0.138).
205    ///
206    /// Runs the compiler pipeline in-process (no `bynkc` binary required); with
207    /// `BYNK_BYNKC` set, the pinned compiler is shelled instead so an
208    /// externally-managed `bynkc` still governs the result.
209    Check {
210        /// Input `.bynk` file or project root. Defaults to the current directory.
211        #[arg(default_value = ".")]
212        input: PathBuf,
213        /// Diagnostic output format. `rich` (default) is the ariadne
214        /// source-context rendering; `short` emits one terse
215        /// `path:line:col: severity[category]: message` line per diagnostic,
216        /// for tooling (the VS Code problem-matcher, CI, scripts).
217        /// In both, `path` is the file as you'd type it from the working
218        /// directory (the input you passed, joined with the file's place in
219        /// it), the same path `fmt` reports.
220        #[arg(long, value_enum, default_value = "rich")]
221        format: CheckFormatArg,
222        #[command(flatten)]
223        lock: LockArgs,
224    },
225    /// Format `.bynk` source files in place — the `bynkc fmt` behaviour through
226    /// the driver (v0.138). Passing `-` reads from stdin and writes to stdout.
227    ///
228    /// Runs the formatter in-process (no `bynkc` binary required); with
229    /// `BYNK_BYNKC` set, the pinned compiler is shelled instead — the style
230    /// flags are forwarded to it either way.
231    ///
232    /// `--indent`, `--indent-width`, `--max-line-width` and
233    /// `--no-trailing-comma` override the canonical style for this run; with
234    /// none of them the output is the canonical formatting.
235    Fmt {
236        #[command(flatten)]
237        args: bynk_driver::FmtArgs,
238    },
239    /// Discover and run test declarations in a project — the `bynkc test`
240    /// behaviour through the driver (v0.138).
241    ///
242    /// Delegates to the `bynkc` the driver resolves (`BYNK_BYNKC` → PATH →
243    /// sibling-of-`bynk`), so an editor or developer inherits the driver's
244    /// richer compiler resolution instead of locating `bynkc` themselves.
245    /// Requires `tsc` (with Node.js) or `tsx` on PATH, exactly as `bynkc test`.
246    Test {
247        #[command(flatten)]
248        args: bynk_driver::test_runner::TestArgs,
249        #[command(flatten)]
250        lock: LockArgs,
251        /// Run even when the resolved `bynkc` is a different *major* version
252        /// from `bynk` (#1675). A minor skew only warns. Also settable as
253        /// `BYNK_ALLOW_SKEW=1`.
254        #[arg(long)]
255        allow_skew: bool,
256    },
257    /// Resolve the project's dependencies, fetch the git ones missing from the
258    /// store, and write `bynk.lock` (#2240). `check`, `test`, `dev` and
259    /// `deploy` do this first anyway; `fetch` does only this — warm the store
260    /// before an `--offline` CI step, or after editing `[dependencies]`.
261    Fetch {
262        /// Project (or workspace) directory. Defaults to `.`.
263        #[arg(default_value = ".")]
264        path: PathBuf,
265        #[command(flatten)]
266        lock: LockArgs,
267    },
268    /// Re-resolve git dependencies' revisions — a branch that moved upstream —
269    /// and rewrite `bynk.lock` (#2240). Name aliases or packages to update only
270    /// those; with none, every git dependency is re-resolved.
271    Update {
272        /// The aliases or `org/package` names to update. All when omitted.
273        #[arg(value_name = "DEPENDENCY")]
274        names: Vec<String>,
275        /// Project (or workspace) directory.
276        #[arg(long, default_value = ".")]
277        path: PathBuf,
278    },
279    /// Where each agent class's stored state stands, and the sweep that
280    /// migrates every stored instance so old `migrate` steps can retire.
281    Migrate {
282        #[command(subcommand)]
283        command: MigrateCommand,
284    },
285    /// Add a registry dependency to `bynk.toml` (#2274): the highest version
286    /// its registry offers (or one matching `@<range>`), written as
287    /// `<alias> = { package = "…", version = "^…" }`, then resolved and locked.
288    Add {
289        /// `org/package`, optionally `@<range>` (`acme/money@^1.4`).
290        #[arg(value_name = "PACKAGE")]
291        package: String,
292        /// The alias to reach it by; defaults to the package segment.
293        #[arg(long = "as", value_name = "ALIAS")]
294        alias: Option<String>,
295        /// Project directory.
296        #[arg(long, default_value = ".")]
297        path: PathBuf,
298    },
299    /// Publish this package to the registry its organisation routes to
300    /// (#2287): check it, pack it, upload it through the registry's publish
301    /// API, and verify the signed line the registry returns.
302    Publish {
303        /// The package directory.
304        #[arg(long, default_value = ".")]
305        path: PathBuf,
306        /// Check and pack, and report what would be published, without
307        /// uploading.
308        #[arg(long)]
309        dry_run: bool,
310    },
311    /// Yank a published version through its registry's publish API (#2287):
312    /// kept for locked builds, never newly chosen.
313    Yank {
314        /// `org/package`.
315        package: String,
316        /// The version.
317        version: String,
318        /// Clear the yank instead.
319        #[arg(long)]
320        undo: bool,
321        /// The project whose `[registries]` route the organisation.
322        #[arg(long, default_value = ".")]
323        path: PathBuf,
324    },
325    /// Sign in to a registry's accounts (#2378) through GitHub's device flow,
326    /// and keep the account token in the credentials file.
327    Login {
328        #[command(flatten)]
329        target: AccountTarget,
330    },
331    /// Sign out of a registry's accounts (#2378), here and on the server.
332    Logout {
333        #[command(flatten)]
334        target: AccountTarget,
335    },
336    /// Manage an organisation on a registry with accounts (#2378): claim and
337    /// block it, add and remove members, mint and revoke publish tokens.
338    Org {
339        #[command(subcommand)]
340        command: OrgCommand,
341    },
342    /// Review and approve the npm packages that git and registry
343    /// dependencies' adapters bring into the build (#2296), recording each
344    /// approval in `bynk.lock`. With no package, asks about each pending one.
345    Approve {
346        /// Approve this npm package's pending requirements.
347        #[arg(conflicts_with_all = ["all", "list"])]
348        package: Option<String>,
349        /// Approve every pending requirement without asking.
350        #[arg(long, conflicts_with = "list")]
351        all: bool,
352        /// List pending and approved requirements; change nothing.
353        #[arg(long)]
354        list: bool,
355        /// Project (or workspace) directory.
356        #[arg(long, default_value = ".")]
357        path: PathBuf,
358    },
359    /// Build and maintain a static package registry in a directory (#2274) —
360    /// served by any static host, or read as a `file://` index.
361    Registry {
362        #[command(subcommand)]
363        command: RegistryCommand,
364    },
365    /// Explain a diagnostic code — the longer-form "what the rule is, why it
366    /// exists, and how to fix it" behind a `bynk.*` error code (#853).
367    ///
368    /// Prints a curated blurb, a minimal before/after example, and a link to
369    /// the relevant Book concept page. The blurb is offline-complete, so the
370    /// substance is available without network. Codes without a curated
371    /// explanation report that gracefully; an unrecognised code exits non-zero.
372    /// The same explanations back the editor's clickable diagnostic-code links.
373    Explain {
374        /// The diagnostic code to explain, e.g. `bynk.resolve.unknown_type`.
375        #[arg(value_name = "CODE")]
376        code: String,
377    },
378}
379
380/// `bynk migrate`'s subcommands (state-migrations S4, #2259).
381#[derive(Subcommand, Debug)]
382pub enum MigrateCommand {
383    /// Each agent class's recorded version and floor, its `migrate` steps and
384    /// the versions they cover, any version with no path forward, and the
385    /// environments a floor-raising sweep must cover. Offline: reads
386    /// `bynk.schema.lock`, the source, and `bynk.deploy.lock`.
387    Status {
388        /// Project directory. Defaults to the current directory.
389        #[arg(default_value = ".")]
390        path: PathBuf,
391        #[command(flatten)]
392        lock: LockArgs,
393        /// Output format. `json` is for scripts.
394        #[arg(long, value_enum, default_value = "short")]
395        format: DeployFormatArg,
396    },
397    /// Wake every stored instance of the project's agent classes in the
398    /// deployed Workers, so each migrates to its class's current version, and
399    /// raise each class's floor when the run covered every environment
400    /// `bynk.deploy.lock` records.
401    ///
402    /// Lists the instances through Cloudflare's API, so it needs
403    /// `CLOUDFLARE_API_TOKEN` (with "Workers Scripts: Edit") and
404    /// `CLOUDFLARE_ACCOUNT_ID`. Each Worker gets a temporary secret,
405    /// `BYNK_MIGRATE_KEY`, for the run; setting and deleting it each create a
406    /// Worker version.
407    Sweep {
408        /// Project directory. Defaults to the current directory.
409        #[arg(default_value = ".")]
410        path: PathBuf,
411        /// Sweep this context's classes alone (dotted name or Worker
412        /// directory). A floor rises only for the classes swept.
413        #[arg(long, value_name = "NAME")]
414        context: Option<String>,
415        /// Sweep this environment. Repeatable. Omit to sweep every environment
416        /// the ledger records; a narrower run migrates but raises no floor.
417        #[arg(long = "env", value_name = "NAME")]
418        environments: Vec<String>,
419        /// Instances woken per request to a Worker. The default keeps within
420        /// the Free plan's 50 subrequests per invocation.
421        #[arg(long, default_value_t = 40, value_parser = clap::value_parser!(u16).range(1..=1000))]
422        batch: u16,
423        /// Where a Worker that does not serve on `workers.dev` is reached, as
424        /// `<deployed Worker name>=https://<host>`. Repeatable.
425        #[arg(long = "url", value_name = "WORKER=URL")]
426        urls: Vec<String>,
427        /// Skip the confirmation. Required for non-interactive runs.
428        #[arg(long)]
429        yes: bool,
430    },
431}
432
433/// `bynk check --format` selector, mirroring `bynkc`'s `DiagFormat` (rich/short)
434/// so the two commands are drop-in equivalent.
435#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
436pub enum CheckFormatArg {
437    /// Ariadne rendering with full source context (the default).
438    #[default]
439    Rich,
440    /// One terse `path:line:col: severity[category]: message` line per
441    /// diagnostic — for the VS Code problem-matcher, CI, and scripts.
442    Short,
443}
444
445impl CheckFormatArg {
446    /// The `bynkc check --format` token this maps to when the pinned compiler
447    /// is shelled under `BYNK_BYNKC`.
448    pub fn as_bynkc_arg(self) -> &'static str {
449        match self {
450            CheckFormatArg::Rich => "rich",
451            CheckFormatArg::Short => "short",
452        }
453    }
454}
455
456/// `--only` selector. Mirrors [`Capability`] minus the internal distinctions.
457#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
458pub enum CapabilityArg {
459    /// `bynkc` compile / check / fmt.
460    Compile,
461    /// `bynk test` — Node + tsc|tsx.
462    Test,
463    /// dev / deploy to Cloudflare — Node + wrangler.
464    Deploy,
465    /// Editor support — bynkc-lsp.
466    Editor,
467    /// Build Bynk from source — a Rust toolchain.
468    Build,
469}
470
471impl From<CapabilityArg> for Capability {
472    fn from(a: CapabilityArg) -> Self {
473        match a {
474            CapabilityArg::Compile => Capability::Compile,
475            CapabilityArg::Test => Capability::Test,
476            CapabilityArg::Deploy => Capability::Deploy,
477            CapabilityArg::Editor => Capability::Editor,
478            CapabilityArg::Build => Capability::BuildFromSource,
479        }
480    }
481}
482
483#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
484pub enum FormatArg {
485    #[default]
486    Human,
487    Short,
488    Json,
489}
490
491/// Output choices for `bynk dev` (#2094).
492#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
493pub enum DevFormatArg {
494    /// The readable stream on stderr.
495    #[default]
496    Human,
497    /// Also one JSON lifecycle event per line on stdout.
498    Json,
499}
500
501impl From<DevFormatArg> for crate::dev::DevFormat {
502    fn from(f: DevFormatArg) -> Self {
503        match f {
504            DevFormatArg::Human => crate::dev::DevFormat::Human,
505            DevFormatArg::Json => crate::dev::DevFormat::Json,
506        }
507    }
508}
509
510/// Scriptable output choices for `bynk deploy`'s plan.
511#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
512pub enum DeployFormatArg {
513    #[default]
514    Short,
515    Json,
516}
517
518impl From<FormatArg> for Format {
519    fn from(f: FormatArg) -> Self {
520        match f {
521            FormatArg::Human => Format::Human,
522            FormatArg::Short => Format::Short,
523            FormatArg::Json => Format::Json,
524        }
525    }
526}
527
528/// Build the [`DoctorOptions`] from the parsed flags.
529pub fn doctor_options(only: Option<CapabilityArg>, strict: bool) -> DoctorOptions {
530    DoctorOptions {
531        only: only.map(Into::into),
532        strict,
533    }
534}
535
536/// #2240: how a command may change `bynk.lock` and the store.
537#[derive(Args, Debug, Clone, Copy, Default)]
538pub struct LockArgs {
539    /// Fail instead of changing `bynk.lock` — for CI, which should build
540    /// exactly what was committed.
541    #[arg(long)]
542    pub locked: bool,
543    /// Fail instead of fetching: every git dependency must already be in the
544    /// store.
545    #[arg(long)]
546    pub offline: bool,
547}
548
549impl LockArgs {
550    /// The fetch options these flags ask for.
551    pub fn options(self) -> crate::fetch::FetchOptions {
552        crate::fetch::FetchOptions {
553            locked: self.locked,
554            offline: self.offline,
555            update: None,
556        }
557    }
558}
559
560/// #2378: which registry an account command talks to.
561#[derive(Args, Debug, Clone)]
562pub struct AccountTarget {
563    /// The registry, by its `[registries]` name: by default, the one the
564    /// organisation routes to (the first-party registry for `login` and
565    /// `logout`).
566    #[arg(long)]
567    pub registry: Option<String>,
568    /// The project whose `[registries]` name and route the registry.
569    #[arg(long, default_value = ".")]
570    pub path: PathBuf,
571}
572
573/// #2378: `bynk org` subcommands.
574#[derive(Subcommand, Debug)]
575pub enum OrgCommand {
576    /// Claim an organisation, first come: you become its owner.
577    Claim {
578        org: String,
579        /// Hold the name without publishing: every publish is refused.
580        #[arg(long)]
581        block: bool,
582        /// Claim it for this GitHub user (an operator token only).
583        #[arg(long)]
584        owner: Option<String>,
585        #[command(flatten)]
586        target: AccountTarget,
587    },
588    /// Show an organisation's owner, members, whether it is blocked, and its
589    /// publish tokens (never the tokens themselves).
590    Show {
591        org: String,
592        /// Output format.
593        #[arg(long, value_enum, default_value_t)]
594        format: DeployFormatArg,
595        #[command(flatten)]
596        target: AccountTarget,
597    },
598    /// Block an organisation: every publish to it is refused.
599    Block {
600        org: String,
601        #[command(flatten)]
602        target: AccountTarget,
603    },
604    /// Unblock an organisation.
605    Unblock {
606        org: String,
607        #[command(flatten)]
608        target: AccountTarget,
609    },
610    /// Add or remove members.
611    Member {
612        #[command(subcommand)]
613        command: MemberCommand,
614    },
615    /// Mint or revoke publish tokens.
616    Token {
617        #[command(subcommand)]
618        command: OrgTokenCommand,
619    },
620}
621
622/// #2378: `bynk org member` subcommands.
623#[derive(Subcommand, Debug)]
624pub enum MemberCommand {
625    /// Add a GitHub user as a member: they may mint publish tokens.
626    Add {
627        org: String,
628        /// Their GitHub login.
629        login: String,
630        #[command(flatten)]
631        target: AccountTarget,
632    },
633    /// Remove a member, and every token they minted.
634    Remove {
635        org: String,
636        /// Their GitHub login, or numeric id.
637        member: String,
638        #[command(flatten)]
639        target: AccountTarget,
640    },
641}
642
643/// #2378: `bynk org token` subcommands.
644#[derive(Subcommand, Debug)]
645pub enum OrgTokenCommand {
646    /// Mint a publish token, shown once on stdout (or saved with `--save`).
647    Mint {
648        org: String,
649        /// What it may do: `publish`, `yank` (repeatable or comma-separated;
650        /// both by default).
651        #[arg(long = "can", value_delimiter = ',', default_values = ["publish", "yank"])]
652        can: Vec<String>,
653        /// How many days it lasts (1 to 365).
654        #[arg(long, default_value_t = 90)]
655        days: u32,
656        /// A note to recognise it by in `bynk org show`.
657        #[arg(long, default_value = "")]
658        note: String,
659        /// Save it as `[registries.<name>] token` in the credentials file,
660        /// where `bynk publish` reads it, instead of printing it.
661        #[arg(long)]
662        save: bool,
663        /// Let `--save` replace a different token.
664        #[arg(long, requires = "save")]
665        force: bool,
666        #[command(flatten)]
667        target: AccountTarget,
668    },
669    /// Revoke a publish token, by the id `bynk org show` prints.
670    Revoke {
671        org: String,
672        /// The token's id, or a unique prefix of it.
673        id: String,
674        #[command(flatten)]
675        target: AccountTarget,
676    },
677}
678
679/// #2274: `bynk registry` subcommands.
680#[derive(Subcommand, Debug)]
681pub enum RegistryCommand {
682    /// Create a registry: its `config.json`, and a signing key written outside
683    /// the directory. Prints the `key = "…"` line consumers pin.
684    Init {
685        /// The registry directory to create.
686        dir: PathBuf,
687        /// The registry's id, which every signed line binds.
688        #[arg(long)]
689        id: String,
690        /// Where to write the signing key (default `./<id>.key`).
691        #[arg(long = "key-out")]
692        key_out: Option<PathBuf>,
693    },
694    /// Pack a package, sign its index line, and add it to the registry.
695    Add {
696        /// The registry directory.
697        dir: PathBuf,
698        /// The package's directory (holding its `bynk.toml`).
699        package: PathBuf,
700        /// The registry's signing key.
701        #[arg(long)]
702        key: PathBuf,
703    },
704    /// Serve a registry directory with the publish API (#2287): plain HTTP,
705    /// loopback by default — put a TLS-terminating proxy in front to serve
706    /// beyond this machine.
707    Serve {
708        /// The registry directory.
709        dir: PathBuf,
710        /// The registry's signing key (never inside the directory).
711        #[arg(long)]
712        key: PathBuf,
713        /// The tokens file `bynk registry token` writes (never inside the
714        /// directory).
715        #[arg(long)]
716        tokens: PathBuf,
717        /// The address to listen on.
718        #[arg(long, default_value = crate::registry::serve::DEFAULT_LISTEN)]
719        listen: String,
720    },
721    /// Append a link to the registry's key chain (#2316): a new key, signed
722    /// by the current chain key. Clients that trust an older key follow it.
723    Rotate {
724        /// The registry directory (or a copy holding its `config.json`).
725        dir: PathBuf,
726        /// The current (newest) chain key.
727        #[arg(long)]
728        key: PathBuf,
729        /// Where to write the new key (never inside the directory).
730        #[arg(long)]
731        key_out: PathBuf,
732    },
733    /// Delegate signing to a new key (#2316): it may sign index lines, and
734    /// nothing else. Use it as `serve`'s or `add`'s `--key`, keeping the chain
735    /// key offline.
736    Delegate {
737        /// The registry directory (or a copy holding its `config.json`).
738        dir: PathBuf,
739        /// A chain key, which signs the delegation.
740        #[arg(long)]
741        key: PathBuf,
742        /// Where to write the delegated key (never inside the directory).
743        #[arg(long)]
744        delegate_out: PathBuf,
745    },
746    /// Revoke a delegated key (#2316): lines it signed are refused from then
747    /// on, so re-sign them with another key first (`resign`).
748    Revoke {
749        /// The registry directory (or a copy holding its `config.json`).
750        dir: PathBuf,
751        /// The delegate to revoke, `ed25519:…`.
752        target: String,
753        /// A chain key, which signs the revocation.
754        #[arg(long)]
755        key: PathBuf,
756        /// Revoke even if some lines would be left with no valid signer.
757        #[arg(long)]
758        force: bool,
759    },
760    /// Re-sign every index line with a key (#2316): the newest chain key or a
761    /// valid delegate. Only each line's `sig` changes.
762    Resign {
763        /// The registry directory.
764        dir: PathBuf,
765        /// The signing key.
766        #[arg(long)]
767        key: PathBuf,
768    },
769    /// Check a registry against the protocol (#2311): every observable rule a
770    /// server must keep, run against its URL. A full run publishes one
771    /// throwaway package under `--org` and leaves it yanked; `--read-only`
772    /// publishes nothing.
773    Conformance {
774        /// The registry's index URL.
775        index: String,
776        /// The organisation the token may publish and yank under.
777        #[arg(long, required_unless_present = "read_only")]
778        org: Option<String>,
779        /// The registry name the token is found under
780        /// (`BYNK_REGISTRY_<NAME>_TOKEN`, or the credentials file).
781        #[arg(long, default_value = "conformance")]
782        name: String,
783        /// The registry's key to pin (`ed25519:…`); trusted on first use for
784        /// the run otherwise.
785        #[arg(long)]
786        key: Option<String>,
787        /// Publish nothing: check `config.json`, and `--package` if given.
788        #[arg(long)]
789        read_only: bool,
790        /// The existing `org/package` a `--read-only` run reads back.
791        #[arg(long, requires = "read_only")]
792        package: Option<String>,
793        /// An environment variable holding a token that can't act for
794        /// `--org`, for the `auth.scope` check (skipped without one).
795        #[arg(long)]
796        narrow_token_env: Option<String>,
797        /// The upload cap, in bytes, to test `413` against.
798        #[arg(long, default_value_t = 64 * 1024 * 1024)]
799        max_upload: u64,
800        /// Output format.
801        #[arg(long, value_enum, default_value_t)]
802        format: DeployFormatArg,
803    },
804    /// Mint a publish token for `bynk registry serve` (#2287): printed once;
805    /// only its hash is stored in the tokens file.
806    Token {
807        /// The tokens file to add it to.
808        tokens: PathBuf,
809        /// An organisation it may act for (repeatable).
810        #[arg(long = "org", required = true)]
811        organisations: Vec<String>,
812        /// What it may do: `publish`, `yank` (repeatable or comma-separated; both by default).
813        #[arg(long = "can", value_delimiter = ',', default_values = ["publish", "yank"])]
814        can: Vec<String>,
815    },
816    /// Mark a version yanked — kept for locked builds, never newly chosen.
817    Yank {
818        /// The registry directory.
819        dir: PathBuf,
820        /// `org/package`.
821        package: String,
822        /// The version to yank.
823        version: String,
824        /// Clear the yank instead.
825        #[arg(long)]
826        undo: bool,
827    },
828    /// Withdraw a version (#2330): a takedown. Yanks its line, records the
829    /// reason clients print, and deletes its archive. Final — there is no undo.
830    Withdraw {
831        /// The registry directory.
832        dir: PathBuf,
833        /// `org/package`.
834        package: String,
835        /// The version to withdraw.
836        version: String,
837        /// Why, as clients will print it (one line, at most 500 characters).
838        #[arg(long)]
839        reason: String,
840    },
841}