Skip to main content

ubt_cli/
cli.rs

1use clap::{Parser, Subcommand};
2
3// ── Top-level CLI ──────────────────────────────────────────────────────
4
5#[derive(Parser, Debug)]
6#[command(name = "ubt", version, about = "Universal Build Tool")]
7pub struct Cli {
8    /// Enable verbose/debug output
9    #[arg(short, long, global = true)]
10    pub verbose: bool,
11
12    /// Suppress non-essential output
13    #[arg(short, long, global = true)]
14    pub quiet: bool,
15
16    /// Force a specific tool/runtime
17    #[arg(long, global = true, env = "UBT_TOOL")]
18    pub tool: Option<String>,
19
20    #[command(subcommand)]
21    pub command: Command,
22}
23
24// ── Top-level command enum ─────────────────────────────────────────────
25
26#[derive(Subcommand, Debug)]
27pub enum Command {
28    /// Dependency management
29    #[command(subcommand)]
30    Dep(DepCommand),
31
32    /// Build the project
33    Build(BuildArgs),
34
35    /// Start the project (dev server, etc.)
36    Start(PassthroughArgs),
37
38    /// Run a project script
39    Run(RunArgs),
40
41    /// Format source code
42    Fmt(FmtArgs),
43
44    /// Run a file directly
45    #[command(name = "run-file", alias = "run:file")]
46    RunFile(RunFileArgs),
47
48    /// Execute an arbitrary command via the tool
49    Exec(ExecArgs),
50
51    /// Run tests
52    Test(TestArgs),
53
54    /// Lint source code
55    Lint(LintArgs),
56
57    /// Type-check / compile-check without producing output
58    Check(PassthroughArgs),
59
60    /// Database operations
61    #[command(subcommand)]
62    Db(DbCommand),
63
64    /// Initialize a new project configuration
65    Init,
66
67    /// Clean build artifacts
68    Clean(PassthroughArgs),
69
70    /// Create a release
71    Release(ReleaseArgs),
72
73    /// Publish a package
74    Publish(PublishArgs),
75
76    /// Tool information and diagnostics
77    #[command(subcommand)]
78    Tool(ToolCommand),
79
80    /// Configuration management
81    #[command(subcommand)]
82    Config(ConfigCommand),
83
84    /// Show detected tool/runtime info
85    Info,
86
87    /// Generate shell completions
88    Completions(CompletionsArgs),
89
90    /// Catch-all for alias dispatch from [aliases] in ubt.toml
91    #[command(external_subcommand)]
92    External(Vec<String>),
93}
94
95// ── Shared passthrough args ────────────────────────────────────────────
96
97#[derive(Parser, Debug)]
98#[command(trailing_var_arg = true, allow_hyphen_values = true)]
99pub struct PassthroughArgs {
100    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
101    pub args: Vec<String>,
102}
103
104// ── Dep subcommands ────────────────────────────────────────────────────
105
106#[derive(Subcommand, Debug)]
107pub enum DepCommand {
108    /// Install dependencies
109    Install(PassthroughArgs),
110
111    /// Remove a dependency
112    Remove(PassthroughArgs),
113
114    /// Update dependencies
115    Update(PassthroughArgs),
116
117    /// Show outdated dependencies
118    Outdated(PassthroughArgs),
119
120    /// List installed dependencies
121    List(PassthroughArgs),
122
123    /// Audit dependencies for vulnerabilities
124    Audit(PassthroughArgs),
125
126    /// Generate or update lock file
127    Lock(PassthroughArgs),
128
129    /// Explain why a dependency is installed
130    Why(PassthroughArgs),
131}
132
133// ── Build args ─────────────────────────────────────────────────────────
134
135#[derive(Parser, Debug)]
136#[command(trailing_var_arg = true, allow_hyphen_values = true)]
137pub struct BuildArgs {
138    /// Development build
139    #[arg(long)]
140    pub dev: bool,
141
142    /// Watch mode — rebuild on changes
143    #[arg(long)]
144    pub watch: bool,
145
146    /// Clean before building
147    #[arg(long)]
148    pub clean: bool,
149
150    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
151    pub args: Vec<String>,
152}
153
154// ── Run args ───────────────────────────────────────────────────────────
155
156#[derive(Parser, Debug)]
157#[command(trailing_var_arg = true, allow_hyphen_values = true)]
158pub struct RunArgs {
159    /// Script name to run
160    pub script: String,
161
162    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
163    pub args: Vec<String>,
164}
165
166// ── Fmt args ───────────────────────────────────────────────────────────
167
168#[derive(Parser, Debug)]
169#[command(trailing_var_arg = true, allow_hyphen_values = true)]
170pub struct FmtArgs {
171    /// Check formatting without modifying files
172    #[arg(long)]
173    pub check: bool,
174
175    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
176    pub args: Vec<String>,
177}
178
179// ── RunFile args ───────────────────────────────────────────────────────
180
181#[derive(Parser, Debug)]
182#[command(trailing_var_arg = true, allow_hyphen_values = true)]
183pub struct RunFileArgs {
184    /// File to run
185    pub file: String,
186
187    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
188    pub args: Vec<String>,
189}
190
191// ── Exec args ──────────────────────────────────────────────────────────
192
193#[derive(Parser, Debug)]
194#[command(trailing_var_arg = true, allow_hyphen_values = true)]
195pub struct ExecArgs {
196    /// Command to execute
197    pub cmd: String,
198
199    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
200    pub args: Vec<String>,
201}
202
203// ── Test args ──────────────────────────────────────────────────────────
204
205#[derive(Parser, Debug)]
206#[command(trailing_var_arg = true, allow_hyphen_values = true)]
207pub struct TestArgs {
208    /// Watch mode — rerun tests on changes
209    #[arg(long)]
210    pub watch: bool,
211
212    /// Collect coverage information
213    #[arg(long)]
214    pub coverage: bool,
215
216    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
217    pub args: Vec<String>,
218}
219
220// ── Lint args ──────────────────────────────────────────────────────────
221
222#[derive(Parser, Debug)]
223#[command(trailing_var_arg = true, allow_hyphen_values = true)]
224pub struct LintArgs {
225    /// Automatically fix lint issues
226    #[arg(long)]
227    pub fix: bool,
228
229    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
230    pub args: Vec<String>,
231}
232
233// ── Db subcommands ─────────────────────────────────────────────────────
234
235#[derive(Subcommand, Debug)]
236pub enum DbCommand {
237    /// Run database migrations
238    Migrate(PassthroughArgs),
239
240    /// Rollback database migrations
241    Rollback(PassthroughArgs),
242
243    /// Seed the database
244    Seed(PassthroughArgs),
245
246    /// Create the database
247    Create(PassthroughArgs),
248
249    /// Drop the database
250    Drop(DbDropArgs),
251
252    /// Reset the database (drop + create + migrate)
253    Reset(DbResetArgs),
254
255    /// Show migration status
256    Status(PassthroughArgs),
257}
258
259#[derive(Parser, Debug)]
260#[command(trailing_var_arg = true, allow_hyphen_values = true)]
261pub struct DbDropArgs {
262    /// Skip confirmation prompt
263    #[arg(short, long)]
264    pub yes: bool,
265
266    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
267    pub args: Vec<String>,
268}
269
270#[derive(Parser, Debug)]
271#[command(trailing_var_arg = true, allow_hyphen_values = true)]
272pub struct DbResetArgs {
273    /// Skip confirmation prompt
274    #[arg(short, long)]
275    pub yes: bool,
276
277    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
278    pub args: Vec<String>,
279}
280
281// ── Release args ───────────────────────────────────────────────────────
282
283#[derive(Parser, Debug)]
284#[command(trailing_var_arg = true, allow_hyphen_values = true)]
285pub struct ReleaseArgs {
286    /// Perform a dry run without making changes
287    #[arg(long)]
288    pub dry_run: bool,
289
290    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
291    pub args: Vec<String>,
292}
293
294// ── Publish args ───────────────────────────────────────────────────────
295
296#[derive(Parser, Debug)]
297#[command(trailing_var_arg = true, allow_hyphen_values = true)]
298pub struct PublishArgs {
299    /// Skip confirmation prompt
300    #[arg(short, long)]
301    pub yes: bool,
302
303    /// Perform a dry run without publishing
304    #[arg(long)]
305    pub dry_run: bool,
306
307    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
308    pub args: Vec<String>,
309}
310
311// ── Tool subcommands ───────────────────────────────────────────────────
312
313#[derive(Subcommand, Debug)]
314pub enum ToolCommand {
315    /// Show detected tool information
316    Info,
317
318    /// Run diagnostic checks
319    Doctor,
320
321    /// List available tools/plugins
322    List,
323
324    /// Open tool documentation
325    Docs,
326}
327
328// ── Config subcommands ─────────────────────────────────────────────────
329
330#[derive(Subcommand, Debug)]
331pub enum ConfigCommand {
332    /// Show current configuration
333    Show,
334}
335
336// ── Completions args ───────────────────────────────────────────────────
337
338#[derive(Parser, Debug)]
339pub struct CompletionsArgs {
340    /// Shell to generate completions for
341    pub shell: clap_complete::Shell,
342}
343
344// ── Universal flags ────────────────────────────────────────────────────
345
346#[derive(Debug, Default, PartialEq)]
347pub struct UniversalFlags {
348    pub watch: bool,
349    pub coverage: bool,
350    pub dev: bool,
351    pub clean: bool,
352    pub fix: bool,
353    pub check: bool,
354    pub yes: bool,
355    pub dry_run: bool,
356}
357
358// ── Helper functions ───────────────────────────────────────────────────
359
360/// Returns a dot-notation name for the given command variant.
361pub fn parse_command_name(cmd: &Command) -> &'static str {
362    match cmd {
363        Command::Dep(sub) => match sub {
364            DepCommand::Install(..) => "dep.install",
365            DepCommand::Remove(..) => "dep.remove",
366            DepCommand::Update(..) => "dep.update",
367            DepCommand::Outdated(..) => "dep.outdated",
368            DepCommand::List(..) => "dep.list",
369            DepCommand::Audit(..) => "dep.audit",
370            DepCommand::Lock(..) => "dep.lock",
371            DepCommand::Why(..) => "dep.why",
372        },
373        Command::Build(..) => "build",
374        Command::Start(..) => "start",
375        Command::Run(..) => "run",
376        Command::Fmt(..) => "fmt",
377        Command::RunFile(..) => "run-file",
378        Command::Exec(..) => "exec",
379        Command::Test(..) => "test",
380        Command::Lint(..) => "lint",
381        Command::Check(..) => "check",
382        Command::Db(sub) => match sub {
383            DbCommand::Migrate(..) => "db.migrate",
384            DbCommand::Rollback(..) => "db.rollback",
385            DbCommand::Seed(..) => "db.seed",
386            DbCommand::Create(..) => "db.create",
387            DbCommand::Drop(..) => "db.drop",
388            DbCommand::Reset(..) => "db.reset",
389            DbCommand::Status(..) => "db.status",
390        },
391        Command::Init => "init",
392        Command::Clean(..) => "clean",
393        Command::Release(..) => "release",
394        Command::Publish(..) => "publish",
395        Command::Tool(sub) => match sub {
396            ToolCommand::Info => "tool.info",
397            ToolCommand::Doctor => "tool.doctor",
398            ToolCommand::List => "tool.list",
399            ToolCommand::Docs => "tool.docs",
400        },
401        Command::Config(sub) => match sub {
402            ConfigCommand::Show => "config.show",
403        },
404        Command::Info => "info",
405        Command::Completions(..) => "completions",
406        Command::External(..) => unreachable!("External is dispatched before parse_command_name"),
407    }
408}
409
410/// Extracts known universal flags from a command variant.
411pub fn collect_universal_flags(cmd: &Command) -> UniversalFlags {
412    match cmd {
413        Command::Build(args) => UniversalFlags {
414            dev: args.dev,
415            watch: args.watch,
416            clean: args.clean,
417            ..Default::default()
418        },
419        Command::Test(args) => UniversalFlags {
420            watch: args.watch,
421            coverage: args.coverage,
422            ..Default::default()
423        },
424        Command::Lint(args) => UniversalFlags {
425            fix: args.fix,
426            ..Default::default()
427        },
428        Command::Fmt(args) => UniversalFlags {
429            check: args.check,
430            ..Default::default()
431        },
432        Command::Db(DbCommand::Drop(args)) => UniversalFlags {
433            yes: args.yes,
434            ..Default::default()
435        },
436        Command::Db(DbCommand::Reset(args)) => UniversalFlags {
437            yes: args.yes,
438            ..Default::default()
439        },
440        Command::Release(args) => UniversalFlags {
441            dry_run: args.dry_run,
442            ..Default::default()
443        },
444        Command::Publish(args) => UniversalFlags {
445            yes: args.yes,
446            dry_run: args.dry_run,
447            ..Default::default()
448        },
449        Command::External(..) => {
450            unreachable!("External is dispatched before collect_universal_flags")
451        }
452        _ => UniversalFlags::default(),
453    }
454}
455
456/// Collects the passthrough/trailing args from any command variant.
457pub fn collect_remaining_args(cmd: &Command) -> Vec<String> {
458    match cmd {
459        Command::Dep(sub) => match sub {
460            DepCommand::Install(a)
461            | DepCommand::Remove(a)
462            | DepCommand::Update(a)
463            | DepCommand::Outdated(a)
464            | DepCommand::List(a)
465            | DepCommand::Audit(a)
466            | DepCommand::Lock(a)
467            | DepCommand::Why(a) => a.args.clone(),
468        },
469        Command::Build(a) => a.args.clone(),
470        Command::Start(a) => a.args.clone(),
471        Command::Run(a) => a.args.clone(),
472        Command::Fmt(a) => a.args.clone(),
473        Command::RunFile(a) => a.args.clone(),
474        Command::Exec(a) => a.args.clone(),
475        Command::Test(a) => a.args.clone(),
476        Command::Lint(a) => a.args.clone(),
477        Command::Check(a) => a.args.clone(),
478        Command::Db(sub) => match sub {
479            DbCommand::Migrate(a)
480            | DbCommand::Rollback(a)
481            | DbCommand::Seed(a)
482            | DbCommand::Create(a)
483            | DbCommand::Status(a) => a.args.clone(),
484            DbCommand::Drop(a) => a.args.clone(),
485            DbCommand::Reset(a) => a.args.clone(),
486        },
487        Command::Clean(a) => a.args.clone(),
488        Command::Release(a) => a.args.clone(),
489        Command::Publish(a) => a.args.clone(),
490        Command::Init
491        | Command::Info
492        | Command::Tool(_)
493        | Command::Config(_)
494        | Command::Completions(_) => vec![],
495        Command::External(..) => {
496            unreachable!("External is dispatched before collect_remaining_args")
497        }
498    }
499}
500
501// ── Tests ──────────────────────────────────────────────────────────────
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use clap::Parser;
507
508    // Helper to parse CLI from arguments
509    fn parse(args: &[&str]) -> Cli {
510        Cli::parse_from(args)
511    }
512
513    // ── Command name mapping ───────────────────────────────────────────
514
515    #[test]
516    fn command_name_dep_install() {
517        let cli = parse(&["ubt", "dep", "install"]);
518        assert_eq!(parse_command_name(&cli.command), "dep.install");
519    }
520
521    #[test]
522    fn command_name_dep_remove() {
523        let cli = parse(&["ubt", "dep", "remove"]);
524        assert_eq!(parse_command_name(&cli.command), "dep.remove");
525    }
526
527    #[test]
528    fn command_name_dep_update() {
529        let cli = parse(&["ubt", "dep", "update"]);
530        assert_eq!(parse_command_name(&cli.command), "dep.update");
531    }
532
533    #[test]
534    fn command_name_dep_outdated() {
535        let cli = parse(&["ubt", "dep", "outdated"]);
536        assert_eq!(parse_command_name(&cli.command), "dep.outdated");
537    }
538
539    #[test]
540    fn command_name_dep_list() {
541        let cli = parse(&["ubt", "dep", "list"]);
542        assert_eq!(parse_command_name(&cli.command), "dep.list");
543    }
544
545    #[test]
546    fn command_name_dep_audit() {
547        let cli = parse(&["ubt", "dep", "audit"]);
548        assert_eq!(parse_command_name(&cli.command), "dep.audit");
549    }
550
551    #[test]
552    fn command_name_dep_lock() {
553        let cli = parse(&["ubt", "dep", "lock"]);
554        assert_eq!(parse_command_name(&cli.command), "dep.lock");
555    }
556
557    #[test]
558    fn command_name_dep_why() {
559        let cli = parse(&["ubt", "dep", "why"]);
560        assert_eq!(parse_command_name(&cli.command), "dep.why");
561    }
562
563    #[test]
564    fn command_name_build() {
565        let cli = parse(&["ubt", "build"]);
566        assert_eq!(parse_command_name(&cli.command), "build");
567    }
568
569    #[test]
570    fn command_name_start() {
571        let cli = parse(&["ubt", "start"]);
572        assert_eq!(parse_command_name(&cli.command), "start");
573    }
574
575    #[test]
576    fn command_name_run() {
577        let cli = parse(&["ubt", "run", "dev"]);
578        assert_eq!(parse_command_name(&cli.command), "run");
579    }
580
581    #[test]
582    fn command_name_fmt() {
583        let cli = parse(&["ubt", "fmt"]);
584        assert_eq!(parse_command_name(&cli.command), "fmt");
585    }
586
587    #[test]
588    fn command_name_run_file() {
589        let cli = parse(&["ubt", "run-file", "script.ts"]);
590        assert_eq!(parse_command_name(&cli.command), "run-file");
591    }
592
593    #[test]
594    fn command_name_exec() {
595        let cli = parse(&["ubt", "exec", "node"]);
596        assert_eq!(parse_command_name(&cli.command), "exec");
597    }
598
599    #[test]
600    fn command_name_test() {
601        let cli = parse(&["ubt", "test"]);
602        assert_eq!(parse_command_name(&cli.command), "test");
603    }
604
605    #[test]
606    fn command_name_lint() {
607        let cli = parse(&["ubt", "lint"]);
608        assert_eq!(parse_command_name(&cli.command), "lint");
609    }
610
611    #[test]
612    fn command_name_check() {
613        let cli = parse(&["ubt", "check"]);
614        assert_eq!(parse_command_name(&cli.command), "check");
615    }
616
617    #[test]
618    fn command_name_db_migrate() {
619        let cli = parse(&["ubt", "db", "migrate"]);
620        assert_eq!(parse_command_name(&cli.command), "db.migrate");
621    }
622
623    #[test]
624    fn command_name_db_rollback() {
625        let cli = parse(&["ubt", "db", "rollback"]);
626        assert_eq!(parse_command_name(&cli.command), "db.rollback");
627    }
628
629    #[test]
630    fn command_name_db_seed() {
631        let cli = parse(&["ubt", "db", "seed"]);
632        assert_eq!(parse_command_name(&cli.command), "db.seed");
633    }
634
635    #[test]
636    fn command_name_db_create() {
637        let cli = parse(&["ubt", "db", "create"]);
638        assert_eq!(parse_command_name(&cli.command), "db.create");
639    }
640
641    #[test]
642    fn command_name_db_drop() {
643        let cli = parse(&["ubt", "db", "drop"]);
644        assert_eq!(parse_command_name(&cli.command), "db.drop");
645    }
646
647    #[test]
648    fn command_name_db_reset() {
649        let cli = parse(&["ubt", "db", "reset"]);
650        assert_eq!(parse_command_name(&cli.command), "db.reset");
651    }
652
653    #[test]
654    fn command_name_db_status() {
655        let cli = parse(&["ubt", "db", "status"]);
656        assert_eq!(parse_command_name(&cli.command), "db.status");
657    }
658
659    #[test]
660    fn command_name_init() {
661        let cli = parse(&["ubt", "init"]);
662        assert_eq!(parse_command_name(&cli.command), "init");
663    }
664
665    #[test]
666    fn command_name_clean() {
667        let cli = parse(&["ubt", "clean"]);
668        assert_eq!(parse_command_name(&cli.command), "clean");
669    }
670
671    #[test]
672    fn command_name_release() {
673        let cli = parse(&["ubt", "release"]);
674        assert_eq!(parse_command_name(&cli.command), "release");
675    }
676
677    #[test]
678    fn command_name_publish() {
679        let cli = parse(&["ubt", "publish"]);
680        assert_eq!(parse_command_name(&cli.command), "publish");
681    }
682
683    #[test]
684    fn command_name_tool_info() {
685        let cli = parse(&["ubt", "tool", "info"]);
686        assert_eq!(parse_command_name(&cli.command), "tool.info");
687    }
688
689    #[test]
690    fn command_name_tool_doctor() {
691        let cli = parse(&["ubt", "tool", "doctor"]);
692        assert_eq!(parse_command_name(&cli.command), "tool.doctor");
693    }
694
695    #[test]
696    fn command_name_tool_list() {
697        let cli = parse(&["ubt", "tool", "list"]);
698        assert_eq!(parse_command_name(&cli.command), "tool.list");
699    }
700
701    #[test]
702    fn command_name_tool_docs() {
703        let cli = parse(&["ubt", "tool", "docs"]);
704        assert_eq!(parse_command_name(&cli.command), "tool.docs");
705    }
706
707    #[test]
708    fn command_name_config_show() {
709        let cli = parse(&["ubt", "config", "show"]);
710        assert_eq!(parse_command_name(&cli.command), "config.show");
711    }
712
713    #[test]
714    fn command_name_info() {
715        let cli = parse(&["ubt", "info"]);
716        assert_eq!(parse_command_name(&cli.command), "info");
717    }
718
719    #[test]
720    fn command_name_completions() {
721        let cli = parse(&["ubt", "completions", "bash"]);
722        assert_eq!(parse_command_name(&cli.command), "completions");
723    }
724
725    // ── Global flag extraction ─────────────────────────────────────────
726
727    #[test]
728    fn global_verbose_flag() {
729        let cli = parse(&["ubt", "-v", "info"]);
730        assert!(cli.verbose);
731        assert!(!cli.quiet);
732    }
733
734    #[test]
735    fn global_quiet_flag() {
736        let cli = parse(&["ubt", "-q", "info"]);
737        assert!(!cli.verbose);
738        assert!(cli.quiet);
739    }
740
741    #[test]
742    fn global_tool_flag() {
743        let cli = parse(&["ubt", "--tool", "npm", "info"]);
744        assert_eq!(cli.tool, Some("npm".to_string()));
745    }
746
747    #[test]
748    fn global_tool_flag_absent() {
749        let cli = parse(&["ubt", "info"]);
750        assert_eq!(cli.tool, None);
751    }
752
753    // ── Universal flag extraction ──────────────────────────────────────
754
755    #[test]
756    fn universal_flags_build_dev() {
757        let cli = parse(&["ubt", "build", "--dev"]);
758        let flags = collect_universal_flags(&cli.command);
759        assert!(flags.dev);
760        assert!(!flags.watch);
761        assert!(!flags.clean);
762    }
763
764    #[test]
765    fn universal_flags_build_watch_clean() {
766        let cli = parse(&["ubt", "build", "--watch", "--clean"]);
767        let flags = collect_universal_flags(&cli.command);
768        assert!(flags.watch);
769        assert!(flags.clean);
770    }
771
772    #[test]
773    fn universal_flags_test_coverage_watch() {
774        let cli = parse(&["ubt", "test", "--coverage", "--watch"]);
775        let flags = collect_universal_flags(&cli.command);
776        assert!(flags.coverage);
777        assert!(flags.watch);
778    }
779
780    #[test]
781    fn universal_flags_lint_fix() {
782        let cli = parse(&["ubt", "lint", "--fix"]);
783        let flags = collect_universal_flags(&cli.command);
784        assert!(flags.fix);
785    }
786
787    #[test]
788    fn universal_flags_fmt_check() {
789        let cli = parse(&["ubt", "fmt", "--check"]);
790        let flags = collect_universal_flags(&cli.command);
791        assert!(flags.check);
792    }
793
794    #[test]
795    fn universal_flags_db_drop_yes() {
796        let cli = parse(&["ubt", "db", "drop", "--yes"]);
797        let flags = collect_universal_flags(&cli.command);
798        assert!(flags.yes);
799    }
800
801    #[test]
802    fn universal_flags_db_reset_yes() {
803        let cli = parse(&["ubt", "db", "reset", "-y"]);
804        let flags = collect_universal_flags(&cli.command);
805        assert!(flags.yes);
806    }
807
808    #[test]
809    fn universal_flags_publish_dry_run() {
810        let cli = parse(&["ubt", "publish", "--dry-run"]);
811        let flags = collect_universal_flags(&cli.command);
812        assert!(flags.dry_run);
813        assert!(!flags.yes);
814    }
815
816    #[test]
817    fn universal_flags_publish_yes_and_dry_run() {
818        let cli = parse(&["ubt", "publish", "--yes", "--dry-run"]);
819        let flags = collect_universal_flags(&cli.command);
820        assert!(flags.yes);
821        assert!(flags.dry_run);
822    }
823
824    #[test]
825    fn universal_flags_release_dry_run() {
826        let cli = parse(&["ubt", "release", "--dry-run"]);
827        let flags = collect_universal_flags(&cli.command);
828        assert!(flags.dry_run);
829    }
830
831    #[test]
832    fn universal_flags_default_for_info() {
833        let cli = parse(&["ubt", "info"]);
834        let flags = collect_universal_flags(&cli.command);
835        assert_eq!(flags, UniversalFlags::default());
836    }
837
838    // ── Remaining args collection ──────────────────────────────────────
839
840    #[test]
841    fn remaining_args_build() {
842        let cli = parse(&["ubt", "build", "--dev", "--", "extra1", "extra2"]);
843        let args = collect_remaining_args(&cli.command);
844        assert!(args.contains(&"extra1".to_string()));
845        assert!(args.contains(&"extra2".to_string()));
846    }
847
848    #[test]
849    fn remaining_args_start() {
850        let cli = parse(&["ubt", "start", "foo", "bar"]);
851        let args = collect_remaining_args(&cli.command);
852        assert_eq!(args, vec!["foo", "bar"]);
853    }
854
855    #[test]
856    fn remaining_args_test_with_flags() {
857        let cli = parse(&["ubt", "test", "--watch", "some-pattern"]);
858        let args = collect_remaining_args(&cli.command);
859        assert_eq!(args, vec!["some-pattern"]);
860    }
861
862    #[test]
863    fn remaining_args_dep_install() {
864        let cli = parse(&["ubt", "dep", "install", "lodash", "express"]);
865        let args = collect_remaining_args(&cli.command);
866        assert_eq!(args, vec!["lodash", "express"]);
867    }
868
869    #[test]
870    fn remaining_args_run() {
871        let cli = parse(&["ubt", "run", "dev", "--port", "3000"]);
872        let args = collect_remaining_args(&cli.command);
873        assert_eq!(args, vec!["--port", "3000"]);
874    }
875
876    #[test]
877    fn remaining_args_empty_for_init() {
878        let cli = parse(&["ubt", "init"]);
879        let args = collect_remaining_args(&cli.command);
880        assert!(args.is_empty());
881    }
882
883    #[test]
884    fn remaining_args_empty_for_info() {
885        let cli = parse(&["ubt", "info"]);
886        let args = collect_remaining_args(&cli.command);
887        assert!(args.is_empty());
888    }
889
890    #[test]
891    fn remaining_args_empty_for_tool() {
892        let cli = parse(&["ubt", "tool", "info"]);
893        let args = collect_remaining_args(&cli.command);
894        assert!(args.is_empty());
895    }
896
897    #[test]
898    fn remaining_args_exec() {
899        let cli = parse(&["ubt", "exec", "node", "-e", "console.log(1)"]);
900        let args = collect_remaining_args(&cli.command);
901        assert_eq!(args, vec!["-e", "console.log(1)"]);
902    }
903
904    // ── Help & version output ──────────────────────────────────────────
905
906    #[test]
907    fn help_output_produces_error() {
908        let result = Cli::try_parse_from(["ubt", "--help"]);
909        assert!(result.is_err());
910        let err = result.unwrap_err();
911        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
912    }
913
914    #[test]
915    fn version_output_produces_error() {
916        let result = Cli::try_parse_from(["ubt", "--version"]);
917        assert!(result.is_err());
918        let err = result.unwrap_err();
919        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
920    }
921
922    // ── Error on unknown command ───────────────────────────────────────
923
924    #[test]
925    fn unknown_command_routes_to_external() {
926        let cli = parse(&["ubt", "nonexistent"]);
927        assert!(matches!(cli.command, Command::External(ref args) if args[0] == "nonexistent"));
928    }
929
930    // ── Completions shell parsing ──────────────────────────────────────
931
932    #[test]
933    fn completions_bash() {
934        let cli = parse(&["ubt", "completions", "bash"]);
935        if let Command::Completions(args) = &cli.command {
936            assert_eq!(args.shell, clap_complete::Shell::Bash);
937        } else {
938            panic!("expected Completions command");
939        }
940    }
941
942    #[test]
943    fn completions_zsh() {
944        let cli = parse(&["ubt", "completions", "zsh"]);
945        if let Command::Completions(args) = &cli.command {
946            assert_eq!(args.shell, clap_complete::Shell::Zsh);
947        } else {
948            panic!("expected Completions command");
949        }
950    }
951
952    #[test]
953    fn completions_fish() {
954        let cli = parse(&["ubt", "completions", "fish"]);
955        if let Command::Completions(args) = &cli.command {
956            assert_eq!(args.shell, clap_complete::Shell::Fish);
957        } else {
958            panic!("expected Completions command");
959        }
960    }
961
962    #[test]
963    fn completions_powershell() {
964        let cli = parse(&["ubt", "completions", "powershell"]);
965        if let Command::Completions(args) = &cli.command {
966            assert_eq!(args.shell, clap_complete::Shell::PowerShell);
967        } else {
968            panic!("expected Completions command");
969        }
970    }
971
972    // ── run-file alias ─────────────────────────────────────────────────
973
974    #[test]
975    fn run_file_alias() {
976        let cli = parse(&["ubt", "run:file", "script.ts"]);
977        assert_eq!(parse_command_name(&cli.command), "run-file");
978    }
979}