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        /// In both, `path` is the file as you'd type it from the working
189        /// directory (the input you passed, joined with the file's place in
190        /// it), the same path `fmt` reports.
191        #[arg(long, value_enum, default_value = "rich")]
192        format: CheckFormatArg,
193    },
194    /// Format `.bynk` source files in place — the `bynkc fmt` behaviour through
195    /// the driver (v0.138). Passing `-` reads from stdin and writes to stdout.
196    ///
197    /// Runs the formatter in-process (no `bynkc` binary required); with
198    /// `BYNK_BYNKC` set, the pinned compiler is shelled instead — the style
199    /// flags are forwarded to it either way.
200    ///
201    /// `--indent`, `--indent-width`, `--max-line-width` and
202    /// `--no-trailing-comma` override the canonical style for this run; with
203    /// none of them the output is the canonical formatting.
204    Fmt {
205        #[command(flatten)]
206        args: bynk_driver::FmtArgs,
207    },
208    /// Discover and run test declarations in a project — the `bynkc test`
209    /// behaviour through the driver (v0.138).
210    ///
211    /// Delegates to the `bynkc` the driver resolves (`BYNK_BYNKC` → PATH →
212    /// sibling-of-`bynk`), so an editor or developer inherits the driver's
213    /// richer compiler resolution instead of locating `bynkc` themselves.
214    /// Requires `tsc` (with Node.js) or `tsx` on PATH, exactly as `bynkc test`.
215    Test {
216        #[command(flatten)]
217        args: bynk_driver::test_runner::TestArgs,
218        /// Run even when the resolved `bynkc` is a different *major* version
219        /// from `bynk` (#1675). A minor skew only warns. Also settable as
220        /// `BYNK_ALLOW_SKEW=1`.
221        #[arg(long)]
222        allow_skew: bool,
223    },
224    /// Explain a diagnostic code — the longer-form "what the rule is, why it
225    /// exists, and how to fix it" behind a `bynk.*` error code (#853).
226    ///
227    /// Prints a curated blurb, a minimal before/after example, and a link to
228    /// the relevant Book concept page. The blurb is offline-complete, so the
229    /// substance is available without network. Codes without a curated
230    /// explanation report that gracefully; an unrecognised code exits non-zero.
231    /// The same explanations back the editor's clickable diagnostic-code links.
232    Explain {
233        /// The diagnostic code to explain, e.g. `bynk.resolve.unknown_type`.
234        #[arg(value_name = "CODE")]
235        code: String,
236    },
237}
238
239/// `bynk check --format` selector, mirroring `bynkc`'s `DiagFormat` (rich/short)
240/// so the two commands are drop-in equivalent.
241#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
242pub enum CheckFormatArg {
243    /// Ariadne rendering with full source context (the default).
244    #[default]
245    Rich,
246    /// One terse `path:line:col: severity[category]: message` line per
247    /// diagnostic — for the VS Code problem-matcher, CI, and scripts.
248    Short,
249}
250
251impl CheckFormatArg {
252    /// The `bynkc check --format` token this maps to when the pinned compiler
253    /// is shelled under `BYNK_BYNKC`.
254    pub fn as_bynkc_arg(self) -> &'static str {
255        match self {
256            CheckFormatArg::Rich => "rich",
257            CheckFormatArg::Short => "short",
258        }
259    }
260}
261
262/// `--only` selector. Mirrors [`Capability`] minus the internal distinctions.
263#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
264pub enum CapabilityArg {
265    /// `bynkc` compile / check / fmt.
266    Compile,
267    /// `bynk test` — Node + tsc|tsx.
268    Test,
269    /// dev / deploy to Cloudflare — Node + wrangler.
270    Deploy,
271    /// Editor support — bynkc-lsp.
272    Editor,
273    /// Build Bynk from source — a Rust toolchain.
274    Build,
275}
276
277impl From<CapabilityArg> for Capability {
278    fn from(a: CapabilityArg) -> Self {
279        match a {
280            CapabilityArg::Compile => Capability::Compile,
281            CapabilityArg::Test => Capability::Test,
282            CapabilityArg::Deploy => Capability::Deploy,
283            CapabilityArg::Editor => Capability::Editor,
284            CapabilityArg::Build => Capability::BuildFromSource,
285        }
286    }
287}
288
289#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
290pub enum FormatArg {
291    #[default]
292    Human,
293    Short,
294    Json,
295}
296
297/// Scriptable output choices for `bynk deploy`'s plan.
298#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
299pub enum DeployFormatArg {
300    #[default]
301    Short,
302    Json,
303}
304
305impl From<FormatArg> for Format {
306    fn from(f: FormatArg) -> Self {
307        match f {
308            FormatArg::Human => Format::Human,
309            FormatArg::Short => Format::Short,
310            FormatArg::Json => Format::Json,
311        }
312    }
313}
314
315/// Build the [`DoctorOptions`] from the parsed flags.
316pub fn doctor_options(only: Option<CapabilityArg>, strict: bool) -> DoctorOptions {
317    DoctorOptions {
318        only: only.map(Into::into),
319        strict,
320    }
321}