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}