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(DocsArgs),
326}
327
328#[derive(Parser, Debug)]
329pub struct DocsArgs {
330    /// Open the documentation URL in the system browser
331    #[arg(long)]
332    pub open: bool,
333}
334
335// ── Config subcommands ─────────────────────────────────────────────────
336
337#[derive(Subcommand, Debug)]
338pub enum ConfigCommand {
339    /// Show current configuration
340    Show,
341}
342
343// ── Completions args ───────────────────────────────────────────────────
344
345#[derive(Parser, Debug)]
346pub struct CompletionsArgs {
347    /// Shell to generate completions for
348    pub shell: clap_complete::Shell,
349}
350
351// ── Universal flags ────────────────────────────────────────────────────
352
353#[derive(Debug, Default, PartialEq)]
354pub struct UniversalFlags {
355    pub watch: bool,
356    pub coverage: bool,
357    pub dev: bool,
358    pub clean: bool,
359    pub fix: bool,
360    pub check: bool,
361    pub yes: bool,
362    pub dry_run: bool,
363}
364
365// ── Helper functions ───────────────────────────────────────────────────
366
367/// Returns the dot-notation command name and an optional reference to the
368/// passthrough args vec for the given command variant.
369pub fn command_parts(cmd: &Command) -> (&'static str, Option<&Vec<String>>) {
370    match cmd {
371        Command::Dep(sub) => match sub {
372            DepCommand::Install(a) => ("dep.install", Some(&a.args)),
373            DepCommand::Remove(a) => ("dep.remove", Some(&a.args)),
374            DepCommand::Update(a) => ("dep.update", Some(&a.args)),
375            DepCommand::Outdated(a) => ("dep.outdated", Some(&a.args)),
376            DepCommand::List(a) => ("dep.list", Some(&a.args)),
377            DepCommand::Audit(a) => ("dep.audit", Some(&a.args)),
378            DepCommand::Lock(a) => ("dep.lock", Some(&a.args)),
379            DepCommand::Why(a) => ("dep.why", Some(&a.args)),
380        },
381        Command::Build(a) => ("build", Some(&a.args)),
382        Command::Start(a) => ("start", Some(&a.args)),
383        Command::Run(a) => ("run", Some(&a.args)),
384        Command::Fmt(a) => ("fmt", Some(&a.args)),
385        Command::RunFile(a) => ("run-file", Some(&a.args)),
386        Command::Exec(a) => ("exec", Some(&a.args)),
387        Command::Test(a) => ("test", Some(&a.args)),
388        Command::Lint(a) => ("lint", Some(&a.args)),
389        Command::Check(a) => ("check", Some(&a.args)),
390        Command::Db(sub) => match sub {
391            DbCommand::Migrate(a) => ("db.migrate", Some(&a.args)),
392            DbCommand::Rollback(a) => ("db.rollback", Some(&a.args)),
393            DbCommand::Seed(a) => ("db.seed", Some(&a.args)),
394            DbCommand::Create(a) => ("db.create", Some(&a.args)),
395            DbCommand::Drop(a) => ("db.drop", Some(&a.args)),
396            DbCommand::Reset(a) => ("db.reset", Some(&a.args)),
397            DbCommand::Status(a) => ("db.status", Some(&a.args)),
398        },
399        Command::Init => ("init", None),
400        Command::Clean(a) => ("clean", Some(&a.args)),
401        Command::Release(a) => ("release", Some(&a.args)),
402        Command::Publish(a) => ("publish", Some(&a.args)),
403        Command::Tool(sub) => match sub {
404            ToolCommand::Info => ("tool.info", None),
405            ToolCommand::Doctor => ("tool.doctor", None),
406            ToolCommand::List => ("tool.list", None),
407            ToolCommand::Docs(_) => ("tool.docs", None),
408        },
409        Command::Config(sub) => match sub {
410            ConfigCommand::Show => ("config.show", None),
411        },
412        Command::Info => ("info", None),
413        Command::Completions(..) => ("completions", None),
414        Command::External(..) => unreachable!("External is dispatched before command_parts"),
415    }
416}
417
418/// Returns a dot-notation name for the given command variant.
419pub fn parse_command_name(cmd: &Command) -> &'static str {
420    command_parts(cmd).0
421}
422
423/// Extracts known universal flags from a command variant.
424pub fn collect_universal_flags(cmd: &Command) -> UniversalFlags {
425    match cmd {
426        Command::Build(args) => UniversalFlags {
427            dev: args.dev,
428            watch: args.watch,
429            clean: args.clean,
430            ..Default::default()
431        },
432        Command::Test(args) => UniversalFlags {
433            watch: args.watch,
434            coverage: args.coverage,
435            ..Default::default()
436        },
437        Command::Lint(args) => UniversalFlags {
438            fix: args.fix,
439            ..Default::default()
440        },
441        Command::Fmt(args) => UniversalFlags {
442            check: args.check,
443            ..Default::default()
444        },
445        Command::Db(DbCommand::Drop(args)) => UniversalFlags {
446            yes: args.yes,
447            ..Default::default()
448        },
449        Command::Db(DbCommand::Reset(args)) => UniversalFlags {
450            yes: args.yes,
451            ..Default::default()
452        },
453        Command::Release(args) => UniversalFlags {
454            dry_run: args.dry_run,
455            ..Default::default()
456        },
457        Command::Publish(args) => UniversalFlags {
458            yes: args.yes,
459            dry_run: args.dry_run,
460            ..Default::default()
461        },
462        Command::External(..) => {
463            unreachable!("External is dispatched before collect_universal_flags")
464        }
465        _ => UniversalFlags::default(),
466    }
467}
468
469/// Collects the passthrough/trailing args from any command variant.
470pub fn collect_remaining_args(cmd: &Command) -> Vec<String> {
471    command_parts(cmd).1.cloned().unwrap_or_default()
472}
473
474// ── Tests ──────────────────────────────────────────────────────────────
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use clap::Parser;
480
481    // Helper to parse CLI from arguments
482    fn parse(args: &[&str]) -> Cli {
483        Cli::parse_from(args)
484    }
485
486    // ── Command name mapping ───────────────────────────────────────────
487
488    #[test]
489    fn command_name_dep_install() {
490        let cli = parse(&["ubt", "dep", "install"]);
491        assert_eq!(parse_command_name(&cli.command), "dep.install");
492    }
493
494    #[test]
495    fn command_name_dep_remove() {
496        let cli = parse(&["ubt", "dep", "remove"]);
497        assert_eq!(parse_command_name(&cli.command), "dep.remove");
498    }
499
500    #[test]
501    fn command_name_dep_update() {
502        let cli = parse(&["ubt", "dep", "update"]);
503        assert_eq!(parse_command_name(&cli.command), "dep.update");
504    }
505
506    #[test]
507    fn command_name_dep_outdated() {
508        let cli = parse(&["ubt", "dep", "outdated"]);
509        assert_eq!(parse_command_name(&cli.command), "dep.outdated");
510    }
511
512    #[test]
513    fn command_name_dep_list() {
514        let cli = parse(&["ubt", "dep", "list"]);
515        assert_eq!(parse_command_name(&cli.command), "dep.list");
516    }
517
518    #[test]
519    fn command_name_dep_audit() {
520        let cli = parse(&["ubt", "dep", "audit"]);
521        assert_eq!(parse_command_name(&cli.command), "dep.audit");
522    }
523
524    #[test]
525    fn command_name_dep_lock() {
526        let cli = parse(&["ubt", "dep", "lock"]);
527        assert_eq!(parse_command_name(&cli.command), "dep.lock");
528    }
529
530    #[test]
531    fn command_name_dep_why() {
532        let cli = parse(&["ubt", "dep", "why"]);
533        assert_eq!(parse_command_name(&cli.command), "dep.why");
534    }
535
536    #[test]
537    fn command_name_build() {
538        let cli = parse(&["ubt", "build"]);
539        assert_eq!(parse_command_name(&cli.command), "build");
540    }
541
542    #[test]
543    fn command_name_start() {
544        let cli = parse(&["ubt", "start"]);
545        assert_eq!(parse_command_name(&cli.command), "start");
546    }
547
548    #[test]
549    fn command_name_run() {
550        let cli = parse(&["ubt", "run", "dev"]);
551        assert_eq!(parse_command_name(&cli.command), "run");
552    }
553
554    #[test]
555    fn command_name_fmt() {
556        let cli = parse(&["ubt", "fmt"]);
557        assert_eq!(parse_command_name(&cli.command), "fmt");
558    }
559
560    #[test]
561    fn command_name_run_file() {
562        let cli = parse(&["ubt", "run-file", "script.ts"]);
563        assert_eq!(parse_command_name(&cli.command), "run-file");
564    }
565
566    #[test]
567    fn command_name_exec() {
568        let cli = parse(&["ubt", "exec", "node"]);
569        assert_eq!(parse_command_name(&cli.command), "exec");
570    }
571
572    #[test]
573    fn command_name_test() {
574        let cli = parse(&["ubt", "test"]);
575        assert_eq!(parse_command_name(&cli.command), "test");
576    }
577
578    #[test]
579    fn command_name_lint() {
580        let cli = parse(&["ubt", "lint"]);
581        assert_eq!(parse_command_name(&cli.command), "lint");
582    }
583
584    #[test]
585    fn command_name_check() {
586        let cli = parse(&["ubt", "check"]);
587        assert_eq!(parse_command_name(&cli.command), "check");
588    }
589
590    #[test]
591    fn command_name_db_migrate() {
592        let cli = parse(&["ubt", "db", "migrate"]);
593        assert_eq!(parse_command_name(&cli.command), "db.migrate");
594    }
595
596    #[test]
597    fn command_name_db_rollback() {
598        let cli = parse(&["ubt", "db", "rollback"]);
599        assert_eq!(parse_command_name(&cli.command), "db.rollback");
600    }
601
602    #[test]
603    fn command_name_db_seed() {
604        let cli = parse(&["ubt", "db", "seed"]);
605        assert_eq!(parse_command_name(&cli.command), "db.seed");
606    }
607
608    #[test]
609    fn command_name_db_create() {
610        let cli = parse(&["ubt", "db", "create"]);
611        assert_eq!(parse_command_name(&cli.command), "db.create");
612    }
613
614    #[test]
615    fn command_name_db_drop() {
616        let cli = parse(&["ubt", "db", "drop"]);
617        assert_eq!(parse_command_name(&cli.command), "db.drop");
618    }
619
620    #[test]
621    fn command_name_db_reset() {
622        let cli = parse(&["ubt", "db", "reset"]);
623        assert_eq!(parse_command_name(&cli.command), "db.reset");
624    }
625
626    #[test]
627    fn command_name_db_status() {
628        let cli = parse(&["ubt", "db", "status"]);
629        assert_eq!(parse_command_name(&cli.command), "db.status");
630    }
631
632    #[test]
633    fn command_name_init() {
634        let cli = parse(&["ubt", "init"]);
635        assert_eq!(parse_command_name(&cli.command), "init");
636    }
637
638    #[test]
639    fn command_name_clean() {
640        let cli = parse(&["ubt", "clean"]);
641        assert_eq!(parse_command_name(&cli.command), "clean");
642    }
643
644    #[test]
645    fn command_name_release() {
646        let cli = parse(&["ubt", "release"]);
647        assert_eq!(parse_command_name(&cli.command), "release");
648    }
649
650    #[test]
651    fn command_name_publish() {
652        let cli = parse(&["ubt", "publish"]);
653        assert_eq!(parse_command_name(&cli.command), "publish");
654    }
655
656    #[test]
657    fn command_name_tool_info() {
658        let cli = parse(&["ubt", "tool", "info"]);
659        assert_eq!(parse_command_name(&cli.command), "tool.info");
660    }
661
662    #[test]
663    fn command_name_tool_doctor() {
664        let cli = parse(&["ubt", "tool", "doctor"]);
665        assert_eq!(parse_command_name(&cli.command), "tool.doctor");
666    }
667
668    #[test]
669    fn command_name_tool_list() {
670        let cli = parse(&["ubt", "tool", "list"]);
671        assert_eq!(parse_command_name(&cli.command), "tool.list");
672    }
673
674    #[test]
675    fn command_name_tool_docs() {
676        let cli = parse(&["ubt", "tool", "docs"]);
677        assert_eq!(parse_command_name(&cli.command), "tool.docs");
678    }
679
680    #[test]
681    fn command_name_config_show() {
682        let cli = parse(&["ubt", "config", "show"]);
683        assert_eq!(parse_command_name(&cli.command), "config.show");
684    }
685
686    #[test]
687    fn command_name_info() {
688        let cli = parse(&["ubt", "info"]);
689        assert_eq!(parse_command_name(&cli.command), "info");
690    }
691
692    #[test]
693    fn command_name_completions() {
694        let cli = parse(&["ubt", "completions", "bash"]);
695        assert_eq!(parse_command_name(&cli.command), "completions");
696    }
697
698    // ── Global flag extraction ─────────────────────────────────────────
699
700    #[test]
701    fn global_verbose_flag() {
702        let cli = parse(&["ubt", "-v", "info"]);
703        assert!(cli.verbose);
704        assert!(!cli.quiet);
705    }
706
707    #[test]
708    fn global_quiet_flag() {
709        let cli = parse(&["ubt", "-q", "info"]);
710        assert!(!cli.verbose);
711        assert!(cli.quiet);
712    }
713
714    #[test]
715    fn global_tool_flag() {
716        let cli = parse(&["ubt", "--tool", "npm", "info"]);
717        assert_eq!(cli.tool, Some("npm".to_string()));
718    }
719
720    #[test]
721    fn global_tool_flag_absent() {
722        let cli = parse(&["ubt", "info"]);
723        assert_eq!(cli.tool, None);
724    }
725
726    // ── Universal flag extraction ──────────────────────────────────────
727
728    #[test]
729    fn universal_flags_build_dev() {
730        let cli = parse(&["ubt", "build", "--dev"]);
731        let flags = collect_universal_flags(&cli.command);
732        assert!(flags.dev);
733        assert!(!flags.watch);
734        assert!(!flags.clean);
735    }
736
737    #[test]
738    fn universal_flags_build_watch_clean() {
739        let cli = parse(&["ubt", "build", "--watch", "--clean"]);
740        let flags = collect_universal_flags(&cli.command);
741        assert!(flags.watch);
742        assert!(flags.clean);
743    }
744
745    #[test]
746    fn universal_flags_test_coverage_watch() {
747        let cli = parse(&["ubt", "test", "--coverage", "--watch"]);
748        let flags = collect_universal_flags(&cli.command);
749        assert!(flags.coverage);
750        assert!(flags.watch);
751    }
752
753    #[test]
754    fn universal_flags_lint_fix() {
755        let cli = parse(&["ubt", "lint", "--fix"]);
756        let flags = collect_universal_flags(&cli.command);
757        assert!(flags.fix);
758    }
759
760    #[test]
761    fn universal_flags_fmt_check() {
762        let cli = parse(&["ubt", "fmt", "--check"]);
763        let flags = collect_universal_flags(&cli.command);
764        assert!(flags.check);
765    }
766
767    #[test]
768    fn universal_flags_db_drop_yes() {
769        let cli = parse(&["ubt", "db", "drop", "--yes"]);
770        let flags = collect_universal_flags(&cli.command);
771        assert!(flags.yes);
772    }
773
774    #[test]
775    fn universal_flags_db_reset_yes() {
776        let cli = parse(&["ubt", "db", "reset", "-y"]);
777        let flags = collect_universal_flags(&cli.command);
778        assert!(flags.yes);
779    }
780
781    #[test]
782    fn universal_flags_publish_dry_run() {
783        let cli = parse(&["ubt", "publish", "--dry-run"]);
784        let flags = collect_universal_flags(&cli.command);
785        assert!(flags.dry_run);
786        assert!(!flags.yes);
787    }
788
789    #[test]
790    fn universal_flags_publish_yes_and_dry_run() {
791        let cli = parse(&["ubt", "publish", "--yes", "--dry-run"]);
792        let flags = collect_universal_flags(&cli.command);
793        assert!(flags.yes);
794        assert!(flags.dry_run);
795    }
796
797    #[test]
798    fn universal_flags_release_dry_run() {
799        let cli = parse(&["ubt", "release", "--dry-run"]);
800        let flags = collect_universal_flags(&cli.command);
801        assert!(flags.dry_run);
802    }
803
804    #[test]
805    fn universal_flags_default_for_info() {
806        let cli = parse(&["ubt", "info"]);
807        let flags = collect_universal_flags(&cli.command);
808        assert_eq!(flags, UniversalFlags::default());
809    }
810
811    // ── Remaining args collection ──────────────────────────────────────
812
813    #[test]
814    fn remaining_args_build() {
815        let cli = parse(&["ubt", "build", "--dev", "--", "extra1", "extra2"]);
816        let args = collect_remaining_args(&cli.command);
817        assert!(args.contains(&"extra1".to_string()));
818        assert!(args.contains(&"extra2".to_string()));
819    }
820
821    #[test]
822    fn remaining_args_start() {
823        let cli = parse(&["ubt", "start", "foo", "bar"]);
824        let args = collect_remaining_args(&cli.command);
825        assert_eq!(args, vec!["foo", "bar"]);
826    }
827
828    #[test]
829    fn remaining_args_test_with_flags() {
830        let cli = parse(&["ubt", "test", "--watch", "some-pattern"]);
831        let args = collect_remaining_args(&cli.command);
832        assert_eq!(args, vec!["some-pattern"]);
833    }
834
835    #[test]
836    fn remaining_args_dep_install() {
837        let cli = parse(&["ubt", "dep", "install", "lodash", "express"]);
838        let args = collect_remaining_args(&cli.command);
839        assert_eq!(args, vec!["lodash", "express"]);
840    }
841
842    #[test]
843    fn remaining_args_run() {
844        let cli = parse(&["ubt", "run", "dev", "--port", "3000"]);
845        let args = collect_remaining_args(&cli.command);
846        assert_eq!(args, vec!["--port", "3000"]);
847    }
848
849    #[test]
850    fn remaining_args_empty_for_init() {
851        let cli = parse(&["ubt", "init"]);
852        let args = collect_remaining_args(&cli.command);
853        assert!(args.is_empty());
854    }
855
856    #[test]
857    fn remaining_args_empty_for_info() {
858        let cli = parse(&["ubt", "info"]);
859        let args = collect_remaining_args(&cli.command);
860        assert!(args.is_empty());
861    }
862
863    #[test]
864    fn remaining_args_empty_for_tool() {
865        let cli = parse(&["ubt", "tool", "info"]);
866        let args = collect_remaining_args(&cli.command);
867        assert!(args.is_empty());
868    }
869
870    #[test]
871    fn remaining_args_exec() {
872        let cli = parse(&["ubt", "exec", "node", "-e", "console.log(1)"]);
873        let args = collect_remaining_args(&cli.command);
874        assert_eq!(args, vec!["-e", "console.log(1)"]);
875    }
876
877    // ── Help & version output ──────────────────────────────────────────
878
879    #[test]
880    fn help_output_produces_error() {
881        let result = Cli::try_parse_from(["ubt", "--help"]);
882        assert!(result.is_err());
883        let err = result.unwrap_err();
884        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
885    }
886
887    #[test]
888    fn version_output_produces_error() {
889        let result = Cli::try_parse_from(["ubt", "--version"]);
890        assert!(result.is_err());
891        let err = result.unwrap_err();
892        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
893    }
894
895    // ── Error on unknown command ───────────────────────────────────────
896
897    #[test]
898    fn unknown_command_routes_to_external() {
899        let cli = parse(&["ubt", "nonexistent"]);
900        assert!(matches!(cli.command, Command::External(ref args) if args[0] == "nonexistent"));
901    }
902
903    // ── Completions shell parsing ──────────────────────────────────────
904
905    #[test]
906    fn completions_bash() {
907        let cli = parse(&["ubt", "completions", "bash"]);
908        if let Command::Completions(args) = &cli.command {
909            assert_eq!(args.shell, clap_complete::Shell::Bash);
910        } else {
911            panic!("expected Completions command");
912        }
913    }
914
915    #[test]
916    fn completions_zsh() {
917        let cli = parse(&["ubt", "completions", "zsh"]);
918        if let Command::Completions(args) = &cli.command {
919            assert_eq!(args.shell, clap_complete::Shell::Zsh);
920        } else {
921            panic!("expected Completions command");
922        }
923    }
924
925    #[test]
926    fn completions_fish() {
927        let cli = parse(&["ubt", "completions", "fish"]);
928        if let Command::Completions(args) = &cli.command {
929            assert_eq!(args.shell, clap_complete::Shell::Fish);
930        } else {
931            panic!("expected Completions command");
932        }
933    }
934
935    #[test]
936    fn completions_powershell() {
937        let cli = parse(&["ubt", "completions", "powershell"]);
938        if let Command::Completions(args) = &cli.command {
939            assert_eq!(args.shell, clap_complete::Shell::PowerShell);
940        } else {
941            panic!("expected Completions command");
942        }
943    }
944
945    // ── run-file alias ─────────────────────────────────────────────────
946
947    #[test]
948    fn run_file_alias() {
949        let cli = parse(&["ubt", "run:file", "script.ts"]);
950        assert_eq!(parse_command_name(&cli.command), "run-file");
951    }
952}