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