Skip to main content

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}