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}