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