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::{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        /// Which context to serve, repeatable. Omit to serve every context in
67        /// the project with the service bindings between them wired (#552);
68        /// pass one or more to narrow to a subset. Accepts the dotted name or
69        /// its dasherised worker-dir form.
70        #[arg(long = "context", value_name = "NAME")]
71        contexts: Vec<String>,
72        /// First port of the per-context allocation (context *i* gets
73        /// `--base-port` + *i*, in sorted order). Defaults to wrangler's 8787.
74        /// A single context left on the default keeps `-- --port N` working.
75        #[arg(long, value_name = "PORT")]
76        base_port: Option<u16>,
77        /// Serve with the V8 inspector enabled (slice 3, ADR 0104): `wrangler dev`
78        /// starts with `--inspector-port` so a JavaScript debugger can attach.
79        /// Breakpoints set in `.bynk` sources resolve through the emitted source
80        /// maps, composed into the worker bundle. Prints the inspector URL on start.
81        #[arg(long)]
82        inspect: bool,
83        /// First inspector port for `--inspect`, allocated per context exactly
84        /// as `--base-port` is (default 9229).
85        #[arg(long, default_value_t = 9229)]
86        inspect_port: u16,
87        /// Which `bynk.deploy.lock` environment `-- --remote` reads the KV id
88        /// from. `dev` never provisions, so this only selects among what
89        /// `bynk deploy --env NAME` already recorded — irrelevant without
90        /// `--remote`. Omit for today's single, unqualified default.
91        #[arg(long, default_value = "default", value_name = "NAME")]
92        env: String,
93        /// Arguments after `--`, forwarded to `wrangler dev` (e.g. `-- --remote`).
94        /// Ports are the driver's to allocate: use `--base-port` / `--inspect-port`.
95        #[arg(last = true)]
96        wrangler_args: Vec<String>,
97    },
98    /// Provision each context's Cloudflare resources and deploy its Worker.
99    /// The whole project ships in one command, in Service-Binding dependency
100    /// order — Cloudflare rejects a Worker uploaded before a Worker it binds
101    /// to. The generated configuration remains disposable: Cloudflare ids live
102    /// in the committed `bynk.deploy.lock`.
103    Deploy {
104        /// Project directory to deploy from. Defaults to the current directory.
105        #[arg(default_value = ".")]
106        path: PathBuf,
107        /// Deploy this context alone, assuming the contexts it consumes are
108        /// already live; a dependency that has never been deployed is reported
109        /// rather than pushed into. Accepts the dotted name or its dasherised
110        /// worker-dir form. Omit to deploy the whole project in order.
111        #[arg(long, value_name = "NAME")]
112        context: Option<String>,
113        /// Target environment. Selects the `bynk.deploy.lock` section and,
114        /// for any value other than `default`, synthesises an environment-
115        /// scoped Wrangler config section (KV, queues, Service Bindings all
116        /// qualified) since Cloudflare does not inherit bindings into a named
117        /// environment. Omit to deploy today's single, unqualified default.
118        #[arg(long, default_value = "default", value_name = "NAME")]
119        env: String,
120        /// Print the provisioning and deploy plan without changing Cloudflare
121        /// or writing `bynk.deploy.lock`.
122        #[arg(long, visible_alias = "plan")]
123        dry_run: bool,
124        /// Plan output format. `short` is line-oriented; `json` is for CI.
125        #[arg(long, value_enum, default_value = "short")]
126        format: DeployFormatArg,
127        /// Skip the confirmation required before creating a namespace or
128        /// publishing a Worker. Required for non-interactive automation.
129        #[arg(long)]
130        yes: bool,
131        /// Read secret values from a dotenv-style `NAME=value` file. Supplies
132        /// both names and values; never committed, never persisted. Values move
133        /// to `wrangler secret put` and are dropped.
134        #[arg(long, value_name = "PATH")]
135        secrets_file: Option<PathBuf>,
136        /// Set this named secret, taking its value from the environment (or a
137        /// prompt). Repeatable. Use for a `bynk.Secrets` name, whose spelling
138        /// the compiler cannot know — an actor's declared `auth` secret needs no
139        /// flag. The environment is never scanned for names.
140        #[arg(long = "secret", value_name = "NAME")]
141        secrets: Vec<String>,
142        /// Overwrite a secret that is already set. The default sets only the
143        /// missing ones, so a re-deploy does not cut a fresh Cloudflare secret
144        /// version for every secret every time.
145        #[arg(long)]
146        force: bool,
147        /// Delete every reported orphan — a KV namespace or queue the ledger
148        /// remembers that the current build no longer declares. Never
149        /// deletes a Worker. Prompts separately from the creation
150        /// confirmation unless --yes is also given; omit to report only.
151        #[arg(long)]
152        prune: bool,
153        /// Arguments after `--`, forwarded to `wrangler deploy` verbatim.
154        #[arg(last = true)]
155        wrangler_args: Vec<String>,
156    },
157    /// Scaffold a new project: a complete, runnable single-context HTTP service
158    /// you can serve immediately with `bynk dev`.
159    ///
160    /// Writes a `bynk.toml`, a `.gitignore`, and `src/<name>.bynk` into a new
161    /// directory. Pure offline file-writing — it shells nothing and needs no
162    /// toolchain, so you can run it before `bynkc`, Node, or `wrangler` are
163    /// installed. The project name defaults to the target directory's final
164    /// component; `--name` overrides it and must be a legal Bynk identifier.
165    New {
166        /// Directory to create for the new project (e.g. `hello` or `./hello`).
167        path: PathBuf,
168        /// Project name / context identifier. Defaults to PATH's final
169        /// component; must be a legal Bynk identifier (a letter followed by
170        /// letters, digits, or underscores).
171        #[arg(long)]
172        name: Option<String>,
173    },
174    /// Type-check a `.bynk` file or project without writing output — the
175    /// `bynkc check` behaviour through the driver's compiler resolution (v0.138).
176    ///
177    /// Runs the compiler pipeline in-process (no `bynkc` binary required); with
178    /// `BYNK_BYNKC` set, the pinned compiler is shelled instead so an
179    /// externally-managed `bynkc` still governs the result.
180    Check {
181        /// Input `.bynk` file or project root. Defaults to the current directory.
182        #[arg(default_value = ".")]
183        input: PathBuf,
184        /// Diagnostic output format. `rich` (default) is the ariadne
185        /// source-context rendering; `short` emits one terse
186        /// `path:line:col: severity[category]: message` line per diagnostic,
187        /// for tooling (the VS Code problem-matcher, CI, scripts).
188        #[arg(long, value_enum, default_value = "rich")]
189        format: CheckFormatArg,
190    },
191    /// Format `.bynk` source files in place — the `bynkc fmt` behaviour through
192    /// the driver (v0.138). Passing `-` reads from stdin and writes to stdout.
193    ///
194    /// Runs the formatter in-process (no `bynkc` binary required); with
195    /// `BYNK_BYNKC` set, the pinned compiler is shelled instead — the style
196    /// flags are forwarded to it either way.
197    ///
198    /// `--indent`, `--indent-width`, `--max-line-width` and
199    /// `--no-trailing-comma` override the canonical style for this run; with
200    /// none of them the output is the canonical formatting.
201    Fmt {
202        #[command(flatten)]
203        args: bynk_driver::FmtArgs,
204    },
205    /// Discover and run test declarations in a project — the `bynkc test`
206    /// behaviour through the driver (v0.138).
207    ///
208    /// Delegates to the `bynkc` the driver resolves (`BYNK_BYNKC` → PATH →
209    /// sibling-of-`bynk`), so an editor or developer inherits the driver's
210    /// richer compiler resolution instead of locating `bynkc` themselves.
211    /// Requires `tsc` (with Node.js) or `tsx` on PATH, exactly as `bynkc test`.
212    Test {
213        #[command(flatten)]
214        args: bynk_driver::test_runner::TestArgs,
215    },
216    /// Explain a diagnostic code — the longer-form "what the rule is, why it
217    /// exists, and how to fix it" behind a `bynk.*` error code (#853).
218    ///
219    /// Prints a curated blurb, a minimal before/after example, and a link to
220    /// the relevant Book concept page. The blurb is offline-complete, so the
221    /// substance is available without network. Codes without a curated
222    /// explanation report that gracefully; an unrecognised code exits non-zero.
223    /// The same explanations back the editor's clickable diagnostic-code links.
224    Explain {
225        /// The diagnostic code to explain, e.g. `bynk.resolve.unknown_type`.
226        #[arg(value_name = "CODE")]
227        code: String,
228    },
229}
230
231/// `bynk check --format` selector, mirroring `bynkc`'s `DiagFormat` (rich/short)
232/// so the two commands are drop-in equivalent.
233#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
234pub enum CheckFormatArg {
235    /// Ariadne rendering with full source context (the default).
236    #[default]
237    Rich,
238    /// One terse `path:line:col: severity[category]: message` line per
239    /// diagnostic — for the VS Code problem-matcher, CI, and scripts.
240    Short,
241}
242
243impl CheckFormatArg {
244    /// The `bynkc check --format` token this maps to when the pinned compiler
245    /// is shelled under `BYNK_BYNKC`.
246    pub fn as_bynkc_arg(self) -> &'static str {
247        match self {
248            CheckFormatArg::Rich => "rich",
249            CheckFormatArg::Short => "short",
250        }
251    }
252}
253
254/// `--only` selector. Mirrors [`Capability`] minus the internal distinctions.
255#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
256pub enum CapabilityArg {
257    /// `bynkc` compile / check / fmt.
258    Compile,
259    /// `bynk test` — Node + tsc|tsx.
260    Test,
261    /// dev / deploy to Cloudflare — Node + wrangler.
262    Deploy,
263    /// Editor support — bynkc-lsp.
264    Editor,
265    /// Build Bynk from source — a Rust toolchain.
266    Build,
267}
268
269impl From<CapabilityArg> for Capability {
270    fn from(a: CapabilityArg) -> Self {
271        match a {
272            CapabilityArg::Compile => Capability::Compile,
273            CapabilityArg::Test => Capability::Test,
274            CapabilityArg::Deploy => Capability::Deploy,
275            CapabilityArg::Editor => Capability::Editor,
276            CapabilityArg::Build => Capability::BuildFromSource,
277        }
278    }
279}
280
281#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
282pub enum FormatArg {
283    #[default]
284    Human,
285    Short,
286    Json,
287}
288
289/// Scriptable output choices for `bynk deploy`'s plan.
290#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
291pub enum DeployFormatArg {
292    #[default]
293    Short,
294    Json,
295}
296
297impl From<FormatArg> for Format {
298    fn from(f: FormatArg) -> Self {
299        match f {
300            FormatArg::Human => Format::Human,
301            FormatArg::Short => Format::Short,
302            FormatArg::Json => Format::Json,
303        }
304    }
305}
306
307/// Build the [`DoctorOptions`] from the parsed flags.
308pub fn doctor_options(only: Option<CapabilityArg>, strict: bool) -> DoctorOptions {
309    DoctorOptions {
310        only: only.map(Into::into),
311        strict,
312    }
313}