refactor: Migrate CLI parser to usage-rs (#13890)
### Description
Migrate the primary Turborepo CLI parser from `clap` to `usage-rs`
6.5.0.
- Refactor parser-facing command structures where `usage-rs` requires
owned, unboxed argument groups.
- Preserve implicit `turbo <task>` routing, global flags, aliases,
optional values, conflicts, requirements, `--` pass-through arguments,
`--single-package`, and repeated-scalar rejection.
- Replace clap help, diagnostics, and completion generation with
usage-rs equivalents.
- Add deterministic arbitrary-argv property coverage and an
ASan/libFuzzer target, including non-UTF-8 `OsString` inputs on Unix.
- Update CLI snapshots for the expected cosmetic help and diagnostic
changes.
#### Benchmarks
Measured on Apple Silicon macOS using the `release-turborepo` profile:
| Measurement | clap | usage-rs | Difference |
| --- | ---: | ---: | ---: |
| Cold build wall time | 285.66s | 284.24s | 0.5% faster |
| Build max RSS | 3.48 GB | 3.37 GB | 3.2% lower |
| Binary size | 45,090,512 B | 45,479,680 B | 0.86% larger |
| `--help` | 10.9 ± 2.0 ms | 10.5 ± 0.5 ms | ~4% faster |
| Invalid argument | 11.4 ± 3.0 ms | 10.7 ± 2.2 ms | ~9% faster |
The runtime differences are close to process startup noise. `clap` also
remains in the final dependency graph through other crates, so this
migration alone does not remove it from the binary.
#### Differential fuzzing
I preserved a release build of the clap implementation from the
migration base and ran it beside the usage-rs build. The differential
corpus covered explicit and implicit `run`, other subcommands, aliases,
global flags in different positions, optional values, invalid values,
conflicting flags, repeated scalar and repeatable flags, Unicode values,
unknown flags, and `--` pass-through arguments. Both binaries received
the same argv. I compared parser acceptance/rejection and exit behavior,
then compared focused stdout/stderr for stable cases such as help,
unknown arguments, conflicts, and duplicate scalar values; diagnostic
prose and formatting were intentionally treated as cosmetic.
This differential work found that usage-rs accepted a repeated nested
scalar such as two `--concurrency` values where clap rejected it. The
migration now rejects repeated scalar flags explicitly and retains a
regression test. Grammar-guided sentinel runs were also used as a
discovery tool for differences in error precedence and command-local
flag placement; those findings were checked against the existing CLI
regression suite rather than treating first-error wording as semantic
equivalence. This sat alongside, rather than replacing, the
arbitrary-byte property test and ASan/libFuzzer campaigns described
below.
#### Patch-level compatibility escape hatch
A missed parser incompatibility can be repaired inside the usage-rs path
in a normal patch release, without user configuration, a second binary,
or reverting the migration. `Args::parse_args` owns the complete
`Vec<OsString>` immediately before `Args::try_parse_from`, and it
already uses this boundary for compatibility behavior such as stripping
and restoring `--single-package`, rejecting clap-incompatible duplicate
scalar flags, and rejecting values attached to boolean switches. A
targeted hotfix can therefore normalize a legacy spelling or argv shape
before parsing, or adjust the typed `Args` immediately afterward, before
any command handler runs.
For example, if usage-rs stopped accepting the existing `--dry` alias,
the compatibility patch can live at the pre-parse boundary:
```rust
let words: Vec<OsString> = single_package_free
.map(|word| match word.to_str() {
Some("--dry") => OsString::from("--dry-run"),
Some(value) if value.starts_with("--dry=") => {
OsString::from(value.replacen("--dry=", "--dry-run=", 1))
}
_ => word,
})
.collect();
let mut args = Args::try_parse_from(&refs)
.map_err(|error| Args::render_failure(&refs[1..], &error))?;
```
This is transparent to users: the same command line continues to work
after installing the patch release. The same boundary can handle renamed
flags, changed optional-value syntax, argument ordering, legacy aliases,
and other token-level differences. If the parser accepts the tokens but
binds them differently, the patch can instead mutate the typed `args`
directly after `try_parse_from` and before returning from `parse_args`;
command handlers only receive the corrected representation.
I proved this locally and removed it without committing: I temporarily
removed the declared `--dry` alias from usage-rs, added the
normalization above, and verified both `turbo run build --dry` and
`turbo run build --dry=json` still parsed successfully. The existing
focused typed parser tests passed (2 passed, 0 failed), and both
end-to-end binary invocations exited 0.
#### Expected cosmetic changes
- Help uses usage-rs layout, wrapping, headings, and metavariable names.
- Diagnostics are shorter and use usage-rs wording.
- Conflict diagnostics can name flags in a different order.
- Shell completions use usage-rs's smaller dynamic completion protocol
rather than clap's large static script.
### Testing Instructions
- Exercise representative explicit and implicit runs, such as `turbo run
build` and `turbo build`.
- Verify global flags before and after subcommands.
- Verify unknown flags and repeated scalar options fail.
- Verify arguments after `--` reach the underlying task unchanged.
- Generate completions for each supported shell.
Fuzz validation performed:
- 100,000 deterministic property cases with arbitrary byte-backed
arguments, including non-UTF-8 Unix arguments.
- 1,000,000 ASan/libFuzzer executions with no crashes or hangs.
- An additional 100,000 ASan/libFuzzer executions after adding
repeated-scalar enforcement, also with no crashes or hangs.