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