1mod analytics;
2mod cmds;
3mod core;
4mod discover;
5mod hooks;
6mod learn;
7mod parser;
8
9use cmds::cloud::{aws_cmd, container, curl_cmd, psql_cmd, wget_cmd};
11use cmds::dotnet::{binlog, dotnet_cmd, dotnet_format_report, dotnet_trx};
12use cmds::git::{diff_cmd, gh_cmd, git, glab_cmd, gt_cmd};
13use cmds::go::{go_cmd, golangci_cmd};
14use cmds::js::{
15 lint_cmd, next_cmd, npm_cmd, playwright_cmd, pnpm_cmd, prettier_cmd, prisma_cmd, tsc_cmd,
16 vitest_cmd,
17};
18use cmds::jvm::{gradlew_cmd, mvn_cmd};
19use cmds::python::{mypy_cmd, pip_cmd, pytest_cmd, ruff_cmd};
20use cmds::ruby::{rake_cmd, rspec_cmd, rubocop_cmd};
21use cmds::rust::{cargo_cmd, runner};
22use cmds::system::{
23 deps, env_cmd, find_cmd, format_cmd, grep_cmd, json_cmd, local_llm, log_cmd, ls, pipe_cmd,
24 read, summary, tree, wc_cmd,
25};
26
27use anyhow::{Context, Result};
28use clap::error::ErrorKind;
29use clap::{Parser, Subcommand, ValueEnum};
30use std::ffi::OsString;
31use std::path::{Path, PathBuf};
32use std::process::ExitCode;
33
34pub use discover::registry::rewrite_command_with_proxy;
35
36pub mod tracking {
37 pub use crate::core::tracking::*;
38}
39
40pub mod utils {
41 pub use crate::core::utils::*;
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
46pub enum AgentTarget {
47 Claude,
49 Cursor,
51 Windsurf,
53 Cline,
55 Kilocode,
57 Antigravity,
59 Pi,
61 Hermes,
63}
64
65#[derive(Parser)]
66#[command(
67 name = "rtk",
68 version,
69 about = "Rust Token Killer - Minimize LLM token consumption",
70 long_about = "A high-performance CLI proxy designed to filter and summarize system outputs before they reach your LLM context."
71)]
72struct Cli {
73 #[command(subcommand)]
74 command: Commands,
75
76 #[arg(short, long, action = clap::ArgAction::Count, global = true)]
78 verbose: u8,
79
80 #[arg(long, global = true)]
82 ultra_compact: bool,
83
84 #[arg(long = "skip-env", global = true)]
86 skip_env: bool,
87}
88
89#[derive(Debug, Subcommand)]
90enum Commands {
91 Ls {
93 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
95 args: Vec<String>,
96 },
97
98 Tree {
100 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
102 args: Vec<String>,
103 },
104
105 Read {
107 #[arg(required = true, num_args = 1..)]
109 files: Vec<PathBuf>,
110 #[arg(short, long, default_value = "none")]
112 level: core::filter::FilterLevel,
113 #[arg(short, long, conflicts_with = "tail_lines")]
115 max_lines: Option<usize>,
116 #[arg(long, conflicts_with = "max_lines")]
118 tail_lines: Option<usize>,
119 #[arg(short = 'n', long)]
121 line_numbers: bool,
122 },
123
124 Smart {
126 file: PathBuf,
128 #[arg(short, long, default_value = "heuristic")]
130 model: String,
131 #[arg(long)]
133 force_download: bool,
134 },
135
136 Git {
138 #[arg(short = 'C', action = clap::ArgAction::Append)]
140 directory: Vec<String>,
141
142 #[arg(short = 'c', action = clap::ArgAction::Append)]
144 config_override: Vec<String>,
145
146 #[arg(long = "git-dir")]
148 git_dir: Option<String>,
149
150 #[arg(long = "work-tree")]
152 work_tree: Option<String>,
153
154 #[arg(long = "no-pager")]
156 no_pager: bool,
157
158 #[arg(long = "no-optional-locks")]
160 no_optional_locks: bool,
161
162 #[arg(long)]
164 bare: bool,
165
166 #[arg(long = "literal-pathspecs")]
168 literal_pathspecs: bool,
169
170 #[command(subcommand)]
171 command: GitCommands,
172 },
173
174 Gh {
176 subcommand: String,
178 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
180 args: Vec<String>,
181 },
182
183 Glab {
185 #[arg(short = 'R', long = "repo")]
187 repo: Option<String>,
188 #[arg(short = 'g', long = "group")]
190 group: Option<String>,
191 subcommand: String,
193 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
195 args: Vec<String>,
196 },
197
198 Aws {
200 subcommand: String,
202 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
204 args: Vec<String>,
205 },
206
207 #[command(disable_help_flag = true)]
209 Psql {
210 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
212 args: Vec<String>,
213 },
214
215 Pnpm {
217 #[arg(long, short = 'F')]
219 filter: Vec<String>,
220
221 #[command(subcommand)]
222 command: PnpmCommands,
223 },
224
225 Err {
227 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
229 command: Vec<String>,
230 },
231
232 Test {
234 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
236 command: Vec<String>,
237 },
238
239 Json {
241 file: PathBuf,
243 #[arg(short, long, default_value = "5")]
245 depth: usize,
246 #[arg(long)]
248 keys_only: bool,
249 },
250
251 Deps {
253 #[arg(default_value = ".")]
255 path: PathBuf,
256 },
257
258 Env {
260 #[arg(short, long)]
262 filter: Option<String>,
263 #[arg(long)]
265 show_all: bool,
266 },
267
268 Find {
270 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
272 args: Vec<String>,
273 },
274
275 Diff {
277 file1: PathBuf,
279 file2: Option<PathBuf>,
281 },
282
283 Log {
285 file: Option<PathBuf>,
287 },
288
289 Dotnet {
291 #[command(subcommand)]
292 command: DotnetCommands,
293 },
294
295 Docker {
297 #[command(subcommand)]
298 command: DockerCommands,
299 },
300
301 Kubectl {
303 #[command(subcommand)]
304 command: KubectlCommands,
305 },
306
307 Summary {
309 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
311 command: Vec<String>,
312 },
313
314 Grep {
316 pattern: String,
318 #[arg(default_value = ".")]
320 path: String,
321 #[arg(short = 'l', long, default_value = "80")]
323 max_len: usize,
324 #[arg(short, long, default_value = "200")]
326 max: usize,
327 #[arg(long)]
329 context_only: bool,
330 #[arg(short = 't', long)]
332 file_type: Option<String>,
333 #[arg(short = 'n', long)]
335 line_numbers: bool,
336 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
338 extra_args: Vec<String>,
339 },
340
341 Init {
343 #[arg(short, long)]
345 global: bool,
346
347 #[arg(long)]
349 opencode: bool,
350
351 #[arg(long)]
353 gemini: bool,
354
355 #[arg(long, value_enum)]
357 agent: Option<AgentTarget>,
358
359 #[arg(long)]
361 show: bool,
362
363 #[arg(long = "claude-md", group = "mode")]
365 claude_md: bool,
366
367 #[arg(long = "hook-only", group = "mode")]
369 hook_only: bool,
370
371 #[arg(long = "auto-patch", group = "patch")]
373 auto_patch: bool,
374
375 #[arg(long = "no-patch", group = "patch")]
377 no_patch: bool,
378
379 #[arg(long)]
381 uninstall: bool,
382
383 #[arg(long)]
385 codex: bool,
386
387 #[arg(long)]
389 copilot: bool,
390 #[arg(long = "dry-run", conflicts_with = "show")]
392 dry_run: bool,
393 },
394
395 Wget {
397 url: String,
399 #[arg(short = 'O', long = "output-document", allow_hyphen_values = true)]
401 output: Option<String>,
402 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
404 args: Vec<String>,
405 },
406
407 Wc {
409 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
411 args: Vec<String>,
412 },
413
414 Gain {
416 #[arg(short, long)]
418 project: bool,
419 #[arg(short, long)]
421 graph: bool,
422 #[arg(short = 'H', long)]
424 history: bool,
425 #[arg(short, long)]
427 quota: bool,
428 #[arg(short, long, default_value = "20x", requires = "quota")]
430 tier: String,
431 #[arg(short, long)]
433 daily: bool,
434 #[arg(short, long)]
436 weekly: bool,
437 #[arg(short, long)]
439 monthly: bool,
440 #[arg(short, long)]
442 all: bool,
443 #[arg(short, long, default_value = "text")]
445 format: String,
446 #[arg(short = 'F', long)]
448 failures: bool,
449 #[arg(long)]
451 reset: bool,
452 #[arg(long, requires = "reset")]
454 yes: bool,
455 },
456
457 CcEconomics {
459 #[arg(short, long)]
461 daily: bool,
462 #[arg(short, long)]
464 weekly: bool,
465 #[arg(short, long)]
467 monthly: bool,
468 #[arg(short, long)]
470 all: bool,
471 #[arg(short, long, default_value = "text")]
473 format: String,
474 },
475
476 Config {
478 #[arg(long)]
480 create: bool,
481 },
482
483 Jest {
485 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
487 args: Vec<String>,
488 },
489
490 Vitest {
492 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
494 args: Vec<String>,
495 },
496
497 Prisma {
499 #[command(subcommand)]
500 command: PrismaCommands,
501 },
502
503 Tsc {
505 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
507 args: Vec<String>,
508 },
509
510 Next {
512 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
514 args: Vec<String>,
515 },
516
517 Lint {
519 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
521 args: Vec<String>,
522 },
523
524 Prettier {
526 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
528 args: Vec<String>,
529 },
530
531 Format {
533 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
535 args: Vec<String>,
536 },
537
538 Playwright {
540 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
542 args: Vec<String>,
543 },
544
545 Cargo {
547 #[command(subcommand)]
548 command: CargoCommands,
549 },
550
551 Npm {
553 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
555 args: Vec<String>,
556 },
557
558 Npx {
560 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
562 args: Vec<String>,
563 },
564
565 Curl {
567 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
569 args: Vec<String>,
570 },
571
572 Discover {
574 #[arg(short, long)]
576 project: Option<String>,
577 #[arg(short, long, default_value = "15")]
579 limit: usize,
580 #[arg(short, long)]
582 all: bool,
583 #[arg(short, long, default_value = "30")]
585 since: u64,
586 #[arg(short, long, default_value = "text")]
588 format: String,
589 },
590
591 Session {},
593
594 Telemetry {
596 #[command(subcommand)]
597 command: core::telemetry_cmd::TelemetrySubcommand,
598 },
599
600 Learn {
602 #[arg(short, long)]
604 project: Option<String>,
605 #[arg(short, long)]
607 all: bool,
608 #[arg(short, long, default_value = "30")]
610 since: u64,
611 #[arg(short, long, default_value = "text")]
613 format: String,
614 #[arg(short, long)]
616 write_rules: bool,
617 #[arg(long, default_value = "0.6")]
619 min_confidence: f64,
620 #[arg(long, default_value = "1")]
622 min_occurrences: usize,
623 },
624
625 Run {
627 #[arg(short = 'c', long = "command")]
629 command: Option<String>,
630 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
632 args: Vec<String>,
633 },
634
635 Proxy {
637 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
639 args: Vec<OsString>,
640 },
641
642 Pipe {
644 #[arg(short, long)]
646 filter: Option<String>,
647
648 #[arg(long)]
650 passthrough: bool,
651 },
652
653 Trust {
655 #[arg(long)]
657 list: bool,
658 },
659
660 Untrust,
662
663 Verify {
665 #[arg(long)]
667 filter: Option<String>,
668 #[arg(long)]
670 require_all: bool,
671 },
672
673 Ruff {
675 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
677 args: Vec<String>,
678 },
679
680 Pytest {
682 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
684 args: Vec<String>,
685 },
686
687 Mypy {
689 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
691 args: Vec<String>,
692 },
693
694 Rake {
696 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
698 args: Vec<String>,
699 },
700
701 Rubocop {
703 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
705 args: Vec<String>,
706 },
707
708 Rspec {
710 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
712 args: Vec<String>,
713 },
714
715 Pip {
717 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
719 args: Vec<String>,
720 },
721
722 Go {
724 #[command(subcommand)]
725 command: GoCommands,
726 },
727
728 Gt {
730 #[command(subcommand)]
731 command: GtCommands,
732 },
733
734 #[command(name = "golangci-lint")]
736 GolangciLint {
737 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
739 args: Vec<String>,
740 },
741
742 #[command(name = "gradlew")]
744 Gradlew {
745 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
747 args: Vec<String>,
748 },
749
750 #[command(name = "mvn")]
752 Mvn {
753 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
755 args: Vec<String>,
756 },
757
758 #[command(name = "hook-audit")]
760 HookAudit {
761 #[arg(short, long, default_value = "7")]
763 since: u64,
764 },
765
766 Rewrite {
774 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
777 args: Vec<String>,
778 },
779
780 Hook {
782 #[command(subcommand)]
783 command: HookCommands,
784 },
785}
786
787#[derive(Debug, Subcommand)]
788enum HookCommands {
789 Claude,
791 Cursor,
793 Gemini,
795 Copilot,
797 Check {
799 #[arg(long, default_value = "claude")]
801 agent: String,
802 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
804 command: Vec<String>,
805 },
806}
807
808#[derive(Debug, Subcommand)]
809enum GitCommands {
810 Diff {
812 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
814 args: Vec<String>,
815 },
816 Log {
818 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
820 args: Vec<String>,
821 },
822 Status {
824 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
826 args: Vec<String>,
827 },
828 Show {
830 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
832 args: Vec<String>,
833 },
834 Add {
836 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
838 args: Vec<String>,
839 },
840 Commit {
842 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
844 args: Vec<String>,
845 },
846 Push {
848 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
850 args: Vec<String>,
851 },
852 Pull {
854 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
856 args: Vec<String>,
857 },
858 Branch {
860 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
862 args: Vec<String>,
863 },
864 Fetch {
866 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
868 args: Vec<String>,
869 },
870 Stash {
872 subcommand: Option<String>,
874 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
876 args: Vec<String>,
877 },
878 Worktree {
880 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
882 args: Vec<String>,
883 },
884 #[command(external_subcommand)]
886 Other(Vec<OsString>),
887}
888
889#[derive(Debug, Subcommand)]
890enum PnpmCommands {
891 List {
893 #[arg(short, long, default_value = "0")]
895 depth: usize,
896 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
898 args: Vec<String>,
899 },
900 Outdated {
902 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
904 args: Vec<String>,
905 },
906 Install {
908 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
910 args: Vec<String>,
911 },
912 Typecheck {
914 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
916 args: Vec<String>,
917 },
918 #[command(external_subcommand)]
920 Other(Vec<OsString>),
921}
922
923#[derive(Debug, Subcommand)]
924enum DockerCommands {
925 Ps {
927 #[arg(short = 'a', long)]
928 all: bool,
929 },
930 Images,
932 Logs { container: String },
934 Compose {
936 #[command(subcommand)]
937 command: ComposeCommands,
938 },
939 #[command(external_subcommand)]
941 Other(Vec<OsString>),
942}
943
944#[derive(Debug, Subcommand)]
945enum ComposeCommands {
946 Ps {
948 #[arg(short = 'a', long)]
949 all: bool,
950 },
951 Logs {
953 service: Option<String>,
955 #[arg(long, default_value_t = 100)]
957 tail: u32,
958 },
959 Build {
961 service: Option<String>,
963 },
964 #[command(external_subcommand)]
966 Other(Vec<OsString>),
967}
968
969#[derive(Debug, Subcommand)]
970enum KubectlCommands {
971 Get {
973 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
975 args: Vec<String>,
976 },
977 Pods {
979 #[arg(short, long)]
980 namespace: Option<String>,
981 #[arg(short = 'A', long)]
983 all: bool,
984 },
985 Services {
987 #[arg(short, long)]
988 namespace: Option<String>,
989 #[arg(short = 'A', long)]
991 all: bool,
992 },
993 Logs {
995 pod: String,
996 #[arg(short, long)]
997 container: Option<String>,
998 },
999 #[command(external_subcommand)]
1001 Other(Vec<OsString>),
1002}
1003
1004#[derive(Debug, Subcommand)]
1005enum PrismaCommands {
1006 Generate {
1008 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1010 args: Vec<String>,
1011 },
1012 Migrate {
1014 #[command(subcommand)]
1015 command: PrismaMigrateCommands,
1016 },
1017 DbPush {
1019 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1021 args: Vec<String>,
1022 },
1023}
1024
1025#[derive(Debug, Subcommand)]
1026enum PrismaMigrateCommands {
1027 Dev {
1029 #[arg(short, long)]
1031 name: Option<String>,
1032 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1034 args: Vec<String>,
1035 },
1036 Status {
1038 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1040 args: Vec<String>,
1041 },
1042 Deploy {
1044 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1046 args: Vec<String>,
1047 },
1048}
1049
1050#[derive(Debug, Subcommand)]
1051enum CargoCommands {
1052 Build {
1054 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1056 args: Vec<String>,
1057 },
1058 Test {
1060 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1062 args: Vec<String>,
1063 },
1064 Clippy {
1066 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1068 args: Vec<String>,
1069 },
1070 Check {
1072 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1074 args: Vec<String>,
1075 },
1076 Install {
1078 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1080 args: Vec<String>,
1081 },
1082 Nextest {
1084 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1086 args: Vec<String>,
1087 },
1088 #[command(external_subcommand)]
1090 Other(Vec<OsString>),
1091}
1092
1093#[derive(Debug, Subcommand)]
1094enum DotnetCommands {
1095 Build {
1097 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1098 args: Vec<String>,
1099 },
1100 Test {
1102 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1103 args: Vec<String>,
1104 },
1105 Restore {
1107 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1108 args: Vec<String>,
1109 },
1110 Format {
1112 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1113 args: Vec<String>,
1114 },
1115 #[command(external_subcommand)]
1117 Other(Vec<OsString>),
1118}
1119
1120#[derive(Debug, Subcommand)]
1121enum GoCommands {
1122 Test {
1124 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1126 args: Vec<String>,
1127 },
1128 Build {
1130 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1132 args: Vec<String>,
1133 },
1134 Vet {
1136 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1138 args: Vec<String>,
1139 },
1140 #[command(external_subcommand)]
1142 Other(Vec<OsString>),
1143}
1144
1145const RTK_META_COMMANDS: &[&str] = &[
1148 "gain",
1149 "discover",
1150 "learn",
1151 "init",
1152 "config",
1153 "proxy",
1154 "run",
1155 "hook",
1156 "hook-audit",
1157 "pipe",
1158 "cc-economics",
1159 "verify",
1160 "trust",
1161 "untrust",
1162 "session",
1163 "rewrite",
1164];
1165
1166fn run_fallback(parse_error: clap::Error, argv: &[OsString]) -> Result<i32> {
1167 let args: Vec<String> = argv
1171 .iter()
1172 .skip(1)
1173 .map(|s| s.to_string_lossy().into_owned())
1174 .collect();
1175
1176 if args.is_empty() {
1178 parse_error.exit();
1179 }
1180
1181 if RTK_META_COMMANDS.contains(&args[0].as_str()) {
1184 parse_error.exit();
1185 }
1186
1187 let raw_command = args.join(" ");
1188 let error_message = core::utils::strip_ansi(&parse_error.to_string());
1189
1190 let timer = core::tracking::TimedExecution::start();
1192
1193 let lookup_cmd = {
1196 let base = std::path::Path::new(&args[0])
1197 .file_name()
1198 .map(|n| n.to_string_lossy().into_owned())
1199 .unwrap_or_else(|| args[0].clone());
1200 std::iter::once(base.as_str())
1201 .chain(args[1..].iter().map(|s| s.as_str()))
1202 .collect::<Vec<_>>()
1203 .join(" ")
1204 };
1205 let toml_match = if std::env::var("RTK_NO_TOML").ok().as_deref() == Some("1") {
1206 None
1207 } else {
1208 core::toml_filter::find_matching_filter(&lookup_cmd)
1209 };
1210
1211 if let Some(filter) = toml_match {
1212 let result = if filter.filter_stderr {
1214 core::utils::resolved_command(&args[0])
1216 .args(&args[1..])
1217 .stdin(std::process::Stdio::inherit())
1218 .stdout(std::process::Stdio::piped())
1219 .stderr(std::process::Stdio::piped()) .output()
1221 } else {
1222 core::utils::resolved_command(&args[0])
1223 .args(&args[1..])
1224 .stdin(std::process::Stdio::inherit())
1225 .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::inherit()) .output()
1228 };
1229
1230 match result {
1231 Ok(output) => {
1232 let exit_code = core::utils::exit_code_from_output(&output, &raw_command);
1233 let stdout_raw = String::from_utf8_lossy(&output.stdout);
1234 let stderr_raw = String::from_utf8_lossy(&output.stderr);
1235
1236 let combined_raw = if filter.filter_stderr {
1239 format!("{}{}", stdout_raw, stderr_raw)
1240 } else {
1241 stdout_raw.to_string()
1242 };
1243 let tee_hint = if !output.status.success() {
1245 core::tee::tee_and_hint(&combined_raw, &raw_command, exit_code)
1246 } else {
1247 None
1248 };
1249
1250 let filtered = core::toml_filter::apply_filter(filter, &combined_raw);
1251 println!("{}", filtered);
1252 if let Some(hint) = tee_hint {
1253 println!("{}", hint);
1254 }
1255
1256 timer.track(
1257 &raw_command,
1258 &format!("rtk:toml {}", raw_command),
1259 &combined_raw,
1260 &filtered,
1261 );
1262 core::tracking::record_parse_failure_silent(&raw_command, &error_message, true);
1263
1264 Ok(exit_code)
1265 }
1266 Err(e) => {
1267 core::tracking::record_parse_failure_silent(&raw_command, &error_message, false);
1269 eprintln!("[rtk: {}]", e);
1270 Ok(127)
1271 }
1272 }
1273 } else {
1274 let status = core::utils::resolved_command(&args[0])
1276 .args(&args[1..])
1277 .stdin(std::process::Stdio::inherit())
1278 .stdout(std::process::Stdio::inherit())
1279 .stderr(std::process::Stdio::inherit())
1280 .status();
1281
1282 match status {
1283 Ok(s) => {
1284 timer.track_passthrough(&raw_command, &format!("rtk fallback: {}", raw_command));
1285
1286 core::tracking::record_parse_failure_silent(&raw_command, &error_message, true);
1287
1288 Ok(core::utils::exit_code_from_status(&s, &raw_command))
1289 }
1290 Err(e) => {
1291 core::tracking::record_parse_failure_silent(&raw_command, &error_message, false);
1292 eprintln!("[rtk: {}]", e);
1294 Ok(127)
1295 }
1296 }
1297 }
1298}
1299
1300#[derive(Debug, Subcommand)]
1301enum GtCommands {
1302 Log {
1304 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1305 args: Vec<String>,
1306 },
1307 Submit {
1309 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1310 args: Vec<String>,
1311 },
1312 Sync {
1314 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1315 args: Vec<String>,
1316 },
1317 Restack {
1319 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1320 args: Vec<String>,
1321 },
1322 Create {
1324 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1325 args: Vec<String>,
1326 },
1327 Branch {
1329 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1330 args: Vec<String>,
1331 },
1332 #[command(external_subcommand)]
1334 Other(Vec<OsString>),
1335}
1336
1337fn shell_split(input: &str) -> Vec<String> {
1340 discover::lexer::shell_split(input)
1341}
1342
1343fn merge_pnpm_args(filters: &[String], args: &[String]) -> Vec<String> {
1345 filters
1346 .iter()
1347 .map(|filter| format!("--filter={}", filter))
1348 .chain(args.iter().cloned())
1349 .collect()
1350}
1351
1352fn merge_pnpm_args_os(filters: &[String], args: &[OsString]) -> Vec<OsString> {
1354 filters
1355 .iter()
1356 .map(|filter| OsString::from(format!("--filter={}", filter)))
1357 .chain(args.iter().cloned())
1358 .collect()
1359}
1360
1361fn validate_pnpm_filters(filters: &[String], command: &PnpmCommands) -> Option<String> {
1363 match command {
1365 PnpmCommands::Typecheck { .. } => {
1366 if !filters.is_empty() {
1368 let cmd_name = match command {
1369 PnpmCommands::Typecheck { .. } => "tsc",
1370 _ => unreachable!(),
1371 };
1372 let msg = format!(
1373 "[rtk] warning: --filter is not yet supported for pnpm {}, filters preceding the subcommand will be ignored",
1374 cmd_name
1375 );
1376 return Some(msg);
1377 }
1378 None
1379 }
1380 _ => None,
1381 }
1382}
1383
1384fn reset_sigpipe() {
1385 #[cfg(unix)]
1390 #[allow(unsafe_code)]
1391 unsafe {
1393 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
1394 }
1395}
1396
1397pub fn main_entry() -> Result<()> {
1398 std::process::exit(cli_entry_code(std::env::args_os()));
1399}
1400
1401pub fn cli_entry(args: impl IntoIterator<Item = OsString>) -> ExitCode {
1416 i32_to_exit_code(cli_entry_code(args))
1417}
1418
1419pub fn cli_entry_code(args: impl IntoIterator<Item = OsString>) -> i32 {
1423 reset_sigpipe();
1424 match run_cli_from(args) {
1425 Ok(code) => code,
1426 Err(e) => {
1427 eprintln!("rtk: {:#}", e);
1428 1
1429 }
1430 }
1431}
1432
1433fn i32_to_exit_code(code: i32) -> ExitCode {
1434 if code == 0 {
1435 ExitCode::SUCCESS
1436 } else if (1..=255).contains(&code) {
1437 ExitCode::from(code as u8)
1438 } else {
1439 ExitCode::from(1)
1440 }
1441}
1442
1443fn uninstall_init_dispatch<UninstallHermes, UninstallStandard>(
1444 agent: Option<AgentTarget>,
1445 global: bool,
1446 gemini: bool,
1447 codex: bool,
1448 ctx: hooks::init::InitContext,
1449 uninstall_hermes: UninstallHermes,
1450 uninstall_standard: UninstallStandard,
1451) -> Result<()>
1452where
1453 UninstallHermes: FnOnce(hooks::init::InitContext) -> Result<()>,
1454 UninstallStandard: FnOnce(bool, bool, bool, bool, bool, hooks::init::InitContext) -> Result<()>,
1455{
1456 if agent == Some(AgentTarget::Hermes) {
1457 uninstall_hermes(ctx)
1458 } else {
1459 let cursor = agent == Some(AgentTarget::Cursor);
1460 let pi = agent == Some(AgentTarget::Pi);
1461 uninstall_standard(global, gemini, codex, cursor, pi, ctx)
1462 }
1463}
1464
1465fn run_cli_from(args: impl IntoIterator<Item = OsString>) -> Result<i32> {
1466 let argv: Vec<OsString> = args.into_iter().collect();
1467 let hosted = hosted_mode();
1468
1469 if !hosted {
1471 core::telemetry::maybe_ping();
1472 }
1473
1474 let cli = match Cli::try_parse_from(&argv) {
1475 Ok(cli) => cli,
1476 Err(e) => {
1477 if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
1478 e.exit();
1479 }
1480 return run_fallback(e, &argv);
1481 }
1482 };
1483
1484 if !hosted && !matches!(cli.command, Commands::Gain { .. }) {
1487 hooks::hook_check::maybe_warn();
1488 }
1489
1490 if !hosted && is_operational_command(&cli.command) {
1494 hooks::integrity::runtime_check()?;
1495 }
1496
1497 let code = match cli.command {
1498 Commands::Ls { args } => ls::run(&args, cli.verbose)?,
1499
1500 Commands::Tree { args } => tree::run(&args, cli.verbose)?,
1501
1502 Commands::Read {
1504 files,
1505 level,
1506 max_lines,
1507 tail_lines,
1508 line_numbers,
1509 } => {
1510 let mut had_error = false;
1511 let mut stdin_seen = false;
1512 for file in &files {
1513 let result = if file == Path::new("-") {
1514 if stdin_seen {
1515 eprintln!("rtk: warning: stdin specified more than once");
1516 continue;
1517 }
1518 stdin_seen = true;
1519 read::run_stdin(level, max_lines, tail_lines, line_numbers, cli.verbose)
1520 } else {
1521 read::run(
1522 file,
1523 level,
1524 max_lines,
1525 tail_lines,
1526 line_numbers,
1527 cli.verbose,
1528 )
1529 };
1530 if let Err(e) = result {
1531 eprintln!("cat: {}: {}", file.display(), e.root_cause());
1532 had_error = true;
1533 }
1534 }
1535 if had_error {
1536 1
1537 } else {
1538 0
1539 }
1540 }
1541
1542 Commands::Smart {
1543 file,
1544 model,
1545 force_download,
1546 } => {
1547 local_llm::run(&file, &model, force_download, cli.verbose)?;
1548 0
1549 }
1550
1551 Commands::Git {
1552 directory,
1553 config_override,
1554 git_dir,
1555 work_tree,
1556 no_pager,
1557 no_optional_locks,
1558 bare,
1559 literal_pathspecs,
1560 command,
1561 } => {
1562 let mut global_args: Vec<String> = Vec::new();
1564 for dir in &directory {
1565 global_args.push("-C".to_string());
1566 global_args.push(dir.clone());
1567 }
1568 for cfg in &config_override {
1569 global_args.push("-c".to_string());
1570 global_args.push(cfg.clone());
1571 }
1572 if let Some(ref dir) = git_dir {
1573 global_args.push("--git-dir".to_string());
1574 global_args.push(dir.clone());
1575 }
1576 if let Some(ref tree) = work_tree {
1577 global_args.push("--work-tree".to_string());
1578 global_args.push(tree.clone());
1579 }
1580 if no_pager {
1581 global_args.push("--no-pager".to_string());
1582 }
1583 if no_optional_locks {
1584 global_args.push("--no-optional-locks".to_string());
1585 }
1586 if bare {
1587 global_args.push("--bare".to_string());
1588 }
1589 if literal_pathspecs {
1590 global_args.push("--literal-pathspecs".to_string());
1591 }
1592
1593 match command {
1594 GitCommands::Diff { args } => git::run(
1595 git::GitCommand::Diff,
1596 &args,
1597 None,
1598 cli.verbose,
1599 &global_args,
1600 )?,
1601 GitCommands::Log { args } => {
1602 git::run(git::GitCommand::Log, &args, None, cli.verbose, &global_args)?
1603 }
1604 GitCommands::Status { args } => git::run(
1605 git::GitCommand::Status,
1606 &args,
1607 None,
1608 cli.verbose,
1609 &global_args,
1610 )?,
1611 GitCommands::Show { args } => git::run(
1612 git::GitCommand::Show,
1613 &args,
1614 None,
1615 cli.verbose,
1616 &global_args,
1617 )?,
1618 GitCommands::Add { args } => {
1619 git::run(git::GitCommand::Add, &args, None, cli.verbose, &global_args)?
1620 }
1621 GitCommands::Commit { args } => git::run(
1622 git::GitCommand::Commit,
1623 &args,
1624 None,
1625 cli.verbose,
1626 &global_args,
1627 )?,
1628 GitCommands::Push { args } => git::run(
1629 git::GitCommand::Push,
1630 &args,
1631 None,
1632 cli.verbose,
1633 &global_args,
1634 )?,
1635 GitCommands::Pull { args } => git::run(
1636 git::GitCommand::Pull,
1637 &args,
1638 None,
1639 cli.verbose,
1640 &global_args,
1641 )?,
1642 GitCommands::Branch { args } => git::run(
1643 git::GitCommand::Branch,
1644 &args,
1645 None,
1646 cli.verbose,
1647 &global_args,
1648 )?,
1649 GitCommands::Fetch { args } => git::run(
1650 git::GitCommand::Fetch,
1651 &args,
1652 None,
1653 cli.verbose,
1654 &global_args,
1655 )?,
1656 GitCommands::Stash { subcommand, args } => git::run(
1657 git::GitCommand::Stash { subcommand },
1658 &args,
1659 None,
1660 cli.verbose,
1661 &global_args,
1662 )?,
1663 GitCommands::Worktree { args } => git::run(
1664 git::GitCommand::Worktree,
1665 &args,
1666 None,
1667 cli.verbose,
1668 &global_args,
1669 )?,
1670 GitCommands::Other(args) => git::run_passthrough(&args, &global_args, cli.verbose)?,
1671 }
1672 }
1673
1674 Commands::Gh { subcommand, args } => {
1675 gh_cmd::run(&subcommand, &args, cli.verbose, cli.ultra_compact)?
1676 }
1677
1678 Commands::Glab {
1679 repo,
1680 group,
1681 subcommand,
1682 mut args,
1683 } => {
1684 if let Some(r) = repo {
1687 args.push("-R".to_string());
1688 args.push(r);
1689 }
1690 if let Some(g) = group {
1691 args.push("-g".to_string());
1692 args.push(g);
1693 }
1694 glab_cmd::run(&subcommand, &args, cli.verbose, cli.ultra_compact)?
1695 }
1696
1697 Commands::Aws { subcommand, args } => aws_cmd::run(&subcommand, &args, cli.verbose)?,
1698
1699 Commands::Psql { args } => psql_cmd::run(&args, cli.verbose)?,
1700
1701 Commands::Pnpm { filter, command } => {
1702 if let Some(warning) = validate_pnpm_filters(&filter, &command) {
1704 eprintln!("{}", warning);
1705 }
1706
1707 match command {
1708 PnpmCommands::List { depth, args } => pnpm_cmd::run(
1709 pnpm_cmd::PnpmCommand::List { depth },
1710 &merge_pnpm_args(&filter, &args),
1711 cli.verbose,
1712 )?,
1713 PnpmCommands::Outdated { args } => pnpm_cmd::run(
1714 pnpm_cmd::PnpmCommand::Outdated,
1715 &merge_pnpm_args(&filter, &args),
1716 cli.verbose,
1717 )?,
1718 PnpmCommands::Install { args } => pnpm_cmd::run(
1719 pnpm_cmd::PnpmCommand::Install,
1720 &merge_pnpm_args(&filter, &args),
1721 cli.verbose,
1722 )?,
1723 PnpmCommands::Typecheck { args } => tsc_cmd::run(&args, cli.verbose)?,
1724 PnpmCommands::Other(args) => {
1725 pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &args), cli.verbose)?
1726 }
1727 }
1728 }
1729
1730 Commands::Err { command } => {
1731 let cmd = command.join(" ");
1732 runner::run_err(&cmd, cli.verbose)?
1733 }
1734
1735 Commands::Test { command } => {
1736 let cmd = command.join(" ");
1737 runner::run_test(&cmd, cli.verbose)?
1738 }
1739
1740 Commands::Json {
1741 file,
1742 depth,
1743 keys_only,
1744 } => {
1745 if file == Path::new("-") {
1746 json_cmd::run_stdin(depth, keys_only, cli.verbose)?;
1747 } else {
1748 json_cmd::run(&file, depth, keys_only, cli.verbose)?;
1749 }
1750 0
1751 }
1752
1753 Commands::Deps { path } => {
1754 deps::run(&path, cli.verbose)?;
1755 0
1756 }
1757
1758 Commands::Env { filter, show_all } => {
1759 env_cmd::run(filter.as_deref(), show_all, cli.verbose)?;
1760 0
1761 }
1762
1763 Commands::Find { args } => {
1764 find_cmd::run_from_args(&args, cli.verbose)?;
1765 0
1766 }
1767
1768 Commands::Diff { file1, file2 } => {
1769 if let Some(f2) = file2 {
1770 diff_cmd::run(&file1, &f2, cli.verbose)?;
1771 } else {
1772 diff_cmd::run_stdin(cli.verbose)?;
1773 }
1774 0
1775 }
1776
1777 Commands::Log { file } => {
1778 if let Some(f) = file {
1779 log_cmd::run_file(&f, cli.verbose)?;
1780 } else {
1781 log_cmd::run_stdin(cli.verbose)?;
1782 }
1783 0
1784 }
1785
1786 Commands::Dotnet { command } => match command {
1787 DotnetCommands::Build { args } => dotnet_cmd::run_build(&args, cli.verbose)?,
1788 DotnetCommands::Test { args } => dotnet_cmd::run_test(&args, cli.verbose)?,
1789 DotnetCommands::Restore { args } => dotnet_cmd::run_restore(&args, cli.verbose)?,
1790 DotnetCommands::Format { args } => dotnet_cmd::run_format(&args, cli.verbose)?,
1791 DotnetCommands::Other(args) => dotnet_cmd::run_passthrough(&args, cli.verbose)?,
1792 },
1793
1794 Commands::Docker { command } => match command {
1795 DockerCommands::Ps { all } => {
1796 let cmd = if all {
1797 container::ContainerCmd::DockerPsAll
1798 } else {
1799 container::ContainerCmd::DockerPs
1800 };
1801 container::run(cmd, &[], cli.verbose)?
1802 }
1803 DockerCommands::Images => {
1804 container::run(container::ContainerCmd::DockerImages, &[], cli.verbose)?
1805 }
1806 DockerCommands::Logs { container: c } => {
1807 container::run(container::ContainerCmd::DockerLogs, &[c], cli.verbose)?
1808 }
1809 DockerCommands::Compose { command: compose } => match compose {
1810 ComposeCommands::Ps { all } => container::run_compose_ps(all, cli.verbose)?,
1811 ComposeCommands::Logs { service, tail } => {
1812 container::run_compose_logs(service.as_deref(), tail, cli.verbose)?
1813 }
1814 ComposeCommands::Build { service } => {
1815 container::run_compose_build(service.as_deref(), cli.verbose)?
1816 }
1817 ComposeCommands::Other(args) => {
1818 container::run_compose_passthrough(&args, cli.verbose)?
1819 }
1820 },
1821 DockerCommands::Other(args) => container::run_docker_passthrough(&args, cli.verbose)?,
1822 },
1823
1824 Commands::Kubectl { command } => match command {
1825 KubectlCommands::Get { args } => container::run_kubectl_get(&args, cli.verbose)?,
1826 KubectlCommands::Pods { namespace, all } => {
1827 let mut args: Vec<String> = Vec::new();
1828 if all {
1829 args.push("-A".to_string());
1830 } else if let Some(n) = namespace {
1831 args.push("-n".to_string());
1832 args.push(n);
1833 }
1834 container::run(container::ContainerCmd::KubectlPods, &args, cli.verbose)?
1835 }
1836 KubectlCommands::Services { namespace, all } => {
1837 let mut args: Vec<String> = Vec::new();
1838 if all {
1839 args.push("-A".to_string());
1840 } else if let Some(n) = namespace {
1841 args.push("-n".to_string());
1842 args.push(n);
1843 }
1844 container::run(container::ContainerCmd::KubectlServices, &args, cli.verbose)?
1845 }
1846 KubectlCommands::Logs { pod, container: c } => {
1847 let mut args = vec![pod];
1848 if let Some(cont) = c {
1849 args.push("-c".to_string());
1850 args.push(cont);
1851 }
1852 container::run(container::ContainerCmd::KubectlLogs, &args, cli.verbose)?
1853 }
1854 KubectlCommands::Other(args) => container::run_kubectl_passthrough(&args, cli.verbose)?,
1855 },
1856
1857 Commands::Summary { command } => {
1858 let cmd = command.join(" ");
1859 summary::run(&cmd, cli.verbose)?
1860 }
1861
1862 Commands::Grep {
1863 pattern,
1864 path,
1865 max_len,
1866 max,
1867 context_only,
1868 file_type,
1869 line_numbers: _, extra_args,
1871 } => grep_cmd::run(
1872 &pattern,
1873 &path,
1874 max_len,
1875 max,
1876 context_only,
1877 file_type.as_deref(),
1878 &extra_args,
1879 cli.verbose,
1880 )?,
1881
1882 Commands::Init {
1883 global,
1884 opencode,
1885 gemini,
1886 agent,
1887 show,
1888 claude_md,
1889 hook_only,
1890 auto_patch,
1891 no_patch,
1892 uninstall,
1893 codex,
1894 copilot,
1895 dry_run,
1896 } => {
1897 let ctx = hooks::init::InitContext {
1898 verbose: cli.verbose,
1899 dry_run,
1900 };
1901 if show {
1902 hooks::init::show_config(codex)?;
1903 } else if uninstall && copilot {
1904 if global {
1905 hooks::init::uninstall_copilot_global(ctx)?;
1906 } else {
1907 hooks::init::uninstall_copilot(ctx)?;
1908 }
1909 } else if uninstall {
1910 uninstall_init_dispatch(
1911 agent,
1912 global,
1913 gemini,
1914 codex,
1915 ctx,
1916 hooks::init::uninstall_hermes,
1917 hooks::init::uninstall,
1918 )?;
1919 } else if gemini {
1920 let patch_mode = if auto_patch {
1921 hooks::init::PatchMode::Auto
1922 } else if no_patch {
1923 hooks::init::PatchMode::Skip
1924 } else {
1925 hooks::init::PatchMode::Ask
1926 };
1927 hooks::init::run_gemini(global, hook_only, patch_mode, ctx)?;
1928 } else if copilot {
1929 if global {
1930 hooks::init::run_copilot_global(ctx)?;
1931 } else {
1932 hooks::init::run_copilot(ctx)?;
1933 }
1934 } else if agent == Some(AgentTarget::Pi) {
1935 hooks::init::run_pi_mode(global, ctx)?
1936 } else if agent == Some(AgentTarget::Kilocode) {
1937 if global {
1938 anyhow::bail!("Kilo Code is project-scoped. Use: rtk init --agent kilocode");
1939 }
1940 hooks::init::run_kilocode_mode(ctx)?;
1941 } else if agent == Some(AgentTarget::Antigravity) {
1942 if global {
1943 anyhow::bail!(
1944 "Antigravity is project-scoped. Use: rtk init --agent antigravity"
1945 );
1946 }
1947 hooks::init::run_antigravity_mode(ctx)?;
1948 } else if agent == Some(AgentTarget::Hermes) {
1949 hooks::init::run_hermes_mode(ctx)?;
1950 } else {
1951 let install_opencode = opencode;
1952 let install_claude = !opencode;
1953 let install_cursor = agent == Some(AgentTarget::Cursor);
1954 let install_windsurf = agent == Some(AgentTarget::Windsurf);
1955 let install_cline = agent == Some(AgentTarget::Cline);
1956
1957 let patch_mode = if auto_patch {
1958 hooks::init::PatchMode::Auto
1959 } else if no_patch {
1960 hooks::init::PatchMode::Skip
1961 } else {
1962 hooks::init::PatchMode::Ask
1963 };
1964 hooks::init::run(
1965 global,
1966 install_claude,
1967 install_opencode,
1968 install_cursor,
1969 install_windsurf,
1970 install_cline,
1971 claude_md,
1972 hook_only,
1973 codex,
1974 patch_mode,
1975 ctx,
1976 )?;
1977 }
1978 0
1979 }
1980
1981 Commands::Wget { url, output, args } => {
1982 if output.as_deref() == Some("-") {
1983 wget_cmd::run_stdout(&url, &args, cli.verbose)?
1984 } else {
1985 let mut all_args = Vec::new();
1987 if let Some(out_file) = &output {
1988 all_args.push("-O".to_string());
1989 all_args.push(out_file.clone());
1990 }
1991 all_args.extend(args);
1992 wget_cmd::run(&url, &all_args, cli.verbose)?
1993 }
1994 }
1995
1996 Commands::Wc { args } => wc_cmd::run(&args, cli.verbose)?,
1997
1998 Commands::Gain {
1999 project, graph,
2001 history,
2002 quota,
2003 tier,
2004 daily,
2005 weekly,
2006 monthly,
2007 all,
2008 format,
2009 failures,
2010 reset,
2011 yes,
2012 } => {
2013 analytics::gain::run(
2014 project, graph,
2016 history,
2017 quota,
2018 &tier,
2019 daily,
2020 weekly,
2021 monthly,
2022 all,
2023 &format,
2024 failures,
2025 reset,
2026 yes,
2027 cli.verbose,
2028 )?;
2029 0
2030 }
2031
2032 Commands::CcEconomics {
2033 daily,
2034 weekly,
2035 monthly,
2036 all,
2037 format,
2038 } => {
2039 analytics::cc_economics::run(daily, weekly, monthly, all, &format, cli.verbose)?;
2040 0
2041 }
2042
2043 Commands::Config { create } => {
2044 if create {
2045 let path = core::config::Config::create_default()?;
2046 println!("Created: {}", path.display());
2047 } else {
2048 core::config::show_config()?;
2049 }
2050 0
2051 }
2052
2053 Commands::Jest { ref args } | Commands::Vitest { ref args } => {
2054 vitest_cmd::run_test(&cli.command, args, cli.verbose)?
2055 }
2056
2057 Commands::Prisma { command } => match command {
2058 PrismaCommands::Generate { args } => {
2059 prisma_cmd::run(prisma_cmd::PrismaCommand::Generate, &args, cli.verbose)?
2060 }
2061 PrismaCommands::Migrate { command } => match command {
2062 PrismaMigrateCommands::Dev { name, args } => prisma_cmd::run(
2063 prisma_cmd::PrismaCommand::Migrate {
2064 subcommand: prisma_cmd::MigrateSubcommand::Dev { name },
2065 },
2066 &args,
2067 cli.verbose,
2068 )?,
2069 PrismaMigrateCommands::Status { args } => prisma_cmd::run(
2070 prisma_cmd::PrismaCommand::Migrate {
2071 subcommand: prisma_cmd::MigrateSubcommand::Status,
2072 },
2073 &args,
2074 cli.verbose,
2075 )?,
2076 PrismaMigrateCommands::Deploy { args } => prisma_cmd::run(
2077 prisma_cmd::PrismaCommand::Migrate {
2078 subcommand: prisma_cmd::MigrateSubcommand::Deploy,
2079 },
2080 &args,
2081 cli.verbose,
2082 )?,
2083 },
2084 PrismaCommands::DbPush { args } => {
2085 prisma_cmd::run(prisma_cmd::PrismaCommand::DbPush, &args, cli.verbose)?
2086 }
2087 },
2088
2089 Commands::Tsc { args } => tsc_cmd::run(&args, cli.verbose)?,
2090
2091 Commands::Next { args } => next_cmd::run(&args, cli.verbose)?,
2092
2093 Commands::Lint { args } => lint_cmd::run(&args, cli.verbose)?,
2094
2095 Commands::Prettier { args } => prettier_cmd::run(&args, cli.verbose)?,
2096
2097 Commands::Format { args } => format_cmd::run(&args, cli.verbose)?,
2098
2099 Commands::Playwright { args } => playwright_cmd::run(&args, cli.verbose)?,
2100
2101 Commands::Cargo { command } => match command {
2102 CargoCommands::Build { args } => {
2103 cargo_cmd::run(cargo_cmd::CargoCommand::Build, &args, cli.verbose)?
2104 }
2105 CargoCommands::Test { args } => {
2106 cargo_cmd::run(cargo_cmd::CargoCommand::Test, &args, cli.verbose)?
2107 }
2108 CargoCommands::Clippy { args } => {
2109 cargo_cmd::run(cargo_cmd::CargoCommand::Clippy, &args, cli.verbose)?
2110 }
2111 CargoCommands::Check { args } => {
2112 cargo_cmd::run(cargo_cmd::CargoCommand::Check, &args, cli.verbose)?
2113 }
2114 CargoCommands::Install { args } => {
2115 cargo_cmd::run(cargo_cmd::CargoCommand::Install, &args, cli.verbose)?
2116 }
2117 CargoCommands::Nextest { args } => {
2118 cargo_cmd::run(cargo_cmd::CargoCommand::Nextest, &args, cli.verbose)?
2119 }
2120 CargoCommands::Other(args) => cargo_cmd::run_passthrough(&args, cli.verbose)?,
2121 },
2122
2123 Commands::Npm { args } => npm_cmd::run(&args, cli.verbose, cli.skip_env)?,
2124
2125 Commands::Curl { args } => curl_cmd::run(&args, cli.verbose)?,
2126
2127 Commands::Discover {
2128 project,
2129 limit,
2130 all,
2131 since,
2132 format,
2133 } => {
2134 discover::run(project.as_deref(), all, since, limit, &format, cli.verbose)?;
2135 0
2136 }
2137
2138 Commands::Session {} => {
2139 analytics::session_cmd::run(cli.verbose)?;
2140 0
2141 }
2142
2143 Commands::Telemetry { command } => {
2144 core::telemetry_cmd::run(&command)?;
2145 0
2146 }
2147
2148 Commands::Learn {
2149 project,
2150 all,
2151 since,
2152 format,
2153 write_rules,
2154 min_confidence,
2155 min_occurrences,
2156 } => {
2157 learn::run(
2158 project,
2159 all,
2160 since,
2161 format,
2162 write_rules,
2163 min_confidence,
2164 min_occurrences,
2165 )?;
2166 0
2167 }
2168
2169 Commands::Npx { args } => {
2170 if args.is_empty() {
2171 anyhow::bail!("npx requires a command argument");
2172 }
2173
2174 match args[0].as_str() {
2176 "tsc" | "typescript" => tsc_cmd::run(&args[1..], cli.verbose)?,
2177 "eslint" => lint_cmd::run(&args[1..], cli.verbose)?,
2178 "prisma" => {
2179 if args.len() > 1 {
2181 let prisma_args: Vec<String> = args[2..].to_vec();
2182 match args[1].as_str() {
2183 "generate" => prisma_cmd::run(
2184 prisma_cmd::PrismaCommand::Generate,
2185 &prisma_args,
2186 cli.verbose,
2187 )?,
2188 "db" if args.len() > 2 && args[2] == "push" => prisma_cmd::run(
2189 prisma_cmd::PrismaCommand::DbPush,
2190 &args[3..],
2191 cli.verbose,
2192 )?,
2193 _ => {
2194 let timer = core::tracking::TimedExecution::start();
2196 let mut cmd = core::utils::resolved_command("npx");
2197 for arg in &args {
2198 cmd.arg(arg);
2199 }
2200 let status = cmd.status().context("Failed to run npx prisma")?;
2201 let args_str = args.join(" ");
2202 timer.track_passthrough(
2203 &format!("npx {}", args_str),
2204 &format!("rtk npx {} (passthrough)", args_str),
2205 );
2206 core::utils::exit_code_from_status(&status, "npx prisma")
2207 }
2208 }
2209 } else {
2210 let timer = core::tracking::TimedExecution::start();
2211 let status = core::utils::resolved_command("npx")
2212 .arg("prisma")
2213 .status()
2214 .context("Failed to run npx prisma")?;
2215 timer.track_passthrough("npx prisma", "rtk npx prisma (passthrough)");
2216 core::utils::exit_code_from_status(&status, "npx prisma")
2217 }
2218 }
2219 "next" => next_cmd::run(&args[1..], cli.verbose)?,
2220 "prettier" => prettier_cmd::run(&args[1..], cli.verbose)?,
2221 "playwright" => playwright_cmd::run(&args[1..], cli.verbose)?,
2222 _ => npm_cmd::exec(&args, cli.verbose, cli.skip_env)?,
2223 }
2224 }
2225
2226 Commands::Ruff { args } => ruff_cmd::run(&args, cli.verbose)?,
2227
2228 Commands::Pytest { args } => pytest_cmd::run(&args, cli.verbose)?,
2229
2230 Commands::Mypy { args } => mypy_cmd::run(&args, cli.verbose)?,
2231
2232 Commands::Rake { args } => rake_cmd::run(&args, cli.verbose)?,
2233
2234 Commands::Rubocop { args } => rubocop_cmd::run(&args, cli.verbose)?,
2235
2236 Commands::Rspec { args } => rspec_cmd::run(&args, cli.verbose)?,
2237
2238 Commands::Pip { args } => pip_cmd::run(&args, cli.verbose)?,
2239
2240 Commands::Go { command } => match command {
2241 GoCommands::Test { args } => go_cmd::run_test(&args, cli.verbose)?,
2242 GoCommands::Build { args } => go_cmd::run_build(&args, cli.verbose)?,
2243 GoCommands::Vet { args } => go_cmd::run_vet(&args, cli.verbose)?,
2244 GoCommands::Other(args) => go_cmd::run_other(&args, cli.verbose)?,
2245 },
2246
2247 Commands::Gt { command } => match command {
2248 GtCommands::Log { args } => gt_cmd::run_log(&args, cli.verbose)?,
2249 GtCommands::Submit { args } => gt_cmd::run_submit(&args, cli.verbose)?,
2250 GtCommands::Sync { args } => gt_cmd::run_sync(&args, cli.verbose)?,
2251 GtCommands::Restack { args } => gt_cmd::run_restack(&args, cli.verbose)?,
2252 GtCommands::Create { args } => gt_cmd::run_create(&args, cli.verbose)?,
2253 GtCommands::Branch { args } => gt_cmd::run_branch(&args, cli.verbose)?,
2254 GtCommands::Other(args) => gt_cmd::run_other(&args, cli.verbose)?,
2255 },
2256
2257 Commands::GolangciLint { args } => golangci_cmd::run(&args, cli.verbose)?,
2258
2259 Commands::Gradlew { args } => gradlew_cmd::run(&args, cli.verbose)?,
2260
2261 Commands::Mvn { args } => mvn_cmd::run(&args, cli.verbose)?,
2262
2263 Commands::HookAudit { since } => {
2264 hooks::hook_audit_cmd::run(since, cli.verbose)?;
2265 0
2266 }
2267
2268 Commands::Hook { command } => match command {
2269 HookCommands::Claude => {
2270 hooks::hook_cmd::run_claude()?;
2271 0
2272 }
2273 HookCommands::Cursor => {
2274 hooks::hook_cmd::run_cursor()?;
2275 0
2276 }
2277 HookCommands::Gemini => {
2278 hooks::hook_cmd::run_gemini()?;
2279 0
2280 }
2281 HookCommands::Copilot => {
2282 hooks::hook_cmd::run_copilot()?;
2283 0
2284 }
2285 HookCommands::Check { agent: _, command } => {
2286 use crate::discover::registry::rewrite_command;
2287 let raw = command.join(" ");
2288 let (excluded, transparent_prefixes) = crate::core::config::Config::load()
2289 .map(|c| (c.hooks.exclude_commands, c.hooks.transparent_prefixes))
2290 .unwrap_or_default();
2291 match rewrite_command(&raw, &excluded, &transparent_prefixes) {
2292 Some(rewritten) => {
2293 println!("{}", rewritten);
2294 0
2295 }
2296 None => {
2297 eprintln!("No rewrite for: {}", raw);
2298 1
2299 }
2300 }
2301 }
2302 },
2303
2304 Commands::Rewrite { args } => {
2305 let cmd = args.join(" ");
2306 hooks::rewrite_cmd::run(&cmd)?;
2307 0
2308 }
2309
2310 Commands::Pipe {
2311 filter,
2312 passthrough,
2313 } => {
2314 pipe_cmd::run(filter.as_deref(), passthrough)?;
2315 0
2316 }
2317
2318 Commands::Run { command, args } => {
2319 let raw = match command {
2320 Some(c) => c,
2321 None if !args.is_empty() => args.join(" "),
2322 None => String::new(),
2323 };
2324 if raw.trim().is_empty() {
2325 0
2326 } else {
2327 use std::process::Command as ProcCommand;
2328 let shell = if cfg!(windows) { "cmd" } else { "sh" };
2329 let flag = if cfg!(windows) { "/C" } else { "-c" };
2330 let status = ProcCommand::new(shell)
2331 .arg(flag)
2332 .arg(&raw)
2333 .status()
2334 .with_context(|| format!("Failed to execute: {}", raw))?;
2335 status.code().unwrap_or(1)
2336 }
2337 }
2338
2339 Commands::Proxy { args } => {
2340 use std::io::{Read, Write};
2341 use std::process::Stdio;
2342 use std::sync::atomic::{AtomicU32, Ordering};
2343 use std::thread;
2344
2345 if args.is_empty() {
2346 anyhow::bail!(
2347 "proxy requires a command to execute\nUsage: rtk proxy <command> [args...]"
2348 );
2349 }
2350
2351 let timer = core::tracking::TimedExecution::start();
2352
2353 let (cmd_name, cmd_args): (String, Vec<String>) = if args.len() == 1 {
2357 let full = args[0].to_string_lossy();
2358 let parts = shell_split(&full);
2359 if parts.len() > 1 {
2360 (parts[0].clone(), parts[1..].to_vec())
2361 } else {
2362 (full.into_owned(), vec![])
2363 }
2364 } else {
2365 (
2366 args[0].to_string_lossy().into_owned(),
2367 args[1..]
2368 .iter()
2369 .map(|s| s.to_string_lossy().into_owned())
2370 .collect(),
2371 )
2372 };
2373
2374 if cli.verbose > 0 {
2375 eprintln!("Proxy mode: {} {}", cmd_name, cmd_args.join(" "));
2376 }
2377
2378 static PROXY_CHILD_PID: AtomicU32 = AtomicU32::new(0);
2383
2384 #[cfg(unix)]
2385 #[allow(unsafe_code)]
2386 {
2387 unsafe extern "C" fn handle_signal(sig: libc::c_int) {
2388 let pid = PROXY_CHILD_PID.load(Ordering::SeqCst);
2389 if pid != 0 {
2390 libc::kill(pid as libc::pid_t, libc::SIGTERM);
2391 libc::waitpid(pid as libc::pid_t, std::ptr::null_mut(), 0);
2392 }
2393 libc::signal(sig, libc::SIG_DFL);
2394 libc::raise(sig);
2395 }
2396 unsafe {
2398 libc::signal(
2399 libc::SIGINT,
2400 handle_signal as *const () as libc::sighandler_t,
2401 );
2402 libc::signal(
2403 libc::SIGTERM,
2404 handle_signal as *const () as libc::sighandler_t,
2405 );
2406 }
2407 }
2408
2409 struct ChildGuard(Option<std::process::Child>);
2410 impl Drop for ChildGuard {
2411 fn drop(&mut self) {
2412 if let Some(mut child) = self.0.take() {
2413 let _ = child.kill();
2414 let _ = child.wait();
2415 }
2416 PROXY_CHILD_PID.store(0, Ordering::SeqCst);
2417 }
2418 }
2419
2420 let mut child = ChildGuard(Some(
2421 core::utils::resolved_command(cmd_name.as_ref())
2422 .args(&cmd_args)
2423 .stdout(Stdio::piped())
2424 .stderr(Stdio::piped())
2425 .spawn()
2426 .context(format!("Failed to execute command: {}", cmd_name))?,
2427 ));
2428
2429 if let Some(ref inner) = child.0 {
2431 PROXY_CHILD_PID.store(inner.id(), Ordering::SeqCst);
2432 }
2433
2434 let inner = child.0.as_mut().context("Child process missing")?;
2435 let stdout_pipe = inner
2436 .stdout
2437 .take()
2438 .context("Failed to capture child stdout")?;
2439 let stderr_pipe = inner
2440 .stderr
2441 .take()
2442 .context("Failed to capture child stderr")?;
2443
2444 const CAP: usize = 1_048_576;
2445
2446 let stdout_handle = thread::spawn(move || -> std::io::Result<Vec<u8>> {
2447 let mut reader = stdout_pipe;
2448 let mut captured = Vec::new();
2449 let mut buf = [0u8; 8192];
2450
2451 loop {
2452 let count = reader.read(&mut buf)?;
2453 if count == 0 {
2454 break;
2455 }
2456 if captured.len() < CAP {
2457 let take = count.min(CAP - captured.len());
2458 captured.extend_from_slice(&buf[..take]);
2459 }
2460 let mut out = std::io::stdout().lock();
2461 out.write_all(&buf[..count])?;
2462 out.flush()?;
2463 }
2464
2465 Ok(captured)
2466 });
2467
2468 let stderr_handle = thread::spawn(move || -> std::io::Result<Vec<u8>> {
2469 let mut reader = stderr_pipe;
2470 let mut captured = Vec::new();
2471 let mut buf = [0u8; 8192];
2472
2473 loop {
2474 let count = reader.read(&mut buf)?;
2475 if count == 0 {
2476 break;
2477 }
2478 if captured.len() < CAP {
2479 let take = count.min(CAP - captured.len());
2480 captured.extend_from_slice(&buf[..take]);
2481 }
2482 let mut err = std::io::stderr().lock();
2483 err.write_all(&buf[..count])?;
2484 err.flush()?;
2485 }
2486
2487 Ok(captured)
2488 });
2489
2490 let status = child
2491 .0
2492 .take()
2493 .context("Child process missing")?
2494 .wait()
2495 .context(format!("Failed waiting for command: {}", cmd_name))?;
2496
2497 let stdout_bytes = stdout_handle
2498 .join()
2499 .map_err(|_| anyhow::anyhow!("stdout streaming thread panicked"))??;
2500 let stderr_bytes = stderr_handle
2501 .join()
2502 .map_err(|_| anyhow::anyhow!("stderr streaming thread panicked"))??;
2503
2504 let stdout = String::from_utf8_lossy(&stdout_bytes);
2505 let stderr = String::from_utf8_lossy(&stderr_bytes);
2506 let full_output = format!("{}{}", stdout, stderr);
2507
2508 timer.track(
2510 &format!("{} {}", cmd_name, cmd_args.join(" ")),
2511 &format!("rtk proxy {} {}", cmd_name, cmd_args.join(" ")),
2512 &full_output,
2513 &full_output,
2514 );
2515
2516 core::utils::exit_code_from_status(&status, &cmd_name)
2517 }
2518
2519 Commands::Trust { list } => {
2520 hooks::trust::run_trust(list)?;
2521 0
2522 }
2523
2524 Commands::Untrust => {
2525 hooks::trust::run_untrust()?;
2526 0
2527 }
2528
2529 Commands::Verify {
2530 filter,
2531 require_all,
2532 } => {
2533 if filter.is_some() {
2534 hooks::verify_cmd::run(filter, require_all)?;
2536 } else {
2537 hooks::integrity::run_verify(cli.verbose)?;
2539 hooks::verify_cmd::run(None, require_all)?;
2540 }
2541 0
2542 }
2543 };
2544
2545 Ok(code)
2546}
2547
2548fn hosted_mode() -> bool {
2549 cfg!(feature = "hosted") || std::env::var("RTK_HOSTED").unwrap_or_default() == "1"
2550}
2551
2552fn is_operational_command(cmd: &Commands) -> bool {
2563 matches!(
2564 cmd,
2565 Commands::Ls { .. }
2566 | Commands::Tree { .. }
2567 | Commands::Read { .. }
2568 | Commands::Smart { .. }
2569 | Commands::Git { .. }
2570 | Commands::Gh { .. }
2571 | Commands::Glab { .. }
2572 | Commands::Pnpm { .. }
2573 | Commands::Err { .. }
2574 | Commands::Test { .. }
2575 | Commands::Json { .. }
2576 | Commands::Deps { .. }
2577 | Commands::Env { .. }
2578 | Commands::Find { .. }
2579 | Commands::Diff { .. }
2580 | Commands::Log { .. }
2581 | Commands::Dotnet { .. }
2582 | Commands::Docker { .. }
2583 | Commands::Kubectl { .. }
2584 | Commands::Summary { .. }
2585 | Commands::Grep { .. }
2586 | Commands::Wget { .. }
2587 | Commands::Vitest { .. }
2588 | Commands::Prisma { .. }
2589 | Commands::Tsc { .. }
2590 | Commands::Next { .. }
2591 | Commands::Lint { .. }
2592 | Commands::Prettier { .. }
2593 | Commands::Playwright { .. }
2594 | Commands::Cargo { .. }
2595 | Commands::Npm { .. }
2596 | Commands::Npx { .. }
2597 | Commands::Curl { .. }
2598 | Commands::Ruff { .. }
2599 | Commands::Pytest { .. }
2600 | Commands::Rake { .. }
2601 | Commands::Rubocop { .. }
2602 | Commands::Rspec { .. }
2603 | Commands::Pip { .. }
2604 | Commands::Go { .. }
2605 | Commands::GolangciLint { .. }
2606 | Commands::Gt { .. }
2607 )
2608}
2609
2610#[cfg(test)]
2611mod tests {
2612 use super::*;
2613 use clap::Parser;
2614 use std::cell::Cell;
2615
2616 #[test]
2622 #[cfg(unix)]
2623 fn test_run_fallback_uses_passed_argv() {
2624 let argv: Vec<OsString> = ["rtk", "true"].iter().map(OsString::from).collect();
2627 let parse_error = match Cli::try_parse_from(&argv) {
2628 Ok(_) => panic!("`true` must not parse as an rtk subcommand"),
2629 Err(e) => e,
2630 };
2631
2632 let code = run_fallback(parse_error, &argv).expect("fallback should execute `true`");
2633
2634 assert_eq!(code, 0, "fallback must run `true` from the passed argv");
2635 }
2636
2637 #[test]
2638 fn test_git_commit_single_message() {
2639 let cli = Cli::try_parse_from(["rtk", "git", "commit", "-m", "fix: typo"]).unwrap();
2640 match cli.command {
2641 Commands::Git {
2642 command: GitCommands::Commit { args },
2643 ..
2644 } => {
2645 assert_eq!(args, vec!["-m", "fix: typo"]);
2646 }
2647 _ => panic!("Expected Git Commit command"),
2648 }
2649 }
2650
2651 #[test]
2652 fn test_git_commit_multiple_messages() {
2653 let cli = Cli::try_parse_from([
2654 "rtk",
2655 "git",
2656 "commit",
2657 "-m",
2658 "feat: add support",
2659 "-m",
2660 "Body paragraph here.",
2661 ])
2662 .unwrap();
2663 match cli.command {
2664 Commands::Git {
2665 command: GitCommands::Commit { args },
2666 ..
2667 } => {
2668 assert_eq!(
2669 args,
2670 vec!["-m", "feat: add support", "-m", "Body paragraph here."]
2671 );
2672 }
2673 _ => panic!("Expected Git Commit command"),
2674 }
2675 }
2676
2677 #[test]
2679 fn test_git_commit_am_flag() {
2680 let cli = Cli::try_parse_from(["rtk", "git", "commit", "-am", "quick fix"]).unwrap();
2681 match cli.command {
2682 Commands::Git {
2683 command: GitCommands::Commit { args },
2684 ..
2685 } => {
2686 assert_eq!(args, vec!["-am", "quick fix"]);
2687 }
2688 _ => panic!("Expected Git Commit command"),
2689 }
2690 }
2691
2692 #[test]
2693 fn test_git_commit_amend() {
2694 let cli =
2695 Cli::try_parse_from(["rtk", "git", "commit", "--amend", "-m", "new msg"]).unwrap();
2696 match cli.command {
2697 Commands::Git {
2698 command: GitCommands::Commit { args },
2699 ..
2700 } => {
2701 assert_eq!(args, vec!["--amend", "-m", "new msg"]);
2702 }
2703 _ => panic!("Expected Git Commit command"),
2704 }
2705 }
2706
2707 #[test]
2708 fn test_git_global_options_parsing() {
2709 let cli =
2710 Cli::try_parse_from(["rtk", "git", "--no-pager", "--no-optional-locks", "status"])
2711 .unwrap();
2712 match cli.command {
2713 Commands::Git {
2714 no_pager,
2715 no_optional_locks,
2716 bare,
2717 literal_pathspecs,
2718 ..
2719 } => {
2720 assert!(no_pager);
2721 assert!(no_optional_locks);
2722 assert!(!bare);
2723 assert!(!literal_pathspecs);
2724 }
2725 _ => panic!("Expected Git command"),
2726 }
2727 }
2728
2729 #[test]
2730 fn test_git_commit_long_flag_multiple() {
2731 let cli = Cli::try_parse_from([
2732 "rtk",
2733 "git",
2734 "commit",
2735 "--message",
2736 "title",
2737 "--message",
2738 "body",
2739 "--message",
2740 "footer",
2741 ])
2742 .unwrap();
2743 match cli.command {
2744 Commands::Git {
2745 command: GitCommands::Commit { args },
2746 ..
2747 } => {
2748 assert_eq!(
2749 args,
2750 vec![
2751 "--message",
2752 "title",
2753 "--message",
2754 "body",
2755 "--message",
2756 "footer"
2757 ]
2758 );
2759 }
2760 _ => panic!("Expected Git Commit command"),
2761 }
2762 }
2763
2764 #[test]
2765 fn test_try_parse_valid_git_status() {
2766 let result = Cli::try_parse_from(["rtk", "git", "status"]);
2767 assert!(result.is_ok(), "git status should parse successfully");
2768 }
2769
2770 #[test]
2771 fn test_try_parse_init_agent_hermes() {
2772 let cli = Cli::try_parse_from(["rtk", "init", "--agent", "hermes"]).unwrap();
2773 match cli.command {
2774 Commands::Init { agent, .. } => {
2775 assert_eq!(agent, Some(AgentTarget::Hermes));
2776 }
2777 _ => panic!("Expected Init command"),
2778 }
2779 }
2780
2781 #[test]
2782 fn test_try_parse_kubectl_get_alias() {
2783 let cli = Cli::try_parse_from(["rtk", "kubectl", "get", "pods", "-n", "default"]).unwrap();
2784
2785 match cli.command {
2786 Commands::Kubectl {
2787 command: KubectlCommands::Get { args },
2788 } => assert_eq!(args, vec!["pods", "-n", "default"]),
2789 _ => panic!("Expected Kubectl Get command"),
2790 }
2791 }
2792
2793 #[test]
2794 fn test_try_parse_init_agent_hermes_uninstall() {
2795 let cli = Cli::try_parse_from(["rtk", "init", "--agent", "hermes", "--uninstall"]).unwrap();
2796 match cli.command {
2797 Commands::Init {
2798 agent, uninstall, ..
2799 } => {
2800 assert_eq!(agent, Some(AgentTarget::Hermes));
2801 assert!(uninstall);
2802 }
2803 _ => panic!("Expected Init command"),
2804 }
2805 }
2806
2807 #[test]
2808 fn test_init_uninstall_dispatch_routes_hermes_to_hermes_cleanup() {
2809 let hermes_called = Cell::new(false);
2810 let standard_called = Cell::new(false);
2811 let ctx = hooks::init::InitContext {
2812 verbose: 2,
2813 dry_run: true,
2814 };
2815
2816 let result = uninstall_init_dispatch(
2817 Some(AgentTarget::Hermes),
2818 true,
2819 false,
2820 false,
2821 ctx,
2822 |ctx| {
2823 hermes_called.set(true);
2824 assert_eq!(ctx.verbose, 2);
2825 assert!(ctx.dry_run);
2826 Ok(())
2827 },
2828 |_, _, _, _, _, _| {
2829 standard_called.set(true);
2830 Ok(())
2831 },
2832 );
2833
2834 assert!(result.is_ok());
2835 assert!(hermes_called.get());
2836 assert!(!standard_called.get());
2837 }
2838
2839 #[test]
2840 fn test_try_parse_help_is_display_help() {
2841 match Cli::try_parse_from(["rtk", "--help"]) {
2842 Err(e) => assert_eq!(e.kind(), ErrorKind::DisplayHelp),
2843 Ok(_) => panic!("Expected DisplayHelp error"),
2844 }
2845 }
2846
2847 #[test]
2848 fn test_try_parse_version_is_display_version() {
2849 match Cli::try_parse_from(["rtk", "--version"]) {
2850 Err(e) => assert_eq!(e.kind(), ErrorKind::DisplayVersion),
2851 Ok(_) => panic!("Expected DisplayVersion error"),
2852 }
2853 }
2854
2855 #[test]
2856 fn test_try_parse_unknown_subcommand_is_error() {
2857 match Cli::try_parse_from(["rtk", "nonexistent-command"]) {
2858 Err(e) => assert!(!matches!(
2859 e.kind(),
2860 ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
2861 )),
2862 Ok(_) => panic!("Expected parse error for unknown subcommand"),
2863 }
2864 }
2865
2866 #[test]
2867 fn test_try_parse_git_with_dash_c_succeeds() {
2868 let result = Cli::try_parse_from(["rtk", "git", "-C", "/path", "status"]);
2869 assert!(
2870 result.is_ok(),
2871 "git -C /path status should parse successfully"
2872 );
2873 if let Ok(cli) = result {
2874 match cli.command {
2875 Commands::Git { directory, .. } => {
2876 assert_eq!(directory, vec!["/path"]);
2877 }
2878 _ => panic!("Expected Git command"),
2879 }
2880 }
2881 }
2882
2883 #[test]
2884 fn test_gain_failures_flag_parses() {
2885 let result = Cli::try_parse_from(["rtk", "gain", "--failures"]);
2886 assert!(result.is_ok());
2887 if let Ok(cli) = result {
2888 match cli.command {
2889 Commands::Gain { failures, .. } => assert!(failures),
2890 _ => panic!("Expected Gain command"),
2891 }
2892 }
2893 }
2894
2895 #[test]
2896 fn test_gain_failures_short_flag_parses() {
2897 let result = Cli::try_parse_from(["rtk", "gain", "-F"]);
2898 assert!(result.is_ok());
2899 if let Ok(cli) = result {
2900 match cli.command {
2901 Commands::Gain { failures, .. } => assert!(failures),
2902 _ => panic!("Expected Gain command"),
2903 }
2904 }
2905 }
2906
2907 #[test]
2908 fn test_meta_commands_reject_bad_flags() {
2909 for cmd in RTK_META_COMMANDS {
2912 if matches!(*cmd, "proxy" | "run" | "rewrite" | "session") {
2913 continue; }
2915 let result = Cli::try_parse_from(["rtk", cmd, "--nonexistent-flag-xyz"]);
2916 assert!(
2917 result.is_err(),
2918 "Meta-command '{}' with bad flag should fail to parse",
2919 cmd
2920 );
2921 }
2922 }
2923
2924 #[test]
2925 fn test_run_command_with_dash_c() {
2926 let cli = Cli::try_parse_from(["rtk", "run", "-c", "git status && echo done"]).unwrap();
2927 match cli.command {
2928 Commands::Run { command, args } => {
2929 assert_eq!(command, Some("git status && echo done".to_string()));
2930 assert!(args.is_empty());
2931 }
2932 _ => panic!("Expected Run command"),
2933 }
2934 }
2935
2936 #[test]
2937 fn test_run_command_positional_args() {
2938 let cli = Cli::try_parse_from(["rtk", "run", "echo", "hello"]).unwrap();
2939 match cli.command {
2940 Commands::Run { command, args } => {
2941 assert!(command.is_none());
2942 assert_eq!(args, vec!["echo", "hello"]);
2943 }
2944 _ => panic!("Expected Run command"),
2945 }
2946 }
2947
2948 #[test]
2949 fn test_hook_claude_parses() {
2950 let cli = Cli::try_parse_from(["rtk", "hook", "claude"]).unwrap();
2951 assert!(matches!(
2952 cli.command,
2953 Commands::Hook {
2954 command: HookCommands::Claude
2955 }
2956 ));
2957 }
2958
2959 #[test]
2960 fn test_hook_check_parses() {
2961 let cli = Cli::try_parse_from(["rtk", "hook", "check", "git", "status"]).unwrap();
2962 match cli.command {
2963 Commands::Hook {
2964 command: HookCommands::Check { agent, command },
2965 } => {
2966 assert_eq!(agent, "claude");
2967 assert_eq!(command, vec!["git", "status"]);
2968 }
2969 _ => panic!("Expected Hook Check command"),
2970 }
2971 }
2972
2973 #[test]
2974 fn test_hook_check_with_agent() {
2975 let cli =
2976 Cli::try_parse_from(["rtk", "hook", "check", "--agent", "gemini", "cargo", "test"])
2977 .unwrap();
2978 match cli.command {
2979 Commands::Hook {
2980 command: HookCommands::Check { agent, command },
2981 } => {
2982 assert_eq!(agent, "gemini");
2983 assert_eq!(command, vec!["cargo", "test"]);
2984 }
2985 _ => panic!("Expected Hook Check command"),
2986 }
2987 }
2988
2989 #[test]
2990 fn test_hook_check_preserves_double_dash_in_command() {
2991 let cli = Cli::try_parse_from([
2992 "rtk",
2993 "hook",
2994 "check",
2995 "shadowenv",
2996 "exec",
2997 "--",
2998 "git",
2999 "status",
3000 ])
3001 .unwrap();
3002 match cli.command {
3003 Commands::Hook {
3004 command: HookCommands::Check { agent, command },
3005 } => {
3006 assert_eq!(agent, "claude");
3007 assert_eq!(command, vec!["shadowenv", "exec", "--", "git", "status"]);
3008 }
3009 _ => panic!("Expected Hook Check command"),
3010 }
3011 }
3012
3013 #[test]
3014 fn test_meta_command_list_is_complete() {
3015 let meta_cmds_that_parse = [
3017 vec!["rtk", "gain"],
3018 vec!["rtk", "discover"],
3019 vec!["rtk", "learn"],
3020 vec!["rtk", "init"],
3021 vec!["rtk", "config"],
3022 vec!["rtk", "proxy", "echo", "hi"],
3023 vec!["rtk", "run", "-c", "echo hi"],
3024 vec!["rtk", "hook-audit"],
3025 vec!["rtk", "cc-economics"],
3026 ];
3027 for args in &meta_cmds_that_parse {
3028 let result = Cli::try_parse_from(args.iter());
3029 assert!(
3030 result.is_ok(),
3031 "Meta-command {:?} should parse successfully",
3032 args
3033 );
3034 }
3035 }
3036
3037 #[test]
3038 fn test_shell_split_simple() {
3039 assert_eq!(
3040 shell_split("head -50 file.php"),
3041 vec!["head", "-50", "file.php"]
3042 );
3043 }
3044
3045 #[test]
3046 fn test_shell_split_double_quotes() {
3047 assert_eq!(
3048 shell_split(r#"git log --format="%H %s""#),
3049 vec!["git", "log", "--format=%H %s"]
3050 );
3051 }
3052
3053 #[test]
3054 fn test_shell_split_single_quotes() {
3055 assert_eq!(
3056 shell_split("grep -r 'hello world' ."),
3057 vec!["grep", "-r", "hello world", "."]
3058 );
3059 }
3060
3061 #[test]
3062 fn test_shell_split_single_word() {
3063 assert_eq!(shell_split("ls"), vec!["ls"]);
3064 }
3065
3066 #[test]
3067 fn test_shell_split_empty() {
3068 let result: Vec<String> = shell_split("");
3069 assert!(result.is_empty());
3070 }
3071
3072 #[test]
3073 fn test_rewrite_clap_multi_args() {
3074 let cases = vec![
3078 vec!["rtk", "rewrite", "ls", "-al"],
3079 vec!["rtk", "rewrite", "git", "status"],
3080 vec!["rtk", "rewrite", "npm", "exec"],
3081 vec!["rtk", "rewrite", "cargo", "test"],
3082 vec!["rtk", "rewrite", "du", "-sh", "."],
3083 vec!["rtk", "rewrite", "head", "-50", "file.txt"],
3084 ];
3085 for args in &cases {
3086 let result = Cli::try_parse_from(args.iter());
3087 assert!(
3088 result.is_ok(),
3089 "rtk rewrite {:?} should parse (was failing before trailing_var_arg fix)",
3090 &args[2..]
3091 );
3092 if let Ok(cli) = result {
3093 match cli.command {
3094 Commands::Rewrite { ref args } => {
3095 assert!(args.len() >= 2, "rewrite args should capture all tokens");
3096 }
3097 _ => panic!("expected Rewrite command"),
3098 }
3099 }
3100 }
3101 }
3102
3103 #[test]
3104 fn test_rewrite_clap_quoted_single_arg() {
3105 let result = Cli::try_parse_from(["rtk", "rewrite", "git status"]);
3107 assert!(result.is_ok());
3108 if let Ok(cli) = result {
3109 match cli.command {
3110 Commands::Rewrite { ref args } => {
3111 assert_eq!(args.len(), 1);
3112 assert_eq!(args[0], "git status");
3113 }
3114 _ => panic!("expected Rewrite command"),
3115 }
3116 }
3117 }
3118
3119 #[test]
3120 fn test_merge_filters_with_no_args() {
3121 let filters = vec![];
3122 let args = vec!["--depth=0".to_string(), "--no-verbose".to_string()];
3123 let expected_args = vec!["--depth=0", "--no-verbose"];
3124 assert_eq!(merge_pnpm_args(&filters, &args), expected_args);
3125 }
3126
3127 #[test]
3128 fn test_merge_filters_with_args() {
3129 let filters = vec!["@app1".to_string(), "@app2".to_string()];
3130 let args = vec![
3131 "--filter=@app3".to_string(),
3132 "--depth=0".to_string(),
3133 "--no-verbose".to_string(),
3134 ];
3135 let expected_args = vec![
3136 "--filter=@app1",
3137 "--filter=@app2",
3138 "--filter=@app3",
3139 "--depth=0",
3140 "--no-verbose",
3141 ];
3142 assert_eq!(merge_pnpm_args(&filters, &args), expected_args);
3143 }
3144
3145 #[test]
3146 fn test_merge_filters_with_no_args_os() {
3147 let filters = vec![];
3148 let args = vec![OsString::from("--depth=0")];
3149 let expected_args = vec![OsString::from("--depth=0")];
3150 assert_eq!(merge_pnpm_args_os(&filters, &args), expected_args);
3151 }
3152
3153 #[test]
3154 fn test_merge_filters_with_args_os() {
3155 let filters = vec!["@app1".to_string()];
3156 let args = vec![OsString::from("--depth=0")];
3157 let expected_args = vec![
3158 OsString::from("--filter=@app1"),
3159 OsString::from("--depth=0"),
3160 ];
3161 assert_eq!(merge_pnpm_args_os(&filters, &args), expected_args);
3162 }
3163
3164 #[test]
3165 fn test_pnpm_subcommand_with_filter() {
3166 let cli = Cli::try_parse_from([
3167 "rtk", "pnpm", "--filter", "@app1", "--filter", "@app2", "list", "--filter", "@app3",
3168 "--filter", "@app4", "--prod",
3169 ])
3170 .unwrap();
3171 match cli.command {
3172 Commands::Pnpm {
3173 filter,
3174 command: PnpmCommands::List { depth, args },
3175 } => {
3176 assert_eq!(depth, 0);
3177 assert_eq!(filter, vec!["@app1", "@app2"]);
3178 assert_eq!(
3179 args,
3180 vec!["--filter", "@app3", "--filter", "@app4", "--prod"]
3181 );
3182 }
3183 _ => panic!("Expected Pnpm List command"),
3184 }
3185 }
3186
3187 #[test]
3188 fn test_git_push_u_flag_passes_through() {
3189 let cli = Cli::try_parse_from(["rtk", "git", "push", "-u", "origin", "my-branch"]).unwrap();
3190 assert!(
3191 !cli.ultra_compact,
3192 "-u on git push must NOT be consumed as --ultra-compact"
3193 );
3194 match cli.command {
3195 Commands::Git {
3196 command: GitCommands::Push { args },
3197 ..
3198 } => {
3199 assert!(
3200 args.contains(&"-u".to_string()),
3201 "-u must be forwarded to git push, got: {:?}",
3202 args
3203 );
3204 }
3205 _ => panic!("Expected Git Push command"),
3206 }
3207 }
3208
3209 #[test]
3210 fn test_pnpm_subcommand_with_short_filter() {
3211 let cli =
3213 Cli::try_parse_from(["rtk", "pnpm", "-F", "@app1", "-F", "@app2", "list"]).unwrap();
3214 match cli.command {
3215 Commands::Pnpm { filter, .. } => {
3216 assert_eq!(filter, vec!["@app1", "@app2"]);
3217 }
3218 _ => panic!("Expected Pnpm command"),
3219 }
3220 }
3221
3222 #[test]
3223 fn test_pnpm_typecheck_without_filters() {
3224 let cli = Cli::try_parse_from([
3225 "rtk",
3226 "pnpm",
3227 "typecheck",
3228 "--filter",
3229 "@app3",
3230 "--filter",
3231 "@app4",
3232 ])
3233 .unwrap();
3234 match cli.command {
3235 Commands::Pnpm { filter, command } => {
3236 let warning = validate_pnpm_filters(&filter, &command);
3237
3238 assert!(filter.is_empty());
3239 assert!(warning.is_none())
3240 }
3241 _ => panic!("Expected Pnpm Build command"),
3242 }
3243 }
3244
3245 #[test]
3246 fn test_pnpm_typecheck_with_filters() {
3247 let cli = Cli::try_parse_from([
3248 "rtk",
3249 "pnpm",
3250 "--filter",
3251 "@app1",
3252 "--filter",
3253 "@app2",
3254 "typecheck",
3255 "--filter",
3256 "@app3",
3257 "--filter",
3258 "@app4",
3259 ])
3260 .unwrap();
3261 match cli.command {
3262 Commands::Pnpm { filter, command } => {
3263 let warning = validate_pnpm_filters(&filter, &command).unwrap();
3264
3265 assert_eq!(filter, vec!["@app1", "@app2"]);
3266 assert_eq!(warning, "[rtk] warning: --filter is not yet supported for pnpm tsc, filters preceding the subcommand will be ignored")
3267 }
3268 _ => panic!("Expected Pnpm Build command"),
3269 }
3270 }
3271
3272 #[test]
3273 #[ignore] fn test_broken_pipe_does_not_crash() {
3275 let bin_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3276 .join("target")
3277 .join("debug")
3278 .join("rtk");
3279 assert!(
3280 bin_path.exists(),
3281 "Debug binary not found at {:?} - run `cargo build` first",
3282 bin_path
3283 );
3284
3285 let mut child = std::process::Command::new(&bin_path)
3286 .args(["git", "log", "--oneline", "-50"])
3287 .stdout(std::process::Stdio::piped())
3288 .stderr(std::process::Stdio::piped())
3289 .spawn()
3290 .expect("Failed to spawn rtk");
3291
3292 let mut stdout = child.stdout.take().unwrap();
3294 let mut buf = [0u8; 1];
3295 let _ = std::io::Read::read(&mut stdout, &mut buf);
3296
3297 let status = child.wait().expect("Failed to wait for rtk");
3298 let code = status.code().unwrap_or(-1);
3299
3300 assert_ne!(
3301 code, 134,
3302 "rtk crashed with SIGABRT (exit 134) on broken pipe - SIGPIPE handler missing"
3303 );
3304 }
3305
3306 #[test]
3307 fn test_ultra_compact_long_form_still_works() {
3308 let cli = Cli::try_parse_from(["rtk", "--ultra-compact", "git", "status"]).unwrap();
3309 assert!(
3310 cli.ultra_compact,
3311 "--ultra-compact long form must still enable ultra-compact mode"
3312 );
3313 }
3314
3315 #[test]
3316 fn test_npx_unknown_tool_passthrough() {
3317 let cli = Cli::try_parse_from(["rtk", "npx", "cowsay", "hello"]).unwrap();
3322 match cli.command {
3323 Commands::Npx { args } => {
3324 assert_eq!(args, vec!["cowsay", "hello"]);
3325 }
3326 _ => panic!("Expected Commands::Npx for unknown tool"),
3327 }
3328 }
3329
3330 #[test]
3331 fn test_init_pi_flag_rejected() {
3332 let result = Cli::try_parse_from(["rtk", "init", "--pi"]);
3334 assert!(result.is_err(), "--pi must be rejected as unknown argument");
3335 }
3336
3337 #[test]
3338 fn test_init_agent_pi_parses() {
3339 let cli = Cli::try_parse_from(["rtk", "init", "--agent", "pi"]).unwrap();
3340 match cli.command {
3341 Commands::Init { agent, .. } => {
3342 assert_eq!(
3343 agent,
3344 Some(AgentTarget::Pi),
3345 "--agent pi must set Pi variant"
3346 );
3347 }
3348 _ => panic!("Expected Init command"),
3349 }
3350 }
3351
3352 #[test]
3353 fn test_init_uninstall_agent_pi_parses() {
3354 let cli = Cli::try_parse_from(["rtk", "init", "--uninstall", "--agent", "pi", "--global"])
3355 .unwrap();
3356 match cli.command {
3357 Commands::Init {
3358 uninstall,
3359 agent,
3360 global,
3361 ..
3362 } => {
3363 assert!(uninstall);
3364 assert_eq!(agent, Some(AgentTarget::Pi));
3365 assert!(global);
3366 }
3367 _ => panic!("Expected Init command"),
3368 }
3369 }
3370}