1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//! The `bynk` driver command-line interface.
//!
//! The developer front-end: `doctor` / `new` / `dev`, plus the everyday
//! `check` / `fmt` / `test` (v0.138, #487). `check` and `fmt` run the linked
//! pipeline in-process; `test` delegates to the driver-resolved `bynkc`. The
//! flag surfaces mirror `bynkc`'s so the two are drop-in equivalent.
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use crate::doctor::{Capability, DoctorOptions};
use crate::report::Format;
#[derive(Parser, Debug)]
#[command(name = "bynk", version, about = "The Bynk driver", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Check whether your machine is ready to compile, test, and deploy Bynk —
/// and print the exact remedy for anything missing.
///
/// Bare `bynk doctor` is informational: it surveys every capability and
/// exits 0 unless `bynkc` itself is unusable. `--only <capability>` gates on
/// one capability (exits non-zero if its tools are missing); `--strict`
/// turns every warning into a failure, for CI.
Doctor {
/// Project directory to inspect (for project-local `node_modules/.bin`
/// resolution). Defaults to the current directory.
#[arg(default_value = ".")]
input: PathBuf,
/// Scope the check — and the exit code — to one capability.
#[arg(long, value_enum)]
only: Option<CapabilityArg>,
/// Treat every warning (optional gaps, npx provisionability, minor
/// version skew) as a failure. For an all-green CI gate.
#[arg(long)]
strict: bool,
/// Output format. `human` (default) is a grouped table; `short` and
/// `json` are the stable scriptable surface.
#[arg(long, value_enum, default_value = "human")]
format: FormatArg,
},
/// Build the project and serve it locally with `wrangler dev` — one step in
/// place of the manual compile + `cd` + `wrangler dev` recipe.
///
/// Compiles into a managed `.bynk/dev/` build dir, picks the worker to serve
/// (one context → served; `--context` to choose; ambiguous → lists them),
/// and runs `wrangler dev` from inside it in local mode (Miniflare) — no
/// namespace provisioning needed. Everything after `--` is forwarded to
/// `wrangler dev` verbatim.
Dev {
/// Project directory to serve from (anywhere inside the project; the
/// root is found by walking up for `bynk.toml`). Defaults to `.`.
#[arg(default_value = ".")]
path: PathBuf,
/// Which context's worker to serve, for multi-context projects.
#[arg(long)]
context: Option<String>,
/// Serve with the V8 inspector enabled (slice 3, ADR 0104): `wrangler dev`
/// starts with `--inspector-port` so a JavaScript debugger can attach.
/// Breakpoints set in `.bynk` sources resolve through the emitted source
/// maps, composed into the worker bundle. Prints the inspector URL on start.
#[arg(long)]
inspect: bool,
/// Inspector port for `--inspect` (default 9229).
#[arg(long, default_value_t = 9229)]
inspect_port: u16,
/// Arguments after `--`, forwarded to `wrangler dev` (e.g. `-- --port 8788`).
#[arg(last = true)]
wrangler_args: Vec<String>,
},
/// Scaffold a new project: a complete, runnable single-context HTTP service
/// you can serve immediately with `bynk dev`.
///
/// Writes a `bynk.toml`, a `.gitignore`, and `src/<name>.bynk` into a new
/// directory. Pure offline file-writing — it shells nothing and needs no
/// toolchain, so you can run it before `bynkc`, Node, or `wrangler` are
/// installed. The project name defaults to the target directory's final
/// component; `--name` overrides it and must be a legal Bynk identifier.
New {
/// Directory to create for the new project (e.g. `hello` or `./hello`).
path: PathBuf,
/// Project name / context identifier. Defaults to PATH's final
/// component; must be a legal Bynk identifier (a letter followed by
/// letters, digits, or underscores).
#[arg(long)]
name: Option<String>,
},
/// Type-check a `.bynk` file or project without writing output — the
/// `bynkc check` behaviour through the driver's compiler resolution (v0.138).
///
/// Runs the compiler pipeline in-process (no `bynkc` binary required); with
/// `BYNK_BYNKC` set, the pinned compiler is shelled instead so an
/// externally-managed `bynkc` still governs the result.
Check {
/// Input `.bynk` file or project root. Defaults to the current directory.
#[arg(default_value = ".")]
input: PathBuf,
/// Diagnostic output format. `rich` (default) is the ariadne
/// source-context rendering; `short` emits one terse
/// `path:line:col: severity[category]: message` line per diagnostic,
/// for tooling (the VS Code problem-matcher, CI, scripts).
#[arg(long, value_enum, default_value = "rich")]
format: CheckFormatArg,
},
/// Format `.bynk` source files in place — the `bynkc fmt` behaviour through
/// the driver (v0.138). Passing `-` reads from stdin and writes to stdout.
///
/// Runs the formatter in-process (no `bynkc` binary required); with
/// `BYNK_BYNKC` set, the pinned compiler is shelled instead.
Fmt {
/// Files to format. Use `-` for stdin → stdout.
inputs: Vec<PathBuf>,
/// Check formatting without writing changes. Exits non-zero if any
/// file is not already canonical.
#[arg(long)]
check: bool,
},
/// Discover and run test declarations in a project — the `bynkc test`
/// behaviour through the driver (v0.138).
///
/// Delegates to the `bynkc` the driver resolves (`BYNK_BYNKC` → PATH →
/// sibling-of-`bynk`), so an editor or developer inherits the driver's
/// richer compiler resolution instead of locating `bynkc` themselves.
/// Requires `tsc` (with Node.js) or `tsx` on PATH, exactly as `bynkc test`.
Test {
/// Input project root directory. Defaults to the current directory.
#[arg(default_value = ".")]
input: PathBuf,
/// Where to write compiled TypeScript test runner modules.
/// Defaults to `<input>/out`.
#[arg(short, long)]
output: Option<PathBuf>,
/// Skip the runner invocation. With `--format rich` this emits the
/// generated test files; with `--format json` it emits a discovery
/// document listing every suite and case without running them.
#[arg(long)]
no_run: bool,
/// Output format. `rich` (default) is the grouped ✓ / ✗ human output;
/// `json` is a single pinned JSON document of results, for tooling.
#[arg(long, value_enum, default_value = "rich")]
format: TestFormatArg,
/// Compile a debug build and launch the test runner under Node's
/// inspector (`node --inspect-brk`), printing the inspector URL for a
/// JavaScript debugger to attach. Requires Node ≥ 22.18 (or ≥ 23.6
/// unflagged). Does not run `tsc`.
#[arg(long)]
inspect: bool,
/// The root seed for generative `property` tests, as hex (e.g.
/// `0x5f3a`). A failing property prints the seed it used; re-running
/// with `--seed <hex>` reproduces that run byte-for-byte.
#[arg(long)]
seed: Option<String>,
/// Run only test cases whose name matches `<name>`, skipping the rest —
/// the filter behind the editor's per-case `▷ Run Test` lens. No effect
/// with `--no-run`.
#[arg(long, value_name = "NAME")]
case: Option<String>,
},
}
/// `bynk check --format` selector, mirroring `bynkc`'s `DiagFormat` (rich/short)
/// so the two commands are drop-in equivalent.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
pub enum CheckFormatArg {
/// Ariadne rendering with full source context (the default).
#[default]
Rich,
/// One terse `path:line:col: severity[category]: message` line per
/// diagnostic — for the VS Code problem-matcher, CI, and scripts.
Short,
}
/// `bynk test --format` selector, mirroring `bynkc`'s `TestFormat` (rich/json).
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
pub enum TestFormatArg {
/// The grouped `✓ / ✗` human output (the default).
#[default]
Rich,
/// A single pinned JSON document of results, for tooling and CI.
Json,
}
impl CheckFormatArg {
/// The `bynkc check --format` token this maps to when the pinned compiler
/// is shelled under `BYNK_BYNKC`.
pub fn as_bynkc_arg(self) -> &'static str {
match self {
CheckFormatArg::Rich => "rich",
CheckFormatArg::Short => "short",
}
}
}
impl TestFormatArg {
/// The `bynkc test --format` token this maps to when `bynk test` shells the
/// resolved compiler.
pub fn as_bynkc_arg(self) -> &'static str {
match self {
TestFormatArg::Rich => "rich",
TestFormatArg::Json => "json",
}
}
}
/// `--only` selector. Mirrors [`Capability`] minus the internal distinctions.
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum CapabilityArg {
/// `bynkc` compile / check / fmt.
Compile,
/// `bynk test` — Node + tsc|tsx.
Test,
/// dev / deploy to Cloudflare — Node + wrangler.
Deploy,
/// Editor support — bynkc-lsp.
Editor,
/// Build Bynk from source — a Rust toolchain.
Build,
}
impl From<CapabilityArg> for Capability {
fn from(a: CapabilityArg) -> Self {
match a {
CapabilityArg::Compile => Capability::Compile,
CapabilityArg::Test => Capability::Test,
CapabilityArg::Deploy => Capability::Deploy,
CapabilityArg::Editor => Capability::Editor,
CapabilityArg::Build => Capability::BuildFromSource,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
pub enum FormatArg {
#[default]
Human,
Short,
Json,
}
impl From<FormatArg> for Format {
fn from(f: FormatArg) -> Self {
match f {
FormatArg::Human => Format::Human,
FormatArg::Short => Format::Short,
FormatArg::Json => Format::Json,
}
}
}
/// Build the [`DoctorOptions`] from the parsed flags.
pub fn doctor_options(only: Option<CapabilityArg>, strict: bool) -> DoctorOptions {
DoctorOptions {
only: only.map(Into::into),
strict,
}
}