bougie_cli/lib.rs
1use clap::builder::styling::{AnsiColor, Effects, Styles};
2use clap::{Args, Parser, Subcommand};
3use std::ffi::OsString;
4
5/// Full version string, uv-style: `0.6.4 (63c5f57d3 2026-05-08 <target>)`.
6///
7/// Built by `build.rs`; degrades to the bare crate version when git metadata is
8/// unavailable. clap prefixes the binary name, so `--version` prints
9/// `bougie 0.6.4 (...)`.
10pub const LONG_VERSION: &str = env!("BOUGIE_LONG_VERSION");
11
12const HELP_STYLES: Styles = Styles::styled()
13 .header(AnsiColor::Blue.on_default().effects(Effects::BOLD))
14 .usage(AnsiColor::Magenta.on_default().effects(Effects::BOLD))
15 .literal(AnsiColor::BrightMagenta.on_default().effects(Effects::BOLD))
16 .placeholder(AnsiColor::BrightMagenta.on_default())
17 .error(AnsiColor::Red.on_default().effects(Effects::BOLD))
18 .valid(AnsiColor::Green.on_default())
19 .invalid(AnsiColor::Yellow.on_default().effects(Effects::BOLD));
20
21/// Grouped quick-reference appended to `bougie --help`. clap renders all
22/// subcommands in one flat "Commands:" list (it has no headed-group
23/// support), and `display_order` clusters that list into these same
24/// groups — this cheat-sheet just names the groups so the core verbs are
25/// findable at a glance.
26const COMMAND_GROUPS: &str = "\
27Command groups:
28 Project init, new, start, stop, run, sync, make, format
29 Dependencies add, remove, lock, tree, outdated, ext, composer
30 Toolchain php, node, tool
31 Services server, services, projects
32 Admin cache, self
33
34Run `bougie help <command>` for details on any command.";
35
36#[derive(Parser, Debug)]
37#[command(
38 name = "bougie",
39 version = LONG_VERSION,
40 about,
41 long_about = None,
42 styles = HELP_STYLES,
43 after_long_help = COMMAND_GROUPS,
44)]
45pub struct Cli {
46 #[command(subcommand)]
47 pub command: Command,
48
49 /// Suppress non-error output.
50 #[arg(short, long, global = true)]
51 pub quiet: bool,
52
53 /// Verbose output.
54 #[arg(short, long, global = true)]
55 pub verbose: bool,
56
57 /// Output format.
58 #[arg(long, global = true, default_value = "text")]
59 pub format: OutputFormat,
60}
61
62/// Shared PHP-source preference flags (uv's system-Python model adapted
63/// to PHP). Flattened into `sync` / `run`; `--managed-php` and
64/// `--no-managed-php` are mutually exclusive. With none set, bougie's
65/// default applies: prefer an installed managed PHP, then a qualifying
66/// system PHP, then download a managed one.
67#[derive(Args, Debug, Clone, Copy, Default)]
68pub struct PhpPrefArgs {
69 /// Only use a bougie-managed PHP; never a system PHP.
70 #[arg(long, conflicts_with = "no_managed_php")]
71 pub managed_php: bool,
72 /// Only use a system PHP already on this machine; never a managed one.
73 #[arg(long)]
74 pub no_managed_php: bool,
75 /// Never download a managed PHP — use an installed managed PHP or a
76 /// system one. Errors if neither is present.
77 #[arg(long)]
78 pub no_php_downloads: bool,
79}
80
81#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
82pub enum OutputFormat {
83 Text,
84 /// `json-v1` is bougie's structured envelope; `json` is accepted as
85 /// an alias so Composer-compatible subcommands (`composer show
86 /// --format json`, etc.) work with the same global flag.
87 #[value(name = "json-v1", alias = "json")]
88 JsonV1,
89}
90
91#[derive(Subcommand, Debug)]
92pub enum Command {
93 /// Create a new project.
94 #[command(display_order = 1)]
95 Init {
96 /// Place bougie configuration in a bougie.toml file.
97 #[arg(long)]
98 toml: bool,
99 /// Set the package name (`vendor/package`) of the generated
100 /// composer.json. Overrides the name from a `--starter` manifest.
101 #[arg(long, value_name = "VENDOR/PACKAGE")]
102 name: Option<String>,
103 /// Scaffold from a starter pack: a built-in alias (e.g. `mageos`)
104 /// or an https URL serving a starter manifest. Writes the
105 /// starter's composer.json instead of the empty default.
106 #[arg(long, value_name = "URL_OR_ALIAS")]
107 starter: Option<String>,
108 /// After scaffolding, bring the project up — equivalent to
109 /// `bougie start` (sync the toolchain + vendor, then run the
110 /// project recipe). Unix-only.
111 #[arg(long)]
112 start: bool,
113 },
114
115 /// Create a new project in a new directory.
116 #[command(display_order = 2)]
117 New {
118 /// Directory to create under the current directory and scaffold
119 /// the project into.
120 #[arg(value_name = "DIRECTORY")]
121 directory: String,
122 /// Place bougie configuration in a bougie.toml file.
123 #[arg(long)]
124 toml: bool,
125 /// Set the package name (`vendor/package`) of the generated
126 /// composer.json. Overrides the name from a `--starter` manifest.
127 #[arg(long, value_name = "VENDOR/PACKAGE")]
128 name: Option<String>,
129 /// Scaffold from a starter pack: a built-in alias (e.g. `mageos`)
130 /// or an https URL serving a starter manifest.
131 #[arg(long, value_name = "URL_OR_ALIAS")]
132 starter: Option<String>,
133 /// After scaffolding, bring the project up — equivalent to
134 /// `bougie start`. Unix-only.
135 #[arg(long)]
136 start: bool,
137 },
138
139 /// Manage PHP extensions.
140 #[command(subcommand, display_order = 15)]
141 Ext(ExtCommand),
142
143 /// Add one or more packages to the project and sync. The uv-flavored
144 /// twin of `composer require`: a bare `vendor/pkg` writes a `>=X.Y`
145 /// lower bound (vs `composer require`'s caret `^X.Y`), and an
146 /// explicit constraint uses the `@` syntax (`vendor/pkg@^1.0`), as in
147 /// `bougie tool install` / `bougie ext add`. Edits `composer.json`,
148 /// re-resolves `composer.lock`, and installs into `vendor/`.
149 #[command(display_order = 10)]
150 Add {
151 /// Packages to add, `vendor/pkg` or `vendor/pkg@<constraint>`.
152 #[arg(value_name = "PACKAGES", required = true)]
153 packages: Vec<String>,
154 /// Add to `require-dev` instead of `require`.
155 #[arg(long = "dev")]
156 dev: bool,
157 /// Also update the new packages' dependencies (`-w`).
158 #[arg(short = 'w', long = "with-dependencies")]
159 with_dependencies: bool,
160 /// Also update all dependencies, including shared ones (`-W`).
161 #[arg(short = 'W', long = "with-all-dependencies")]
162 with_all_dependencies: bool,
163 /// Update `composer.json` + `composer.lock` but don't install
164 /// into `vendor/`.
165 #[arg(long = "no-sync")]
166 no_sync: bool,
167 /// Edit `composer.json` only — don't touch the lock or `vendor/`.
168 #[arg(long = "frozen")]
169 frozen: bool,
170 /// Run in this directory instead of CWD (`-d`).
171 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
172 working_dir: Option<std::path::PathBuf>,
173 /// Resolve and report what would change without writing anything.
174 #[arg(long = "dry-run")]
175 dry_run: bool,
176 },
177
178 /// Remove one or more packages from the project and sync. The
179 /// uv-flavored twin of `composer remove`.
180 #[command(display_order = 11)]
181 Remove {
182 /// Packages to remove (`vendor/name`).
183 #[arg(value_name = "PACKAGES", required = true)]
184 packages: Vec<String>,
185 /// Remove from `require-dev` instead of `require`.
186 #[arg(long = "dev")]
187 dev: bool,
188 /// Re-resolve `composer.lock` but don't touch `vendor/`.
189 #[arg(long = "no-sync")]
190 no_sync: bool,
191 /// Edit `composer.json` only — don't touch the lock or `vendor/`.
192 #[arg(long = "frozen")]
193 frozen: bool,
194 /// Run in this directory instead of CWD (`-d`).
195 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
196 working_dir: Option<std::path::PathBuf>,
197 /// Resolve and report what would change without writing anything.
198 #[arg(long = "dry-run")]
199 dry_run: bool,
200 },
201
202 /// Refresh `composer.lock` to match `composer.json` (native; uv's
203 /// `uv lock`). Minimal: keeps every package at its locked version
204 /// where still valid, re-resolving only what changed. Never bumps
205 /// versions and never installs — use `bougie composer update` to pull
206 /// newer versions.
207 #[command(display_order = 12)]
208 Lock {
209 /// Run in this directory instead of CWD (`-d`).
210 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
211 working_dir: Option<std::path::PathBuf>,
212 /// Resolve and report what would change without writing the lock.
213 #[arg(long = "dry-run")]
214 dry_run: bool,
215 },
216
217 /// Print the project's dependency tree (native; uv's `uv tree`).
218 /// Reads `composer.lock`.
219 #[command(display_order = 13)]
220 Tree {
221 /// Root the tree at this package instead of the project.
222 #[arg(value_name = "PACKAGE")]
223 package: Option<String>,
224 /// Skip dev dependencies.
225 #[arg(long = "no-dev")]
226 no_dev: bool,
227 /// Run in this directory instead of CWD (`-d`).
228 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
229 working_dir: Option<std::path::PathBuf>,
230 },
231
232 /// List installed packages with a newer version available (native;
233 /// like `uv`/`pnpm outdated`). Reads `composer.lock` and queries the
234 /// configured repositories.
235 #[command(display_order = 14)]
236 Outdated {
237 /// Optional `vendor/name` filters; with none, all are considered.
238 #[arg(value_name = "PACKAGES")]
239 packages: Vec<String>,
240 /// Only the project's direct dependencies (`--direct` / `-D`).
241 #[arg(short = 'D', long = "direct")]
242 direct: bool,
243 /// Only packages with a new major version.
244 #[arg(long = "major-only")]
245 major_only: bool,
246 /// Only packages with a new minor version.
247 #[arg(long = "minor-only")]
248 minor_only: bool,
249 /// Only packages with a new patch version.
250 #[arg(long = "patch-only")]
251 patch_only: bool,
252 /// Skip dev dependencies.
253 #[arg(long = "no-dev")]
254 no_dev: bool,
255 /// Exit non-zero if any package is outdated.
256 #[arg(long = "strict")]
257 strict: bool,
258 /// Run in this directory instead of CWD (`-d`).
259 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
260 working_dir: Option<std::path::PathBuf>,
261 },
262
263 /// Install everything the project requires.
264 #[command(display_order = 6)]
265 Sync {
266 /// Don't try to download anything, this will fail if there are uncached packages.
267 #[arg(long)]
268 offline: bool,
269 /// Show the plan, change nothing on disk.
270 #[arg(long)]
271 dry_run: bool,
272 /// Run composer.json root scripts for this sync, overriding
273 /// `[scripts] run` in bougie.toml. Off by default (opt-in).
274 #[arg(long, conflicts_with = "no_scripts")]
275 scripts: bool,
276 /// Skip composer.json root scripts for this sync, overriding
277 /// `[scripts] run = true` in bougie.toml.
278 #[arg(long = "no-scripts")]
279 no_scripts: bool,
280 #[command(flatten)]
281 php: PhpPrefArgs,
282 },
283
284 /// Run a command in the project environment.
285 #[command(display_order = 5)]
286 Run {
287 /// Add a temporary extension for this invocation.
288 #[arg(long, value_name = "EXT=VER")]
289 with: Vec<String>,
290 /// Skip the implicit `bougie sync` before running.
291 #[arg(long)]
292 no_sync: bool,
293 /// Layer the server's debug overlay (`vendor/bougie/conf.d-debug/`)
294 /// into `PHP_INI_SCAN_DIR` and set `XDEBUG_SESSION=1` for the
295 /// child. Installs xdebug on first use if not already present.
296 #[arg(long)]
297 xdebug: bool,
298 /// Run with a specific PHP interpreter. Accepts a version
299 /// (`8.3`, `8.3.12`), a constraint (`~8.3`, `>=8.2,<8.4`), or a
300 /// path to a `php` binary. Forces a sync to that interpreter,
301 /// so it can't be combined with `--no-sync`. Mirrors
302 /// `uv run --python`.
303 #[arg(long = "php", value_name = "VER|PATH", conflicts_with = "no_sync")]
304 php_request: Option<String>,
305 #[command(flatten)]
306 php: PhpPrefArgs,
307 /// Command and arguments. `--` separator is optional.
308 #[arg(trailing_var_arg = true, allow_hyphen_values = true, required = true)]
309 argv: Vec<String>,
310 },
311
312 /// Manage PHP interpreters.
313 #[command(subcommand, display_order = 20)]
314 Php(PhpCommand),
315
316 /// Manage Node.js interpreters (for projects that build frontend
317 /// assets — Vite, Laravel Mix, Magento static-content deploy).
318 #[command(subcommand, display_order = 21)]
319 Node(NodeCommand),
320
321 /// Run Composer, reimplemented natively. bougie does not bundle or
322 /// execute the Composer phar; the common Composer surface
323 /// (install/update/require/remove/show/why/why-not/outdated/audit/
324 /// licenses/fund/status/validate/dump-autoload) runs natively, and an
325 /// unrecognized subcommand errors with a pointer to
326 /// `bougie tool install composer/composer` for the full upstream
327 /// Composer.
328 #[command(subcommand, display_order = 16)]
329 Composer(ComposerCommand),
330
331 /// Manage globally-installed, isolated PHP CLI tools. See
332 /// `TOOL_PLAN.md` for the design.
333 #[command(subcommand, display_order = 22)]
334 Tool(ToolCommand),
335
336 /// Runtime shim invoked by tool wrappers (`#!.../bougie tool-exec`).
337 /// Not for direct CLI use; hidden from `--help`.
338 #[command(hide = true, name = "tool-exec")]
339 ToolExec {
340 /// Path to the tool wrapper script the kernel handed us as
341 /// argv[1] via the shebang.
342 wrapper: std::path::PathBuf,
343 /// User-supplied arguments to the tool, passed through to PHP.
344 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
345 args: Vec<std::ffi::OsString>,
346 },
347
348 /// Manage bougie's cache.
349 #[command(subcommand, display_order = 40)]
350 Cache(CacheCommand),
351
352 /// Manage the bougie binary itself.
353 #[command(subcommand)]
354 #[command(name = "self", display_order = 41)]
355 SelfCmd(SelfCommand),
356
357 /// Run the bougie development HTTP server for the current project.
358 /// With no subcommand, registers the project with the shared dev
359 /// server, prints its URL, and streams its log (Ctrl-C detaches).
360 /// See SERVER.md.
361 #[command(display_order = 30)]
362 Server(ServerArgs),
363
364 /// Manage project-scoped dev services (mariadb, redis, …). See
365 /// SERVICES.md and CLI.md §3.8.
366 #[command(subcommand, display_order = 31)]
367 Services(ServicesCommand),
368
369 /// Inspect and manage provisioned tenants across the shared dev
370 /// services and the project each belongs to. Reads the on-disk
371 /// tenant ledgers; no daemon required.
372 #[command(subcommand, display_order = 32)]
373 Projects(ProjectsCommand),
374
375 /// Bring the whole project up: run the detected recipe's `start`
376 /// task, whose DAG syncs the toolchain + vendor, provisions and
377 /// starts the project's services, runs any setup, and starts the
378 /// dev server. The project lifecycle umbrella (ddev's `start`).
379 /// For an individual task use `bougie make <task>`. Unix-only.
380 #[command(display_order = 3)]
381 Start {
382 /// Skip the implicit `bougie sync` prologue.
383 #[arg(long)]
384 no_sync: bool,
385 /// Show what would run, but don't execute.
386 #[arg(long)]
387 dry_run: bool,
388 /// Explain why each step runs or skips.
389 #[arg(long)]
390 explain: bool,
391 /// Ignore the builtin recipe; use only `bougie.toml`.
392 #[arg(long)]
393 no_builtin: bool,
394 /// Force a specific builtin (e.g. `magento`).
395 #[arg(long, value_name = "NAME")]
396 recipe: Option<String>,
397 },
398
399 /// Bring the project down: stop its declared services (or every
400 /// service in `names`), including the dev-server tenant when the
401 /// `server` service is declared. The shared daemon and any other
402 /// project's tenants stay up. The teardown twin of `bougie start`.
403 /// Unix-only.
404 #[command(display_order = 4)]
405 Stop {
406 /// Service names to stop. Empty = every declared service.
407 names: Vec<String>,
408 /// Destroy persisted tenant data (e.g. FLUSHDB on redis). Off
409 /// by default — `bougie start` should restore state.
410 #[arg(long)]
411 purge: bool,
412 },
413
414 /// Walk a project recipe's DAG, running tasks whose freshness
415 /// check fails. With no task, lists the available tasks; use
416 /// `bougie start` to bring the whole project up. See RECIPES.md.
417 #[command(display_order = 7)]
418 Make {
419 /// Task to run. With none, the available tasks are listed.
420 task: Option<String>,
421 /// List available tasks instead of running.
422 #[arg(long, conflicts_with_all = ["dry_run", "explain", "print"])]
423 list: bool,
424 /// Show what would run, but don't execute.
425 #[arg(long)]
426 dry_run: bool,
427 /// Explain why each step runs or skips.
428 #[arg(long)]
429 explain: bool,
430 /// Skip the implicit `bougie sync` prologue.
431 #[arg(long)]
432 no_sync: bool,
433 /// Ignore the builtin recipe; use only `bougie.toml`.
434 #[arg(long)]
435 no_builtin: bool,
436 /// Force a specific builtin (e.g. `magento`).
437 #[arg(long, value_name = "NAME")]
438 recipe: Option<String>,
439 /// Print the merged recipe to stdout instead of running.
440 #[arg(long)]
441 print: bool,
442 },
443
444 /// Format the project's PHP, the way `uv format` runs ruff.
445 ///
446 /// bougie does not bundle a formatter: on first use it downloads a
447 /// pinned `wick` binary (cresset-tools/wick — an unconfigurable,
448 /// Laravel Pint-style formatter), caches it, and execs it. Every
449 /// argument is forwarded verbatim to `wick`, so `bougie format`,
450 /// `bougie format --check`, `bougie format src/ --diff`, and
451 /// `… | bougie format -` behave exactly like the matching `wick`
452 /// invocation. Pin a specific wick with `BOUGIE_WICK_VERSION`.
453 #[command(display_order = 8)]
454 Format {
455 /// Arguments forwarded verbatim to `wick` (paths, `--check`,
456 /// `--diff`, `-` for stdin, …).
457 #[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "ARGS")]
458 args: Vec<std::ffi::OsString>,
459 },
460}
461
462#[derive(Subcommand, Debug)]
463pub enum ServicesCommand {
464 /// Start the project's declared services (or every service in
465 /// `names`) and provision the project's tenant in each. For the
466 /// whole-project bring-up use `bougie start`.
467 Up {
468 /// Service names to bring up. Empty = every declared service.
469 names: Vec<String>,
470 /// Start the services and return immediately instead of
471 /// attaching to their combined log stream. Attaching is the
472 /// default for an interactive (TTY) text-mode invocation;
473 /// non-interactive runs and `--format json-v1` always detach.
474 #[arg(short = 'd', long)]
475 detach: bool,
476 },
477 /// Stop the project's declared services (or every service in
478 /// `names`). The shared global process stays up while any other
479 /// project's tenant remains. For the whole-project teardown use
480 /// `bougie stop`.
481 Down {
482 names: Vec<String>,
483 /// Destroy persisted tenant data (e.g. FLUSHDB on redis). Off
484 /// by default — re-adding the service should restore state.
485 #[arg(long)]
486 purge: bool,
487 },
488 /// Declare a service in the project. Errors if the name isn't in
489 /// the catalog. Use `bougie services catalog` to discover names.
490 Add {
491 /// One or more service names, each optionally `@<version>`.
492 names: Vec<String>,
493 },
494 /// Remove a service declaration from the project. Tenant data is
495 /// kept by default (re-adding restores it); pass `--purge` to also
496 /// destroy it.
497 Remove {
498 /// Service names to remove.
499 names: Vec<String>,
500 /// Also destroy the project's tenant data for each service
501 /// (same as `bougie services down --purge`) before undeclaring.
502 #[arg(long)]
503 purge: bool,
504 },
505 /// List the services declared in the current project.
506 List {
507 /// Reserved for cross-project listing in Phase 3+. Today this
508 /// degrades silently to per-project output.
509 #[arg(long)]
510 all: bool,
511 },
512 /// Print the built-in service catalog (no daemon required).
513 Catalog,
514 /// Restart the named services (or every declared service). Stops
515 /// then starts the underlying global process; the tenant ledger
516 /// is preserved, so generated passwords / DB numbers survive.
517 /// Affects every project sharing the same service.
518 Restart {
519 names: Vec<String>,
520 },
521 /// Per-service status for the current project.
522 Status {
523 /// Limit to a single service.
524 name: Option<String>,
525 },
526 /// Tail (and optionally follow) service logs. With no name, shows
527 /// the combined ("multilog") stream of every service declared in the
528 /// project, each line prefixed with its (colorized) service name —
529 /// the same view `bougie services up` attaches to.
530 Logs {
531 /// Service name. Omit to tail every declared service at once.
532 name: Option<String>,
533 /// Follow the log; runs until interrupted (Ctrl-C).
534 #[arg(short = 'f', long)]
535 follow: bool,
536 /// Number of trailing lines to print before any follow.
537 #[arg(short = 'n', long, default_value_t = 50)]
538 lines: usize,
539 },
540 /// Inspect and control the `bougied` daemon.
541 #[command(subcommand)]
542 Daemon(ServicesDaemonCommand),
543}
544
545#[derive(Subcommand, Debug)]
546pub enum ProjectsCommand {
547 /// List every provisioned tenant across the shared services and the
548 /// project each belongs to. Reads the on-disk tenant ledgers; no
549 /// daemon required.
550 List {
551 /// Show the per-service allocation (redis db number, rabbitmq
552 /// vhost, server hostname, …) as an extra column.
553 #[arg(long)]
554 alloc: bool,
555 },
556 /// Deprovision tenants and remove them from the service ledgers.
557 /// With no flags, targets *orphaned* tenants whose project directory
558 /// no longer exists. Destructive: when the service is running this
559 /// drops the tenant's data (database, vhost, redis db, …); when it's
560 /// stopped, only the ledger entry is removed.
561 Purge {
562 /// Purge a specific project's tenants by path (it may already be
563 /// deleted) instead of the orphaned set.
564 #[arg(long)]
565 project: Option<String>,
566 /// Purge every tenant of every project. Use with care.
567 #[arg(long)]
568 all: bool,
569 /// Print what would be purged and exit without changing anything.
570 #[arg(long)]
571 dry_run: bool,
572 /// Skip the confirmation prompt (required for non-interactive use).
573 #[arg(short = 'y', long)]
574 yes: bool,
575 },
576}
577
578#[derive(Subcommand, Debug)]
579pub enum ServicesDaemonCommand {
580 /// Print daemon PID, socket path, and managed-service count. The
581 /// daemon is auto-spawned if not already running.
582 Status,
583 /// Send a graceful shutdown to the running daemon.
584 Stop,
585 /// Print the daemon's reported version (used by the CLI to detect
586 /// post-`self update` daemon-binary mismatches).
587 Version,
588}
589
590/// `bougie server` — the project verb plus its management subcommands.
591/// With no subcommand, the flattened [`ServeArgs`] drive the default
592/// "serve the current project" action; otherwise a [`ServerCommand`]
593/// runs.
594#[derive(Args, Debug)]
595#[command(args_conflicts_with_subcommands = true)]
596pub struct ServerArgs {
597 #[command(subcommand)]
598 pub command: Option<ServerCommand>,
599 #[command(flatten)]
600 pub serve: ServeArgs,
601}
602
603/// Default-action arguments for `bougie server` (no subcommand):
604/// register the current project with the shared dev server, print its
605/// URL, and stream its log.
606#[derive(Args, Debug)]
607#[allow(clippy::struct_excessive_bools)] // each bool is a distinct CLI flag
608pub struct ServeArgs {
609 /// Hostname label override — the `<name>` in `<name>.bougie.run`.
610 /// Defaults to a name derived from the project.
611 #[arg(value_name = "NAME")]
612 pub name: Option<String>,
613 /// Open the project URL in a browser once the server is ready.
614 #[arg(long)]
615 pub open: bool,
616 /// Serve over HTTPS (requires `bougie server tls install`).
617 #[arg(long)]
618 pub tls: bool,
619 /// Print the URL and return immediately instead of attaching to the
620 /// log stream. Matches `services up`'s `-d`.
621 #[arg(short = 'd', long = "detach")]
622 pub detach: bool,
623 /// Skip the implicit `bougie sync` before serving.
624 #[arg(long)]
625 pub no_sync: bool,
626}
627
628#[derive(Subcommand, Debug)]
629pub enum ServerCommand {
630 /// Low-level primitive: run the server process against an explicit
631 /// multi-host `server.toml`, foreground, with no daemon. This is
632 /// what `bougied` spawns and what CI / power users invoke directly;
633 /// `--config` is required because a multi-host server has no single
634 /// project to default to. The bougied-managed path (`bougie services
635 /// up server`) supplies its own service-scoped `server.toml`.
636 Run {
637 /// `server.toml` path. Required.
638 #[arg(long, value_name = "PATH")]
639 config: std::path::PathBuf,
640 /// CLI override of `[server].listen` (e.g. `127.0.0.1:7080`).
641 #[arg(long, value_name = "ADDR")]
642 listen: Option<String>,
643 /// CLI override of `[server].log_format`.
644 #[arg(long, value_name = "FMT")]
645 log_format: Option<String>,
646 },
647 /// Show the dev server's hosts and live pool state. Reads the
648 /// running server's control socket when available, falling back to
649 /// the configured hosts otherwise. Replaces the old `list`, which
650 /// remains as a hidden alias.
651 #[command(alias = "list")]
652 Status {
653 /// `server.toml` to inspect. Defaults to the bougied-managed
654 /// config.
655 #[arg(long, value_name = "PATH")]
656 config: Option<std::path::PathBuf>,
657 },
658 /// Open the current project's (or NAME's) dev URL in a browser.
659 Open {
660 /// Hostname label to open. Defaults to the current project.
661 #[arg(value_name = "NAME")]
662 name: Option<String>,
663 },
664 /// Stop the shared dev server. Equivalent to `bougie services down
665 /// server`; stops hosting for every project, since the server is shared.
666 Stop,
667 /// Tail the dev server's request log. In a project, defaults to
668 /// this project's host.
669 Logs {
670 /// Follow the log; runs until interrupted (Ctrl-C).
671 #[arg(short = 'f', long)]
672 follow: bool,
673 /// Number of trailing lines to print before any follow.
674 #[arg(short = 'n', long, default_value_t = 50)]
675 lines: usize,
676 },
677 /// Manage local TLS via mkcert.
678 #[command(subcommand)]
679 Tls(ServerTlsCommand),
680 /// Manage `/etc/hosts` overrides.
681 #[command(subcommand)]
682 Hosts(ServerHostsCommand),
683}
684
685#[derive(Subcommand, Debug)]
686pub enum ServerHostsCommand {
687 /// Rewrite the bougie sentinel block in /etc/hosts to match
688 /// server.toml. Requires root — runs via sudo.
689 Apply {
690 /// `server.toml` to read the host list from. Defaults to the
691 /// bougied-managed config.
692 #[arg(long, value_name = "PATH")]
693 config: Option<std::path::PathBuf>,
694 },
695}
696
697#[derive(Subcommand, Debug)]
698pub enum ServerTlsCommand {
699 /// Fetch mkcert and install bougie's local CA.
700 Install,
701 /// Uninstall bougie's local CA.
702 Uninstall,
703}
704
705#[derive(Subcommand, Debug)]
706pub enum ExtCommand {
707 /// Add an extension dependency. Each `<arg>` is either an
708 /// extension name (e.g. `redis`, `xdebug@3.5.1`) — fetched from
709 /// the index and recorded in composer.json — or a path to a local
710 /// `.so` file (e.g. `/opt/tideways/tideways-php-8.5.so`), in which
711 /// case bougie copies it into the store, auto-detects the
712 /// extension name and Zend-ness from the binary, and writes a
713 /// fragment to the durable, machine-local `conf.d-local/` (under
714 /// `$BOUGIE_HOME`) without touching composer.json. Mix and match in
715 /// one invocation.
716 Add {
717 /// Extension names or `.so` paths (anything ending in `.so` is
718 /// treated as a local file).
719 args: Vec<String>,
720 /// Skip the implicit `bougie sync` after the composer call.
721 #[arg(long)]
722 no_sync: bool,
723 #[command(flatten)]
724 php: PhpPrefArgs,
725 },
726 /// Remove an extension dependency.
727 Remove {
728 /// The extension(s) to remove.
729 names: Vec<String>,
730 /// Skip the implicit `bougie sync` after the composer call.
731 #[arg(long)]
732 no_sync: bool,
733 },
734 /// List available extensions.
735 List {
736 /// Only show installed extensions.
737 #[arg(long)]
738 only_installed: bool,
739 /// Only show extensions advertised by the index.
740 #[arg(long)]
741 only_available: bool,
742 /// List all extension versions, including older releases.
743 #[arg(long)]
744 all_versions: bool,
745 /// List extensions for all platforms, not just the host's.
746 #[arg(long)]
747 all_platforms: bool,
748 /// Show the URLs of available extension downloads.
749 #[arg(long)]
750 show_urls: bool,
751 },
752}
753
754#[derive(Subcommand, Debug)]
755pub enum PhpCommand {
756 /// Install a new PHP version.
757 Install {
758 /// The PHP version(s) to install (e.g. `8.3`, `8.3.12`, `8.3+zts`).
759 requests: Vec<String>,
760 /// Build flavor to install [possible values: nts, nts-debug, zts, zts-debug].
761 #[arg(long)]
762 flavor: Option<String>,
763 /// Skip the entire baseline extension set; install only the bare
764 /// Debian-aligned interpreter (`REFACTOR_DEBIAN_ALIGNED.md`).
765 #[arg(long, conflicts_with = "without")]
766 bare: bool,
767 /// Skip a specific baseline extension. Repeatable: `--without opcache
768 /// --without readline`. The named extensions must already be in the
769 /// baseline set; use `bougie ext remove` after install for anything else.
770 #[arg(long, value_name = "EXT", action = clap::ArgAction::Append)]
771 without: Vec<String>,
772 },
773 /// Remove a PHP version.
774 Uninstall {
775 /// The PHP version(s) to uninstall.
776 #[arg(required = true)]
777 requests: Vec<String>,
778 /// Build flavor to uninstall [possible values: nts, nts-debug, zts, zts-debug].
779 #[arg(long)]
780 flavor: Option<String>,
781 },
782 /// List available PHP interpreters.
783 List {
784 /// A PHP request to filter by.
785 request: Option<String>,
786 /// Only show installed PHP versions.
787 #[arg(long)]
788 only_installed: bool,
789 /// Only show PHP versions available for download.
790 #[arg(long)]
791 only_available: bool,
792 /// List all PHP versions, including older patch versions.
793 #[arg(long)]
794 all_versions: bool,
795 /// List PHP downloads for all platforms.
796 #[arg(long)]
797 all_platforms: bool,
798 /// List PHP downloads for all architectures.
799 #[arg(long)]
800 all_arches: bool,
801 /// Show the URLs of available PHP downloads.
802 #[arg(long)]
803 show_urls: bool,
804 },
805 /// Search for a PHP interpreter.
806 Find {
807 /// A PHP request to search for.
808 request: Option<String>,
809 },
810 /// Pin the project's PHP version.
811 Pin {
812 /// The PHP version to pin.
813 request: String,
814 /// Write the pin to `bougie.toml` (creating it if needed).
815 #[arg(long, conflicts_with = "composer")]
816 toml: bool,
817 /// Write the pin to `composer.json`'s `require.php`.
818 #[arg(long, conflicts_with = "toml")]
819 composer: bool,
820 },
821 /// Refresh installed interpreters to the latest published patch.
822 Upgrade {
823 /// The PHP minor version(s) to upgrade (e.g. `8.3`).
824 minor: Option<String>,
825 },
826 /// Show the PHP interpreter installation directory.
827 Dir,
828}
829
830#[derive(Subcommand, Debug)]
831pub enum NodeCommand {
832 /// Install a Node.js version from nodejs.org.
833 Install {
834 /// The Node version(s) to install (e.g. `latest`, `lts`, `20`,
835 /// `20.11`, `20.11.0`). Defaults to `latest`.
836 requests: Vec<String>,
837 },
838 /// Remove an installed Node.js version.
839 Uninstall {
840 /// The Node version(s) to uninstall (exact `20.11.0`).
841 #[arg(required = true)]
842 requests: Vec<String>,
843 },
844 /// List installed Node.js versions.
845 List,
846 /// Resolve a request and show the version + download URL it maps to,
847 /// without installing.
848 Find {
849 /// A Node request to resolve (e.g. `lts`, `20`). Defaults to `latest`.
850 request: Option<String>,
851 },
852 /// Show the Node.js installation directory.
853 Dir,
854}
855
856#[derive(Subcommand, Debug)]
857pub enum ComposerCommand {
858 /// Install a project's `vendor/` from `composer.lock`. Reads
859 /// `composer.json` + `composer.lock` in the working directory,
860 /// content-hash-verifies the lock, parallel-downloads dists into
861 /// `vendor/`, and emits `vendor/autoload.php`.
862 Install {
863 /// Run the install in this directory instead of CWD.
864 /// Mirrors Composer's `--working-dir` / `-d`.
865 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
866 working_dir: Option<std::path::PathBuf>,
867 /// Skip dev-only packages and dev autoload entries.
868 #[arg(long = "no-dev")]
869 no_dev: bool,
870 /// Fail if composer.lock is out of sync with composer.json.
871 /// Currently a no-op — the install already errors on
872 /// content-hash mismatch by default. Accepted for parity
873 /// with Composer's CI usage.
874 #[arg(long = "frozen")]
875 frozen: bool,
876 /// Verify the lock is internally consistent (content-hash,
877 /// requires, transitives) and exit. Doesn't touch `vendor/`
878 /// or run the autoloader. CI-friendly read-only check.
879 #[arg(long = "lock-verify")]
880 lock_verify: bool,
881 /// Ignore all platform requirements (php, ext-*, lib-*).
882 /// Accepted for Composer parity; bougie does not enforce
883 /// platform requirements yet.
884 #[arg(long = "ignore-platform-reqs")]
885 ignore_platform_reqs: bool,
886 /// Ignore a specific platform requirement.
887 #[arg(long = "ignore-platform-req", value_name = "REQ")]
888 ignore_platform_req: Vec<String>,
889 /// Run composer.json root scripts, overriding `[scripts] run`
890 /// in bougie.toml. Off by default (opt-in).
891 #[arg(long, conflicts_with = "no_scripts")]
892 scripts: bool,
893 /// Skip composer.json root scripts, overriding `[scripts] run
894 /// = true` in bougie.toml (Composer-compatible `--no-scripts`).
895 #[arg(long = "no-scripts")]
896 no_scripts: bool,
897 },
898 /// Resolve the project's dependency graph, write a fresh
899 /// `composer.lock`, and install the result into `vendor/` (matching
900 /// Composer's `update`). With no package arguments this re-resolves
901 /// from scratch; naming one or more packages does a partial update —
902 /// only those re-resolve while every other locked package stays
903 /// pinned. `--no-install` stops after writing the lock; `--dry-run`
904 /// previews the solution without writing anything. Aliased to
905 /// `upgrade` / `u`, like Composer.
906 #[command(visible_alias = "upgrade", alias = "u")]
907 Update {
908 /// Packages to update (`vendor/name`). When given, only these
909 /// packages re-resolve; every other package stays pinned to its
910 /// `composer.lock` version (Composer's partial update). With no
911 /// packages, the whole graph re-resolves from scratch.
912 #[arg(value_name = "PACKAGES")]
913 packages: Vec<String>,
914 /// Write the lock but don't install into `vendor/` (Composer's
915 /// `--no-install`).
916 #[arg(long = "no-install")]
917 no_install: bool,
918 /// Also update the named packages' dependencies (Composer's
919 /// `--with-dependencies` / `-w`).
920 #[arg(short = 'w', long = "with-dependencies")]
921 with_dependencies: bool,
922 /// Also update all of the named packages' dependencies, including
923 /// ones shared with other packages (Composer's
924 /// `--with-all-dependencies` / `-W`).
925 #[arg(short = 'W', long = "with-all-dependencies")]
926 with_all_dependencies: bool,
927 /// Run the update in this directory instead of CWD.
928 /// Mirrors Composer's `--working-dir` / `-d`.
929 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
930 working_dir: Option<std::path::PathBuf>,
931 /// Skip dev-only root requires when resolving.
932 #[arg(long = "no-dev")]
933 no_dev: bool,
934 /// Resolve and print the solution without writing
935 /// `composer.lock` or touching `vendor/`. Without this flag,
936 /// `update` writes a fresh `composer.lock`.
937 #[arg(long = "dry-run")]
938 dry_run: bool,
939 /// Ignore all platform requirements (php, ext-*, lib-*).
940 /// Accepted for Composer parity; bougie does not enforce
941 /// platform requirements yet.
942 #[arg(long = "ignore-platform-reqs")]
943 ignore_platform_reqs: bool,
944 /// Ignore a specific platform requirement.
945 #[arg(long = "ignore-platform-req", value_name = "REQ")]
946 ignore_platform_req: Vec<String>,
947 },
948 /// Validate composer.json structure and contents.
949 Validate {
950 /// Run in this directory instead of CWD.
951 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
952 working_dir: Option<std::path::PathBuf>,
953 /// Return non-zero exit code for warnings too.
954 #[arg(long)]
955 strict: bool,
956 /// Skip lock file freshness check.
957 #[arg(long = "no-check-lock")]
958 no_check_lock: bool,
959 /// Skip publish-only checks (name casing, required fields).
960 #[arg(long = "no-check-publish")]
961 no_check_publish: bool,
962 /// Skip unbound/exact version constraint warnings.
963 #[arg(long = "no-check-all")]
964 no_check_all: bool,
965 /// Also validate installed dependencies' composer.json files.
966 #[arg(long = "with-dependencies")]
967 with_dependencies: bool,
968 /// Force lock file checking even when `config.lock` is false.
969 #[arg(long = "check-lock")]
970 check_lock: bool,
971 },
972 /// Regenerate `vendor/composer/autoload_*.php` against the current
973 /// `composer.lock`. Drop-in for `composer dump-autoload`; output
974 /// is byte-equivalent to Composer 2.8.12 with the same flags. Aliased
975 /// to `dump-autoload` for users coming from Composer muscle-memory.
976 #[command(alias = "dump-autoload")]
977 DumpAutoloader {
978 /// Optimize the classmap (`--optimize` / `-o`).
979 #[arg(short = 'o', long = "optimize", alias = "optimize-autoloader")]
980 optimize: bool,
981 /// Emit the classmap-authoritative static loader
982 /// (`--classmap-authoritative` / `-a`). Implies `--optimize`.
983 #[arg(short = 'a', long = "classmap-authoritative")]
984 classmap_authoritative: bool,
985 /// Skip dev autoload entries (`--no-dev`).
986 #[arg(long = "no-dev")]
987 no_dev: bool,
988 /// Emit the `APCu` loader bootstrap (`--apcu-autoloader`).
989 #[arg(long = "apcu-autoloader")]
990 apcu_autoloader: bool,
991 /// Explicit `APCu` prefix; implies `--apcu-autoloader`.
992 #[arg(long = "apcu-autoloader-prefix", value_name = "PREFIX")]
993 apcu_prefix: Option<String>,
994 /// Override the `ComposerAutoloaderInit<X>` class suffix —
995 /// otherwise the value from `composer.json`'s
996 /// `config.autoloader-suffix`, or the `composer.lock`
997 /// content-hash.
998 #[arg(long = "autoloader-suffix", value_name = "SUFFIX")]
999 autoloader_suffix: Option<String>,
1000 /// Run the dump in this directory instead of the current one.
1001 /// Mirrors Composer's `--working-dir` / `-d`.
1002 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1003 working_dir: Option<std::path::PathBuf>,
1004 },
1005 /// Add one or more packages to `composer.json` `require` (or
1006 /// `require-dev`), re-resolve `composer.lock`, and install them.
1007 /// Fully Composer-compatible: a bare `vendor/pkg` resolves the
1008 /// latest stable and writes a caret (`^X.Y`) constraint; supply an
1009 /// explicit constraint with `vendor/pkg:^1.0`, `vendor/pkg=^1.0`, or
1010 /// a trailing argument (`vendor/pkg ^1.0`) — Composer's separators
1011 /// are `:`, `=`, or a space (the `@` separator is *not* accepted, as
1012 /// in Composer). For bougie's `>=`-default + `@`-syntax house style,
1013 /// use the top-level `bougie add` instead.
1014 Require {
1015 /// Packages to require, as Composer name↔version pairs.
1016 #[arg(value_name = "PACKAGES", required = true)]
1017 packages: Vec<String>,
1018 /// Add to `require-dev` instead of `require`.
1019 #[arg(long = "dev")]
1020 dev: bool,
1021 /// Edit `composer.json` only — don't re-resolve `composer.lock`
1022 /// or touch `vendor/` (Composer's `--no-update`).
1023 #[arg(long = "no-update")]
1024 no_update: bool,
1025 /// Re-resolve and write `composer.lock` but don't install into
1026 /// `vendor/` (Composer's `--no-install`).
1027 #[arg(long = "no-install")]
1028 no_install: bool,
1029 /// Also update the new packages' dependencies (`-w`).
1030 #[arg(short = 'w', long = "with-dependencies")]
1031 with_dependencies: bool,
1032 /// Also update all dependencies, including shared ones (`-W`).
1033 #[arg(short = 'W', long = "with-all-dependencies")]
1034 with_all_dependencies: bool,
1035 /// Prefer the lowest matching versions when resolving.
1036 #[arg(long = "prefer-lowest")]
1037 prefer_lowest: bool,
1038 /// Ignore all platform requirements (php, ext-*, lib-*).
1039 #[arg(long = "ignore-platform-reqs")]
1040 ignore_platform_reqs: bool,
1041 /// Ignore a specific platform requirement.
1042 #[arg(long = "ignore-platform-req", value_name = "REQ")]
1043 ignore_platform_req: Vec<String>,
1044 /// Run in this directory instead of CWD (`-d`).
1045 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1046 working_dir: Option<std::path::PathBuf>,
1047 /// Resolve and report what would change without writing
1048 /// `composer.json`, `composer.lock`, or `vendor/`.
1049 #[arg(long = "dry-run")]
1050 dry_run: bool,
1051 },
1052 /// Remove one or more packages from `composer.json`, re-resolve
1053 /// `composer.lock`, and uninstall them from `vendor/`. Drop-in for
1054 /// `composer remove`.
1055 Remove {
1056 /// Packages to remove (`vendor/name`).
1057 #[arg(value_name = "PACKAGES", required = true)]
1058 packages: Vec<String>,
1059 /// Remove from `require-dev` instead of `require`.
1060 #[arg(long = "dev")]
1061 dev: bool,
1062 /// Edit `composer.json` only — don't re-resolve or touch
1063 /// `vendor/` (Composer's `--no-update`).
1064 #[arg(long = "no-update")]
1065 no_update: bool,
1066 /// Re-resolve and write `composer.lock` but don't touch
1067 /// `vendor/` (Composer's `--no-install`).
1068 #[arg(long = "no-install")]
1069 no_install: bool,
1070 /// Skip dev-only packages when resolving.
1071 #[arg(long = "no-dev")]
1072 no_dev: bool,
1073 /// Ignore all platform requirements (php, ext-*, lib-*).
1074 #[arg(long = "ignore-platform-reqs")]
1075 ignore_platform_reqs: bool,
1076 /// Ignore a specific platform requirement.
1077 #[arg(long = "ignore-platform-req", value_name = "REQ")]
1078 ignore_platform_req: Vec<String>,
1079 /// Run in this directory instead of CWD (`-d`).
1080 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1081 working_dir: Option<std::path::PathBuf>,
1082 /// Resolve and report what would change without writing
1083 /// `composer.json`, `composer.lock`, or `vendor/`.
1084 #[arg(long = "dry-run")]
1085 dry_run: bool,
1086 },
1087 /// List installed packages, or show details for one. Reads the
1088 /// project's `composer.lock`. Drop-in for `composer show` (aliases
1089 /// `info`, `list`).
1090 #[command(alias = "info", alias = "list")]
1091 Show {
1092 /// A single `vendor/name` to show details for. With no argument,
1093 /// every installed package is listed.
1094 #[arg(value_name = "PACKAGE")]
1095 package: Option<String>,
1096 /// Render the dependency tree (`--tree` / `-t`).
1097 #[arg(short = 't', long = "tree")]
1098 tree: bool,
1099 /// Only the project's direct dependencies (`--direct` / `-D`).
1100 #[arg(short = 'D', long = "direct")]
1101 direct: bool,
1102 /// Only platform packages — php, ext-*, lib-* (`--platform` / `-p`).
1103 #[arg(short = 'p', long = "platform")]
1104 platform: bool,
1105 /// Show the root package's own info (`--self` / `-s`).
1106 #[arg(short = 's', long = "self")]
1107 self_: bool,
1108 /// Print package names only (`--name-only` / `-N`).
1109 #[arg(short = 'N', long = "name-only")]
1110 name_only: bool,
1111 /// Show each package's install path (`--path` / `-P`).
1112 #[arg(short = 'P', long = "path")]
1113 path: bool,
1114 /// Also fetch and show the latest available version
1115 /// (`--latest` / `-l`).
1116 #[arg(short = 'l', long = "latest")]
1117 latest: bool,
1118 /// Only packages with a newer version available
1119 /// (`--outdated` / `-o`). Implies `--latest`.
1120 #[arg(short = 'o', long = "outdated")]
1121 outdated: bool,
1122 /// Skip dev dependencies (`--no-dev`).
1123 #[arg(long = "no-dev")]
1124 no_dev: bool,
1125 /// Run in this directory instead of CWD (`-d`).
1126 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1127 working_dir: Option<std::path::PathBuf>,
1128 },
1129 /// Show which packages depend on a given package — i.e. why it's
1130 /// installed. Drop-in for `composer why` (alias `depends`).
1131 #[command(alias = "depends")]
1132 Why {
1133 /// The package to explain.
1134 #[arg(value_name = "PACKAGE", required = true)]
1135 package: String,
1136 /// Recurse through the dependency chain (`--recursive` / `-r`).
1137 #[arg(short = 'r', long = "recursive")]
1138 recursive: bool,
1139 /// Render the full dependency-of tree (`--tree` / `-t`).
1140 #[arg(short = 't', long = "tree")]
1141 tree: bool,
1142 /// Run in this directory instead of CWD (`-d`).
1143 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1144 working_dir: Option<std::path::PathBuf>,
1145 },
1146 /// Show what prevents a package (optionally at a version) from being
1147 /// installed — conflicting requirements. Drop-in for
1148 /// `composer why-not` (alias `prohibits`).
1149 #[command(name = "why-not", alias = "prohibits")]
1150 WhyNot {
1151 /// The package to test.
1152 #[arg(value_name = "PACKAGE", required = true)]
1153 package: String,
1154 /// The version (or constraint) to test against. Defaults to `*`.
1155 #[arg(value_name = "VERSION")]
1156 version: Option<String>,
1157 /// Recurse through the dependency chain (`--recursive` / `-r`).
1158 #[arg(short = 'r', long = "recursive")]
1159 recursive: bool,
1160 /// Render the full tree (`--tree` / `-t`).
1161 #[arg(short = 't', long = "tree")]
1162 tree: bool,
1163 /// Run in this directory instead of CWD (`-d`).
1164 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1165 working_dir: Option<std::path::PathBuf>,
1166 },
1167 /// List installed packages that have a newer version available.
1168 /// Drop-in for `composer outdated` (a focused `show --latest
1169 /// --outdated`). Use the global `--format json` for JSON output.
1170 Outdated {
1171 /// Optional `vendor/name` filters; with none, all packages are
1172 /// considered.
1173 #[arg(value_name = "PACKAGES")]
1174 packages: Vec<String>,
1175 /// Only the project's direct dependencies (`--direct` / `-D`).
1176 #[arg(short = 'D', long = "direct")]
1177 direct: bool,
1178 /// Only show packages with a new major version (`--major-only`).
1179 #[arg(long = "major-only")]
1180 major_only: bool,
1181 /// Only show packages with a new minor version (`--minor-only`).
1182 #[arg(long = "minor-only")]
1183 minor_only: bool,
1184 /// Only show packages with a new patch version (`--patch-only`).
1185 #[arg(long = "patch-only")]
1186 patch_only: bool,
1187 /// Skip dev dependencies (`--no-dev`).
1188 #[arg(long = "no-dev")]
1189 no_dev: bool,
1190 /// Exit non-zero if any package is outdated (`--strict`).
1191 #[arg(long = "strict")]
1192 strict: bool,
1193 /// Run in this directory instead of CWD (`-d`).
1194 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1195 working_dir: Option<std::path::PathBuf>,
1196 },
1197 /// Check installed packages against the Packagist security-advisories
1198 /// database. Drop-in for `composer audit`. Exits non-zero when
1199 /// advisories are found. Use the global `--format json` for JSON.
1200 Audit {
1201 /// Skip dev dependencies (`--no-dev`).
1202 #[arg(long = "no-dev")]
1203 no_dev: bool,
1204 /// How to treat abandoned packages (`--abandoned`). Currently
1205 /// accepted for parity; abandoned detection is not yet wired.
1206 #[arg(long = "abandoned", value_enum, default_value = "report")]
1207 abandoned: AbandonedHandling,
1208 /// Audit the locked set (`--locked`). bougie always reads
1209 /// `composer.lock`, so this is the default behavior; accepted
1210 /// for parity.
1211 #[arg(long = "locked")]
1212 locked: bool,
1213 /// Run in this directory instead of CWD (`-d`).
1214 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1215 working_dir: Option<std::path::PathBuf>,
1216 },
1217 /// List the license of every installed package. Drop-in for
1218 /// `composer licenses`. Use the global `--format json` for JSON.
1219 Licenses {
1220 /// Skip dev dependencies (`--no-dev`).
1221 #[arg(long = "no-dev")]
1222 no_dev: bool,
1223 /// Run in this directory instead of CWD (`-d`).
1224 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1225 working_dir: Option<std::path::PathBuf>,
1226 },
1227 /// Report packages that look locally modified. Drop-in for
1228 /// `composer status`. bougie installs from dist archives, so for the
1229 /// common case this reports "no local changes".
1230 Status {
1231 /// Run in this directory instead of CWD (`-d`).
1232 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1233 working_dir: Option<std::path::PathBuf>,
1234 },
1235 /// Show funding information for installed packages, grouped by
1236 /// vendor. Drop-in for `composer fund`. Use `--format json` for JSON.
1237 Fund {
1238 /// Skip dev dependencies (`--no-dev`).
1239 #[arg(long = "no-dev")]
1240 no_dev: bool,
1241 /// Run in this directory instead of CWD (`-d`).
1242 #[arg(short = 'd', long = "working-dir", value_name = "DIR")]
1243 working_dir: Option<std::path::PathBuf>,
1244 },
1245 /// Catch-all for any composer subcommand bougie does not implement
1246 /// natively (`create-project`, `archive`, `bump`, `global`, …).
1247 /// bougie does not bundle the Composer phar, so these no longer run;
1248 /// the dispatch returns an error pointing at
1249 /// `bougie tool install composer/composer` for the full upstream
1250 /// Composer.
1251 #[command(external_subcommand)]
1252 External(Vec<OsString>),
1253}
1254
1255/// How `composer audit` treats abandoned packages.
1256#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
1257pub enum AbandonedHandling {
1258 /// Ignore abandoned packages entirely.
1259 Ignore,
1260 /// Report abandoned packages but don't fail on them.
1261 Report,
1262 /// Treat abandoned packages as an audit failure.
1263 Fail,
1264}
1265
1266#[derive(Subcommand, Debug)]
1267pub enum CacheCommand {
1268 /// Wipe the full cache.
1269 Clean,
1270 /// Remove unneeded library files.
1271 Prune {
1272 /// Show what would be pruned without removing anything.
1273 #[arg(long)]
1274 dry_run: bool,
1275 /// Also remove tracked projects that no longer exist on disk.
1276 #[arg(long)]
1277 prune_projects: bool,
1278 },
1279 /// Show the location of the cache directory.
1280 Dir,
1281 /// Show the cache size.
1282 Size,
1283}
1284
1285#[derive(Subcommand, Debug)]
1286pub enum ToolCommand {
1287 /// Install a tool. Pass `<vendor>/<name>` optionally followed by
1288 /// `@<constraint>` (e.g. `phpstan/phpstan@^1.10`).
1289 Install {
1290 /// Composer package identifier, optionally with `@<constraint>`.
1291 package: String,
1292 /// Pin the tool to a specific PHP. Accepts a version (`8.3`,
1293 /// `8.3.12`) or a constraint (`~8.3`, `>=8.2,<8.4`). When the
1294 /// requested PHP isn't installed, bougie installs it
1295 /// automatically. Defaults to the highest installed NTS PHP.
1296 #[arg(long, value_name = "VER")]
1297 php: Option<String>,
1298 /// Additional Composer package (`vendor/name[@<constraint>]`)
1299 /// or PHP extension (`intl`, `redis`) to install alongside the
1300 /// tool. May be passed multiple times.
1301 #[arg(long, value_name = "PKG_OR_EXT")]
1302 with: Vec<String>,
1303 /// Overwrite an existing executable at the bin-dir path.
1304 #[arg(long)]
1305 force: bool,
1306 },
1307 /// Remove an installed tool by its `<vendor>/<name>` identifier.
1308 Uninstall {
1309 /// Composer package identifier.
1310 package: String,
1311 },
1312 /// Add an extra composer package or PHP extension to an
1313 /// installed tool. Re-resolves the tool's lock and updates the
1314 /// vendor tree in place.
1315 Inject {
1316 /// Composer package identifier of the tool.
1317 package: String,
1318 /// Extra to add (`vendor/name[@<constraint>]` for composer
1319 /// packages, bare name for PHP extensions). Repeatable.
1320 #[arg(long, value_name = "PKG_OR_EXT", required = true)]
1321 with: Vec<String>,
1322 },
1323 /// Remove an extra previously added via `--with` / `inject`.
1324 Uninject {
1325 /// Composer package identifier of the tool.
1326 package: String,
1327 /// Extra to remove. Repeatable.
1328 #[arg(long, value_name = "PKG_OR_EXT", required = true)]
1329 with: Vec<String>,
1330 },
1331 /// List installed tools.
1332 List,
1333 /// Print a tool's install directory, or the tools root if no
1334 /// package is given.
1335 Dir {
1336 /// Composer package identifier; omit to print the tools root.
1337 package: Option<String>,
1338 },
1339 /// Run an installed-or-cached tool one-off. Reuses an existing
1340 /// persistent install if `(package, constraint, php, with)` match
1341 /// exactly; otherwise materialises into the ephemeral cache.
1342 ///
1343 /// `bgx` is provided as a convenient alias for `bougie tool run`;
1344 /// their behavior is identical.
1345 #[command(
1346 override_usage = "bougie tool run [OPTIONS] <PACKAGE> [ARGS]...",
1347 after_help = "Use `bgx` as a shortcut for `bougie tool run`.\n\n\
1348 Use `bougie help tool run` for more details.",
1349 after_long_help = ""
1350 )]
1351 Run(ToolRunArgs),
1352 // Hidden alias for `bougie tool run` for the `bgx` command. The
1353 // variant is reached only via the `bgx` binary exec'ing into it;
1354 // it doesn't surface under `bougie tool --help`. Carrying it as
1355 // a separate variant (with `display_name`, `override_usage`)
1356 // lets clap render `bgx --help` and clap-level error messages
1357 // with `bgx` as the program name rather than leaking
1358 // `bougie tool run`.
1359 #[command(
1360 hide = true,
1361 override_usage = "bgx [OPTIONS] <PACKAGE> [ARGS]...",
1362 about = "Run a tool from a Composer package.",
1363 long_about = None,
1364 after_help = "Use `bougie help tool run` for more details.",
1365 after_long_help = "",
1366 display_name = "bgx",
1367 // `bgx --version` / `bgx -V` exec into `bougie tool bgx`; give
1368 // this variant its own version flag so it short-circuits before
1369 // the required `<PACKAGE>` positional and prints `bgx <version>`.
1370 version = LONG_VERSION
1371 )]
1372 Bgx(BgxArgs),
1373 /// Re-resolve a tool's lock and bring its vendor tree up to date.
1374 /// Pass `--all` to walk every installed tool, or `--reinstall` to
1375 /// wipe and rebuild from scratch (recovery for broken state).
1376 Upgrade {
1377 /// Composer package identifier. Required unless `--all`.
1378 #[arg(required_unless_present = "all", conflicts_with = "all")]
1379 package: Option<String>,
1380 /// Upgrade every installed tool.
1381 #[arg(long)]
1382 all: bool,
1383 /// Wipe the tool dir + every entrypoint symlink and reinstall
1384 /// from scratch using the receipt's pinned `(package,
1385 /// constraint, php_version, with, extensions)` tuple.
1386 #[arg(long)]
1387 reinstall: bool,
1388 },
1389}
1390
1391#[derive(Subcommand, Debug)]
1392pub enum SelfCommand {
1393 /// Update bougie.
1394 Update {
1395 /// Update even when bougie can't confirm it installed this
1396 /// binary. By default `self update` only touches a binary that
1397 /// bougie's own installer placed (per the install receipt);
1398 /// copies from a package manager, cargo, or nix are left for
1399 /// that tool to update. Pass `--force` only if you know this
1400 /// copy came from bougie's installer.
1401 #[arg(long)]
1402 force: bool,
1403 },
1404 /// Show bougie's version.
1405 Version {
1406 /// Only show the version.
1407 #[arg(long)]
1408 short: bool,
1409 },
1410}
1411
1412#[derive(Args, Debug)]
1413pub struct ToolRunArgs {
1414 /// Pin the tool to a specific PHP for this run.
1415 #[arg(long, value_name = "VER")]
1416 pub php: Option<String>,
1417 /// Extra composer package or PHP extension, same shape as
1418 /// `tool install --with`. Repeatable.
1419 #[arg(long, value_name = "PKG_OR_EXT")]
1420 pub with: Vec<String>,
1421 /// The tool's Composer package (optionally `@<constraint>`) followed
1422 /// by the arguments to forward to it. bougie's own options must come
1423 /// *before* the package; everything from the package onward is passed
1424 /// to the tool verbatim, so no `--` separator is needed.
1425 #[arg(
1426 trailing_var_arg = true,
1427 allow_hyphen_values = true,
1428 required = true,
1429 value_name = "PACKAGE"
1430 )]
1431 pub command: Vec<std::ffi::OsString>,
1432}
1433
1434/// Args for the hidden `bgx` alias. Wraps [`ToolRunArgs`] verbatim so
1435/// the two variants share their entire surface; the wrapper exists
1436/// only so clap renders help / errors with `bgx` as the program name.
1437#[derive(Args, Debug)]
1438pub struct BgxArgs {
1439 #[command(flatten)]
1440 pub tool_run: ToolRunArgs,
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446 use clap::Parser;
1447
1448 fn cmd(argv: &[&str]) -> Command {
1449 Cli::try_parse_from(argv).expect("parse").command
1450 }
1451
1452 #[test]
1453 fn start_is_its_own_verb() {
1454 assert!(matches!(cmd(&["bougie", "start"]), Command::Start { .. }));
1455 assert!(matches!(
1456 cmd(&["bougie", "start", "--no-sync", "--dry-run"]),
1457 Command::Start { no_sync: true, dry_run: true, .. }
1458 ));
1459 }
1460
1461 #[test]
1462 fn stop_takes_names_and_purge() {
1463 let Command::Stop { names, purge } = cmd(&["bougie", "stop", "redis", "--purge"]) else {
1464 panic!("expected stop");
1465 };
1466 assert_eq!(names, ["redis"]);
1467 assert!(purge);
1468 }
1469
1470 #[test]
1471 fn up_down_live_under_services() {
1472 assert!(matches!(
1473 cmd(&["bougie", "services", "up", "redis", "-d"]),
1474 Command::Services(ServicesCommand::Up { detach: true, .. })
1475 ));
1476 assert!(matches!(
1477 cmd(&["bougie", "services", "down", "--purge"]),
1478 Command::Services(ServicesCommand::Down { purge: true, .. })
1479 ));
1480 }
1481
1482 #[test]
1483 fn top_level_up_down_are_gone() {
1484 // The deprecated top-level aliases were removed; `up`/`down` only
1485 // exist under `services` now.
1486 assert!(Cli::try_parse_from(["bougie", "up"]).is_err());
1487 assert!(Cli::try_parse_from(["bougie", "down"]).is_err());
1488 }
1489
1490 #[test]
1491 fn server_detach_flag() {
1492 for argv in [
1493 &["bougie", "server", "-d"][..],
1494 &["bougie", "server", "--detach"][..],
1495 ] {
1496 let Command::Server(args) = cmd(argv) else {
1497 panic!("expected server for {argv:?}");
1498 };
1499 assert!(args.serve.detach, "detach should be set for {argv:?}");
1500 }
1501 // The old `--no-attach` spelling is gone.
1502 assert!(Cli::try_parse_from(["bougie", "server", "--no-attach"]).is_err());
1503 }
1504
1505 #[test]
1506 fn make_no_longer_aliases_start() {
1507 // `start` is no longer a clap alias of `make`; it's the
1508 // first-class verb above. `bougie make start` is just `make`
1509 // with the literal task `start`.
1510 let Command::Make { task, .. } = cmd(&["bougie", "make", "start"]) else {
1511 panic!("expected make");
1512 };
1513 assert_eq!(task.as_deref(), Some("start"));
1514
1515 // Bare `bougie make` parses with no task; the dispatcher turns
1516 // that into a task listing.
1517 let Command::Make { task, .. } = cmd(&["bougie", "make"]) else {
1518 panic!("expected make");
1519 };
1520 assert_eq!(task, None);
1521 }
1522}