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