@hasna/terminal 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/dist/App.js +404 -0
  2. package/dist/Browse.js +79 -0
  3. package/dist/FuzzyPicker.js +47 -0
  4. package/dist/Onboarding.js +51 -0
  5. package/dist/Spinner.js +12 -0
  6. package/dist/StatusBar.js +49 -0
  7. package/dist/ai.js +322 -0
  8. package/dist/cache.js +41 -0
  9. package/dist/cli.js +64 -16
  10. package/dist/command-rewriter.js +64 -0
  11. package/dist/command-validator.js +86 -0
  12. package/dist/compression.js +107 -0
  13. package/dist/context-hints.js +275 -0
  14. package/dist/diff-cache.js +107 -0
  15. package/dist/discover.js +212 -0
  16. package/dist/economy.js +123 -0
  17. package/dist/expand-store.js +38 -0
  18. package/dist/file-cache.js +72 -0
  19. package/dist/file-index.js +62 -0
  20. package/dist/history.js +62 -0
  21. package/dist/lazy-executor.js +54 -0
  22. package/dist/line-dedup.js +59 -0
  23. package/dist/loop-detector.js +75 -0
  24. package/dist/mcp/install.js +98 -0
  25. package/dist/mcp/server.js +569 -0
  26. package/dist/noise-filter.js +86 -0
  27. package/dist/output-processor.js +129 -0
  28. package/dist/output-router.js +41 -0
  29. package/dist/output-store.js +111 -0
  30. package/dist/parsers/base.js +2 -0
  31. package/dist/parsers/build.js +64 -0
  32. package/dist/parsers/errors.js +101 -0
  33. package/dist/parsers/files.js +78 -0
  34. package/dist/parsers/git.js +99 -0
  35. package/dist/parsers/index.js +48 -0
  36. package/dist/parsers/tests.js +89 -0
  37. package/dist/providers/anthropic.js +39 -0
  38. package/dist/providers/base.js +4 -0
  39. package/dist/providers/cerebras.js +95 -0
  40. package/dist/providers/groq.js +95 -0
  41. package/dist/providers/index.js +73 -0
  42. package/dist/providers/xai.js +95 -0
  43. package/dist/recipes/model.js +20 -0
  44. package/dist/recipes/storage.js +136 -0
  45. package/dist/search/content-search.js +68 -0
  46. package/dist/search/file-search.js +61 -0
  47. package/dist/search/filters.js +34 -0
  48. package/dist/search/index.js +5 -0
  49. package/dist/search/semantic.js +320 -0
  50. package/dist/session-boot.js +59 -0
  51. package/dist/session-context.js +55 -0
  52. package/dist/sessions-db.js +173 -0
  53. package/dist/smart-display.js +286 -0
  54. package/dist/snapshots.js +51 -0
  55. package/dist/supervisor.js +112 -0
  56. package/dist/test-watchlist.js +131 -0
  57. package/dist/tool-profiles.js +122 -0
  58. package/dist/tree.js +94 -0
  59. package/dist/usage-cache.js +65 -0
  60. package/package.json +8 -1
  61. package/src/ai.ts +8 -0
  62. package/src/cli.tsx +57 -18
  63. package/src/output-processor.ts +6 -1
  64. package/src/output-store.ts +58 -12
  65. package/src/tool-profiles.ts +139 -0
  66. package/.claude/scheduled_tasks.lock +0 -1
  67. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
  68. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
  69. package/CONTRIBUTING.md +0 -80
  70. package/benchmarks/benchmark.mjs +0 -115
  71. package/imported_modules.txt +0 -0
  72. package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
  73. package/temp/rtk/.claude/agents/debugger.md +0 -519
  74. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
  75. package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
  76. package/temp/rtk/.claude/agents/technical-writer.md +0 -355
  77. package/temp/rtk/.claude/commands/diagnose.md +0 -352
  78. package/temp/rtk/.claude/commands/test-routing.md +0 -362
  79. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
  80. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
  81. package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
  82. package/temp/rtk/.claude/rules/cli-testing.md +0 -526
  83. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
  84. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
  85. package/temp/rtk/.claude/skills/performance.md +0 -435
  86. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
  87. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
  88. package/temp/rtk/.claude/skills/repo-recap.md +0 -206
  89. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
  90. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
  91. package/temp/rtk/.claude/skills/security-guardian.md +0 -503
  92. package/temp/rtk/.claude/skills/ship.md +0 -404
  93. package/temp/rtk/.github/workflows/benchmark.yml +0 -34
  94. package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
  95. package/temp/rtk/.github/workflows/release-please.yml +0 -51
  96. package/temp/rtk/.github/workflows/release.yml +0 -343
  97. package/temp/rtk/.github/workflows/security-check.yml +0 -135
  98. package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
  99. package/temp/rtk/.release-please-manifest.json +0 -3
  100. package/temp/rtk/ARCHITECTURE.md +0 -1491
  101. package/temp/rtk/CHANGELOG.md +0 -640
  102. package/temp/rtk/CLAUDE.md +0 -605
  103. package/temp/rtk/CONTRIBUTING.md +0 -199
  104. package/temp/rtk/Cargo.lock +0 -1668
  105. package/temp/rtk/Cargo.toml +0 -64
  106. package/temp/rtk/Formula/rtk.rb +0 -43
  107. package/temp/rtk/INSTALL.md +0 -390
  108. package/temp/rtk/LICENSE +0 -21
  109. package/temp/rtk/README.md +0 -386
  110. package/temp/rtk/README_es.md +0 -159
  111. package/temp/rtk/README_fr.md +0 -197
  112. package/temp/rtk/README_ja.md +0 -159
  113. package/temp/rtk/README_ko.md +0 -159
  114. package/temp/rtk/README_zh.md +0 -167
  115. package/temp/rtk/ROADMAP.md +0 -15
  116. package/temp/rtk/SECURITY.md +0 -217
  117. package/temp/rtk/TEST_EXEC_TIME.md +0 -102
  118. package/temp/rtk/build.rs +0 -57
  119. package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
  120. package/temp/rtk/docs/FEATURES.md +0 -1410
  121. package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
  122. package/temp/rtk/docs/filter-workflow.md +0 -102
  123. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  124. package/temp/rtk/docs/tracking.md +0 -583
  125. package/temp/rtk/hooks/opencode-rtk.ts +0 -39
  126. package/temp/rtk/hooks/rtk-awareness.md +0 -29
  127. package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
  128. package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
  129. package/temp/rtk/install.sh +0 -124
  130. package/temp/rtk/release-please-config.json +0 -10
  131. package/temp/rtk/scripts/benchmark.sh +0 -592
  132. package/temp/rtk/scripts/check-installation.sh +0 -162
  133. package/temp/rtk/scripts/install-local.sh +0 -37
  134. package/temp/rtk/scripts/rtk-economics.sh +0 -137
  135. package/temp/rtk/scripts/test-all.sh +0 -561
  136. package/temp/rtk/scripts/test-aristote.sh +0 -227
  137. package/temp/rtk/scripts/test-tracking.sh +0 -79
  138. package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
  139. package/temp/rtk/scripts/validate-docs.sh +0 -73
  140. package/temp/rtk/src/aws_cmd.rs +0 -880
  141. package/temp/rtk/src/binlog.rs +0 -1645
  142. package/temp/rtk/src/cargo_cmd.rs +0 -1727
  143. package/temp/rtk/src/cc_economics.rs +0 -1157
  144. package/temp/rtk/src/ccusage.rs +0 -340
  145. package/temp/rtk/src/config.rs +0 -187
  146. package/temp/rtk/src/container.rs +0 -855
  147. package/temp/rtk/src/curl_cmd.rs +0 -134
  148. package/temp/rtk/src/deps.rs +0 -268
  149. package/temp/rtk/src/diff_cmd.rs +0 -367
  150. package/temp/rtk/src/discover/mod.rs +0 -274
  151. package/temp/rtk/src/discover/provider.rs +0 -388
  152. package/temp/rtk/src/discover/registry.rs +0 -2022
  153. package/temp/rtk/src/discover/report.rs +0 -202
  154. package/temp/rtk/src/discover/rules.rs +0 -667
  155. package/temp/rtk/src/display_helpers.rs +0 -402
  156. package/temp/rtk/src/dotnet_cmd.rs +0 -1771
  157. package/temp/rtk/src/dotnet_format_report.rs +0 -133
  158. package/temp/rtk/src/dotnet_trx.rs +0 -593
  159. package/temp/rtk/src/env_cmd.rs +0 -204
  160. package/temp/rtk/src/filter.rs +0 -462
  161. package/temp/rtk/src/filters/README.md +0 -52
  162. package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
  163. package/temp/rtk/src/filters/basedpyright.toml +0 -47
  164. package/temp/rtk/src/filters/biome.toml +0 -45
  165. package/temp/rtk/src/filters/brew-install.toml +0 -37
  166. package/temp/rtk/src/filters/composer-install.toml +0 -40
  167. package/temp/rtk/src/filters/df.toml +0 -16
  168. package/temp/rtk/src/filters/dotnet-build.toml +0 -64
  169. package/temp/rtk/src/filters/du.toml +0 -16
  170. package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
  171. package/temp/rtk/src/filters/gcc.toml +0 -49
  172. package/temp/rtk/src/filters/gcloud.toml +0 -22
  173. package/temp/rtk/src/filters/hadolint.toml +0 -24
  174. package/temp/rtk/src/filters/helm.toml +0 -29
  175. package/temp/rtk/src/filters/iptables.toml +0 -27
  176. package/temp/rtk/src/filters/jj.toml +0 -28
  177. package/temp/rtk/src/filters/jq.toml +0 -24
  178. package/temp/rtk/src/filters/make.toml +0 -41
  179. package/temp/rtk/src/filters/markdownlint.toml +0 -24
  180. package/temp/rtk/src/filters/mix-compile.toml +0 -27
  181. package/temp/rtk/src/filters/mix-format.toml +0 -15
  182. package/temp/rtk/src/filters/mvn-build.toml +0 -44
  183. package/temp/rtk/src/filters/oxlint.toml +0 -43
  184. package/temp/rtk/src/filters/ping.toml +0 -63
  185. package/temp/rtk/src/filters/pio-run.toml +0 -40
  186. package/temp/rtk/src/filters/poetry-install.toml +0 -50
  187. package/temp/rtk/src/filters/pre-commit.toml +0 -35
  188. package/temp/rtk/src/filters/ps.toml +0 -16
  189. package/temp/rtk/src/filters/quarto-render.toml +0 -41
  190. package/temp/rtk/src/filters/rsync.toml +0 -48
  191. package/temp/rtk/src/filters/shellcheck.toml +0 -27
  192. package/temp/rtk/src/filters/shopify-theme.toml +0 -29
  193. package/temp/rtk/src/filters/skopeo.toml +0 -45
  194. package/temp/rtk/src/filters/sops.toml +0 -16
  195. package/temp/rtk/src/filters/ssh.toml +0 -44
  196. package/temp/rtk/src/filters/stat.toml +0 -34
  197. package/temp/rtk/src/filters/swift-build.toml +0 -41
  198. package/temp/rtk/src/filters/systemctl-status.toml +0 -33
  199. package/temp/rtk/src/filters/terraform-plan.toml +0 -35
  200. package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
  201. package/temp/rtk/src/filters/tofu-init.toml +0 -38
  202. package/temp/rtk/src/filters/tofu-plan.toml +0 -35
  203. package/temp/rtk/src/filters/tofu-validate.toml +0 -17
  204. package/temp/rtk/src/filters/trunk-build.toml +0 -39
  205. package/temp/rtk/src/filters/ty.toml +0 -50
  206. package/temp/rtk/src/filters/uv-sync.toml +0 -37
  207. package/temp/rtk/src/filters/xcodebuild.toml +0 -99
  208. package/temp/rtk/src/filters/yamllint.toml +0 -25
  209. package/temp/rtk/src/find_cmd.rs +0 -598
  210. package/temp/rtk/src/format_cmd.rs +0 -386
  211. package/temp/rtk/src/gain.rs +0 -723
  212. package/temp/rtk/src/gh_cmd.rs +0 -1651
  213. package/temp/rtk/src/git.rs +0 -2012
  214. package/temp/rtk/src/go_cmd.rs +0 -592
  215. package/temp/rtk/src/golangci_cmd.rs +0 -254
  216. package/temp/rtk/src/grep_cmd.rs +0 -288
  217. package/temp/rtk/src/gt_cmd.rs +0 -810
  218. package/temp/rtk/src/hook_audit_cmd.rs +0 -283
  219. package/temp/rtk/src/hook_check.rs +0 -171
  220. package/temp/rtk/src/init.rs +0 -1859
  221. package/temp/rtk/src/integrity.rs +0 -537
  222. package/temp/rtk/src/json_cmd.rs +0 -231
  223. package/temp/rtk/src/learn/detector.rs +0 -628
  224. package/temp/rtk/src/learn/mod.rs +0 -119
  225. package/temp/rtk/src/learn/report.rs +0 -184
  226. package/temp/rtk/src/lint_cmd.rs +0 -694
  227. package/temp/rtk/src/local_llm.rs +0 -316
  228. package/temp/rtk/src/log_cmd.rs +0 -248
  229. package/temp/rtk/src/ls.rs +0 -324
  230. package/temp/rtk/src/main.rs +0 -2482
  231. package/temp/rtk/src/mypy_cmd.rs +0 -389
  232. package/temp/rtk/src/next_cmd.rs +0 -241
  233. package/temp/rtk/src/npm_cmd.rs +0 -236
  234. package/temp/rtk/src/parser/README.md +0 -267
  235. package/temp/rtk/src/parser/error.rs +0 -46
  236. package/temp/rtk/src/parser/formatter.rs +0 -336
  237. package/temp/rtk/src/parser/mod.rs +0 -311
  238. package/temp/rtk/src/parser/types.rs +0 -119
  239. package/temp/rtk/src/pip_cmd.rs +0 -302
  240. package/temp/rtk/src/playwright_cmd.rs +0 -479
  241. package/temp/rtk/src/pnpm_cmd.rs +0 -573
  242. package/temp/rtk/src/prettier_cmd.rs +0 -221
  243. package/temp/rtk/src/prisma_cmd.rs +0 -482
  244. package/temp/rtk/src/psql_cmd.rs +0 -382
  245. package/temp/rtk/src/pytest_cmd.rs +0 -384
  246. package/temp/rtk/src/read.rs +0 -217
  247. package/temp/rtk/src/rewrite_cmd.rs +0 -50
  248. package/temp/rtk/src/ruff_cmd.rs +0 -402
  249. package/temp/rtk/src/runner.rs +0 -271
  250. package/temp/rtk/src/summary.rs +0 -297
  251. package/temp/rtk/src/tee.rs +0 -405
  252. package/temp/rtk/src/telemetry.rs +0 -248
  253. package/temp/rtk/src/toml_filter.rs +0 -1655
  254. package/temp/rtk/src/tracking.rs +0 -1416
  255. package/temp/rtk/src/tree.rs +0 -209
  256. package/temp/rtk/src/tsc_cmd.rs +0 -259
  257. package/temp/rtk/src/utils.rs +0 -432
  258. package/temp/rtk/src/verify_cmd.rs +0 -47
  259. package/temp/rtk/src/vitest_cmd.rs +0 -385
  260. package/temp/rtk/src/wc_cmd.rs +0 -401
  261. package/temp/rtk/src/wget_cmd.rs +0 -260
  262. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
  263. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
  264. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
  265. package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
  266. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
  267. package/tsconfig.json +0 -15
@@ -1,2022 +0,0 @@
1
- use lazy_static::lazy_static;
2
- use regex::{Regex, RegexSet};
3
-
4
- use super::rules::{IGNORED_EXACT, IGNORED_PREFIXES, PATTERNS, RULES};
5
-
6
- /// Result of classifying a command.
7
- #[derive(Debug, PartialEq)]
8
- pub enum Classification {
9
- Supported {
10
- rtk_equivalent: &'static str,
11
- category: &'static str,
12
- estimated_savings_pct: f64,
13
- status: super::report::RtkStatus,
14
- },
15
- Unsupported {
16
- base_command: String,
17
- },
18
- Ignored,
19
- }
20
-
21
- /// Average token counts per category for estimation when no output_len available.
22
- pub fn category_avg_tokens(category: &str, subcmd: &str) -> usize {
23
- match category {
24
- "Git" => match subcmd {
25
- "log" | "diff" | "show" => 200,
26
- _ => 40,
27
- },
28
- "Cargo" => match subcmd {
29
- "test" => 500,
30
- _ => 150,
31
- },
32
- "Tests" => 800,
33
- "Files" => 100,
34
- "Build" => 300,
35
- "Infra" => 120,
36
- "Network" => 150,
37
- "GitHub" => 200,
38
- "PackageManager" => 150,
39
- _ => 150,
40
- }
41
- }
42
-
43
- lazy_static! {
44
- static ref REGEX_SET: RegexSet = RegexSet::new(PATTERNS).expect("invalid regex patterns");
45
- static ref COMPILED: Vec<Regex> = PATTERNS
46
- .iter()
47
- .map(|p| Regex::new(p).expect("invalid regex"))
48
- .collect();
49
- static ref ENV_PREFIX: Regex =
50
- Regex::new(r"^(?:sudo\s+|env\s+|[A-Z_][A-Z0-9_]*=[^\s]*\s+)+").unwrap();
51
- }
52
-
53
- /// Classify a single (already-split) command.
54
- pub fn classify_command(cmd: &str) -> Classification {
55
- let trimmed = cmd.trim();
56
- if trimmed.is_empty() {
57
- return Classification::Ignored;
58
- }
59
-
60
- // Check ignored
61
- for exact in IGNORED_EXACT {
62
- if trimmed == *exact {
63
- return Classification::Ignored;
64
- }
65
- }
66
- for prefix in IGNORED_PREFIXES {
67
- if trimmed.starts_with(prefix) {
68
- return Classification::Ignored;
69
- }
70
- }
71
-
72
- // Strip env prefixes (sudo, env VAR=val, VAR=val)
73
- let stripped = ENV_PREFIX.replace(trimmed, "");
74
- let cmd_clean = stripped.trim();
75
- if cmd_clean.is_empty() {
76
- return Classification::Ignored;
77
- }
78
-
79
- // Exclude cat/head/tail with redirect operators — these are writes, not reads (#315)
80
- if cmd_clean.starts_with("cat ")
81
- || cmd_clean.starts_with("head ")
82
- || cmd_clean.starts_with("tail ")
83
- {
84
- let has_redirect = cmd_clean
85
- .split_whitespace()
86
- .skip(1)
87
- .any(|t| t.starts_with('>') || t == "<" || t.starts_with(">>"));
88
- if has_redirect {
89
- return Classification::Unsupported {
90
- base_command: cmd_clean
91
- .split_whitespace()
92
- .next()
93
- .unwrap_or("cat")
94
- .to_string(),
95
- };
96
- }
97
- }
98
-
99
- // Fast check with RegexSet — take the last (most specific) match
100
- let matches: Vec<usize> = REGEX_SET.matches(cmd_clean).into_iter().collect();
101
- if let Some(&idx) = matches.last() {
102
- let rule = &RULES[idx];
103
-
104
- // Extract subcommand for savings override and status detection
105
- let (savings, status) = if let Some(caps) = COMPILED[idx].captures(cmd_clean) {
106
- if let Some(sub) = caps.get(1) {
107
- let subcmd = sub.as_str();
108
- // Check if this subcommand has a special status
109
- let status = rule
110
- .subcmd_status
111
- .iter()
112
- .find(|(s, _)| *s == subcmd)
113
- .map(|(_, st)| *st)
114
- .unwrap_or(super::report::RtkStatus::Existing);
115
-
116
- // Check if this subcommand has custom savings
117
- let savings = rule
118
- .subcmd_savings
119
- .iter()
120
- .find(|(s, _)| *s == subcmd)
121
- .map(|(_, pct)| *pct)
122
- .unwrap_or(rule.savings_pct);
123
-
124
- (savings, status)
125
- } else {
126
- (rule.savings_pct, super::report::RtkStatus::Existing)
127
- }
128
- } else {
129
- (rule.savings_pct, super::report::RtkStatus::Existing)
130
- };
131
-
132
- Classification::Supported {
133
- rtk_equivalent: rule.rtk_cmd,
134
- category: rule.category,
135
- estimated_savings_pct: savings,
136
- status,
137
- }
138
- } else {
139
- // Extract base command for unsupported
140
- let base = extract_base_command(cmd_clean);
141
- if base.is_empty() {
142
- Classification::Ignored
143
- } else {
144
- Classification::Unsupported {
145
- base_command: base.to_string(),
146
- }
147
- }
148
- }
149
- }
150
-
151
- /// Extract the base command (first word, or first two if it looks like a subcommand pattern).
152
- fn extract_base_command(cmd: &str) -> &str {
153
- let parts: Vec<&str> = cmd.splitn(3, char::is_whitespace).collect();
154
- match parts.len() {
155
- 0 => "",
156
- 1 => parts[0],
157
- _ => {
158
- let second = parts[1];
159
- // If the second token looks like a subcommand (no leading -)
160
- if !second.starts_with('-') && !second.contains('/') && !second.contains('.') {
161
- // Return "cmd subcmd"
162
- let end = cmd
163
- .find(char::is_whitespace)
164
- .and_then(|i| {
165
- let rest = &cmd[i..];
166
- let trimmed = rest.trim_start();
167
- trimmed
168
- .find(char::is_whitespace)
169
- .map(|j| i + (rest.len() - trimmed.len()) + j)
170
- })
171
- .unwrap_or(cmd.len());
172
- &cmd[..end]
173
- } else {
174
- parts[0]
175
- }
176
- }
177
- }
178
- }
179
-
180
- /// Split a command chain on `&&`, `||`, `;` outside quotes.
181
- /// For pipes `|`, only keep the first command.
182
- /// Lines with `<<` (heredoc) or `$((` are returned whole.
183
- pub fn split_command_chain(cmd: &str) -> Vec<&str> {
184
- let trimmed = cmd.trim();
185
- if trimmed.is_empty() {
186
- return vec![];
187
- }
188
-
189
- // Heredoc or arithmetic expansion: treat as single command
190
- if trimmed.contains("<<") || trimmed.contains("$((") {
191
- return vec![trimmed];
192
- }
193
-
194
- let mut results = Vec::new();
195
- let mut start = 0;
196
- let bytes = trimmed.as_bytes();
197
- let len = bytes.len();
198
- let mut i = 0;
199
- let mut in_single = false;
200
- let mut in_double = false;
201
- let mut pipe_seen = false;
202
-
203
- while i < len {
204
- let b = bytes[i];
205
- match b {
206
- b'\'' if !in_double => {
207
- in_single = !in_single;
208
- i += 1;
209
- }
210
- b'"' if !in_single => {
211
- in_double = !in_double;
212
- i += 1;
213
- }
214
- b'|' if !in_single && !in_double => {
215
- if i + 1 < len && bytes[i + 1] == b'|' {
216
- // ||
217
- let segment = trimmed[start..i].trim();
218
- if !segment.is_empty() {
219
- results.push(segment);
220
- }
221
- i += 2;
222
- start = i;
223
- } else {
224
- // pipe: keep only first command
225
- let segment = trimmed[start..i].trim();
226
- if !segment.is_empty() {
227
- results.push(segment);
228
- }
229
- pipe_seen = true;
230
- break;
231
- }
232
- }
233
- b'&' if !in_single && !in_double && i + 1 < len && bytes[i + 1] == b'&' => {
234
- let segment = trimmed[start..i].trim();
235
- if !segment.is_empty() {
236
- results.push(segment);
237
- }
238
- i += 2;
239
- start = i;
240
- }
241
- b';' if !in_single && !in_double => {
242
- let segment = trimmed[start..i].trim();
243
- if !segment.is_empty() {
244
- results.push(segment);
245
- }
246
- i += 1;
247
- start = i;
248
- }
249
- _ => {
250
- i += 1;
251
- }
252
- }
253
- }
254
-
255
- if !pipe_seen && start < len {
256
- let segment = trimmed[start..].trim();
257
- if !segment.is_empty() {
258
- results.push(segment);
259
- }
260
- }
261
-
262
- results
263
- }
264
-
265
- /// Check if a command has RTK_DISABLED= prefix in its env prefix portion.
266
- pub fn has_rtk_disabled_prefix(cmd: &str) -> bool {
267
- let trimmed = cmd.trim();
268
- let stripped = ENV_PREFIX.replace(trimmed, "");
269
- let prefix_len = trimmed.len() - stripped.len();
270
- let prefix_part = &trimmed[..prefix_len];
271
- prefix_part.contains("RTK_DISABLED=")
272
- }
273
-
274
- /// Strip RTK_DISABLED=X and other env prefixes, return the actual command.
275
- pub fn strip_disabled_prefix(cmd: &str) -> &str {
276
- let trimmed = cmd.trim();
277
- let stripped = ENV_PREFIX.replace(trimmed, "");
278
- // stripped is a Cow<str> that borrows from trimmed when no replacement happens.
279
- // We need to return a &str into the original, so compute the offset.
280
- let prefix_len = trimmed.len() - stripped.len();
281
- trimmed[prefix_len..].trim_start()
282
- }
283
-
284
- /// Rewrite a raw command to its RTK equivalent.
285
- ///
286
- /// Returns `Some(rewritten)` if the command has an RTK equivalent or is already RTK.
287
- /// Returns `None` if the command is unsupported or ignored (hook should pass through).
288
- ///
289
- /// Handles compound commands (`&&`, `||`, `;`) by rewriting each segment independently.
290
- /// For pipes (`|`), only rewrites the first command (the filter stays raw).
291
- pub fn rewrite_command(cmd: &str, excluded: &[String]) -> Option<String> {
292
- let trimmed = cmd.trim();
293
- if trimmed.is_empty() {
294
- return None;
295
- }
296
-
297
- // Heredoc or arithmetic expansion — unsafe to split/rewrite
298
- if trimmed.contains("<<") || trimmed.contains("$((") {
299
- return None;
300
- }
301
-
302
- // Simple (non-compound) already-RTK command — return as-is.
303
- // For compound commands that start with "rtk" (e.g. "rtk git add . && cargo test"),
304
- // fall through to rewrite_compound so the remaining segments get rewritten.
305
- let has_compound = trimmed.contains("&&")
306
- || trimmed.contains("||")
307
- || trimmed.contains(';')
308
- || trimmed.contains('|')
309
- || trimmed.contains(" & ");
310
- if !has_compound && (trimmed.starts_with("rtk ") || trimmed == "rtk") {
311
- return Some(trimmed.to_string());
312
- }
313
-
314
- rewrite_compound(trimmed, excluded)
315
- }
316
-
317
- /// Rewrite a compound command (with `&&`, `||`, `;`, `|`) by rewriting each segment.
318
- fn rewrite_compound(cmd: &str, excluded: &[String]) -> Option<String> {
319
- let bytes = cmd.as_bytes();
320
- let len = bytes.len();
321
- let mut result = String::with_capacity(len + 32);
322
- let mut any_changed = false;
323
- let mut seg_start = 0;
324
- let mut i = 0;
325
- let mut in_single = false;
326
- let mut in_double = false;
327
-
328
- while i < len {
329
- let b = bytes[i];
330
- match b {
331
- b'\'' if !in_double => {
332
- in_single = !in_single;
333
- i += 1;
334
- }
335
- b'"' if !in_single => {
336
- in_double = !in_double;
337
- i += 1;
338
- }
339
- b'|' if !in_single && !in_double => {
340
- if i + 1 < len && bytes[i + 1] == b'|' {
341
- // `||` operator — rewrite left, continue
342
- let seg = cmd[seg_start..i].trim();
343
- let rewritten =
344
- rewrite_segment(seg, excluded).unwrap_or_else(|| seg.to_string());
345
- if rewritten != seg {
346
- any_changed = true;
347
- }
348
- result.push_str(&rewritten);
349
- result.push_str(" || ");
350
- i += 2;
351
- while i < len && bytes[i] == b' ' {
352
- i += 1;
353
- }
354
- seg_start = i;
355
- } else {
356
- // `|` pipe — rewrite first segment only, pass through the rest unchanged
357
- let seg = cmd[seg_start..i].trim();
358
- let rewritten =
359
- rewrite_segment(seg, excluded).unwrap_or_else(|| seg.to_string());
360
- if rewritten != seg {
361
- any_changed = true;
362
- }
363
- result.push_str(&rewritten);
364
- // Preserve the space before the pipe that was lost by trim()
365
- result.push(' ');
366
- result.push_str(cmd[i..].trim_start());
367
- return if any_changed { Some(result) } else { None };
368
- }
369
- }
370
- b'&' if !in_single && !in_double && i + 1 < len && bytes[i + 1] == b'&' => {
371
- // `&&` operator — rewrite left, continue
372
- let seg = cmd[seg_start..i].trim();
373
- let rewritten = rewrite_segment(seg, excluded).unwrap_or_else(|| seg.to_string());
374
- if rewritten != seg {
375
- any_changed = true;
376
- }
377
- result.push_str(&rewritten);
378
- result.push_str(" && ");
379
- i += 2;
380
- while i < len && bytes[i] == b' ' {
381
- i += 1;
382
- }
383
- seg_start = i;
384
- }
385
- b'&' if !in_single && !in_double => {
386
- // #346: redirect detection — 2>&1 / >&2 (> before &) or &>file / &>>file (> after &)
387
- let is_redirect =
388
- (i > 0 && bytes[i - 1] == b'>') || (i + 1 < len && bytes[i + 1] == b'>');
389
- if is_redirect {
390
- i += 1;
391
- } else {
392
- // single `&` background execution operator
393
- let seg = cmd[seg_start..i].trim();
394
- let rewritten =
395
- rewrite_segment(seg, excluded).unwrap_or_else(|| seg.to_string());
396
- if rewritten != seg {
397
- any_changed = true;
398
- }
399
- result.push_str(&rewritten);
400
- result.push_str(" & ");
401
- i += 1;
402
- while i < len && bytes[i] == b' ' {
403
- i += 1;
404
- }
405
- seg_start = i;
406
- }
407
- }
408
- b';' if !in_single && !in_double => {
409
- // `;` separator
410
- let seg = cmd[seg_start..i].trim();
411
- let rewritten = rewrite_segment(seg, excluded).unwrap_or_else(|| seg.to_string());
412
- if rewritten != seg {
413
- any_changed = true;
414
- }
415
- result.push_str(&rewritten);
416
- result.push(';');
417
- i += 1;
418
- while i < len && bytes[i] == b' ' {
419
- i += 1;
420
- }
421
- if i < len {
422
- result.push(' ');
423
- }
424
- seg_start = i;
425
- }
426
- _ => {
427
- i += 1;
428
- }
429
- }
430
- }
431
-
432
- // Last (or only) segment
433
- let seg = cmd[seg_start..len].trim();
434
- let rewritten = rewrite_segment(seg, excluded).unwrap_or_else(|| seg.to_string());
435
- if rewritten != seg {
436
- any_changed = true;
437
- }
438
- result.push_str(&rewritten);
439
-
440
- if any_changed {
441
- Some(result)
442
- } else {
443
- None
444
- }
445
- }
446
-
447
- /// Rewrite `head -N file` → `rtk read file --max-lines N`.
448
- /// Returns `None` if the command doesn't match this pattern (fall through to generic logic).
449
- fn rewrite_head_numeric(cmd: &str) -> Option<String> {
450
- // Match: head -<digits> <file> (with optional env prefix)
451
- lazy_static! {
452
- static ref HEAD_N: Regex = Regex::new(r"^head\s+-(\d+)\s+(.+)$").expect("valid regex");
453
- static ref HEAD_LINES: Regex =
454
- Regex::new(r"^head\s+--lines=(\d+)\s+(.+)$").expect("valid regex");
455
- }
456
- if let Some(caps) = HEAD_N.captures(cmd) {
457
- let n = caps.get(1)?.as_str();
458
- let file = caps.get(2)?.as_str();
459
- return Some(format!("rtk read {} --max-lines {}", file, n));
460
- }
461
- if let Some(caps) = HEAD_LINES.captures(cmd) {
462
- let n = caps.get(1)?.as_str();
463
- let file = caps.get(2)?.as_str();
464
- return Some(format!("rtk read {} --max-lines {}", file, n));
465
- }
466
- // head with any other flag (e.g. -c, -q): skip rewriting to avoid clap errors
467
- if cmd.starts_with("head -") {
468
- return None;
469
- }
470
- None
471
- }
472
-
473
- /// Rewrite `tail` numeric line forms to `rtk read ... --tail-lines N`.
474
- /// Returns `None` when the pattern is unsupported (caller falls through / skips rewrite).
475
- fn rewrite_tail_lines(cmd: &str) -> Option<String> {
476
- lazy_static! {
477
- static ref TAIL_N: Regex = Regex::new(r"^tail\s+-(\d+)\s+(.+)$").expect("valid regex");
478
- static ref TAIL_N_SPACE: Regex =
479
- Regex::new(r"^tail\s+-n\s+(\d+)\s+(.+)$").expect("valid regex");
480
- static ref TAIL_LINES_EQ: Regex =
481
- Regex::new(r"^tail\s+--lines=(\d+)\s+(.+)$").expect("valid regex");
482
- static ref TAIL_LINES_SPACE: Regex =
483
- Regex::new(r"^tail\s+--lines\s+(\d+)\s+(.+)$").expect("valid regex");
484
- }
485
-
486
- for re in [
487
- &*TAIL_N,
488
- &*TAIL_N_SPACE,
489
- &*TAIL_LINES_EQ,
490
- &*TAIL_LINES_SPACE,
491
- ] {
492
- if let Some(caps) = re.captures(cmd) {
493
- let n = caps.get(1)?.as_str();
494
- let file = caps.get(2)?.as_str();
495
- return Some(format!("rtk read {} --tail-lines {}", file, n));
496
- }
497
- }
498
-
499
- // Unknown tail form: skip rewrite to preserve native behavior.
500
- None
501
- }
502
-
503
- /// Rewrite a single (non-compound) command segment.
504
- /// Returns `Some(rewritten)` if matched (including already-RTK pass-through).
505
- /// Returns `None` if no match (caller uses original segment).
506
- fn rewrite_segment(seg: &str, excluded: &[String]) -> Option<String> {
507
- let trimmed = seg.trim();
508
- if trimmed.is_empty() {
509
- return None;
510
- }
511
-
512
- // Already RTK — pass through unchanged
513
- if trimmed.starts_with("rtk ") || trimmed == "rtk" {
514
- return Some(trimmed.to_string());
515
- }
516
-
517
- // Special case: `head -N file` / `head --lines=N file` → `rtk read file --max-lines N`
518
- // Must intercept before generic prefix replacement, which would produce `rtk read -20 file`.
519
- // Only intercept when head has a flag (-N, --lines=N, -c, etc.); plain `head file` falls
520
- // through to the generic rewrite below and produces `rtk read file` as expected.
521
- if trimmed.starts_with("head -") {
522
- return rewrite_head_numeric(trimmed);
523
- }
524
-
525
- // tail has several forms that are not compatible with generic prefix replacement.
526
- // Only rewrite recognized numeric line forms; otherwise skip rewrite.
527
- if trimmed.starts_with("tail ") {
528
- return rewrite_tail_lines(trimmed);
529
- }
530
-
531
- // Use classify_command for correct ignore/prefix handling
532
- let rtk_equivalent = match classify_command(trimmed) {
533
- Classification::Supported { rtk_equivalent, .. } => {
534
- // Check if the base command is excluded from rewriting (#243)
535
- let base = trimmed.split_whitespace().next().unwrap_or("");
536
- if excluded.iter().any(|e| e == base) {
537
- return None;
538
- }
539
- rtk_equivalent
540
- }
541
- _ => return None,
542
- };
543
-
544
- // Find the matching rule (rtk_cmd values are unique across all rules)
545
- let rule = RULES.iter().find(|r| r.rtk_cmd == rtk_equivalent)?;
546
-
547
- // Extract env prefix (sudo, env VAR=val, etc.)
548
- let stripped_cow = ENV_PREFIX.replace(trimmed, "");
549
- let env_prefix_len = trimmed.len() - stripped_cow.len();
550
- let env_prefix = &trimmed[..env_prefix_len];
551
- let cmd_clean = stripped_cow.trim();
552
-
553
- // #345: RTK_DISABLED=1 in env prefix → skip rewrite entirely
554
- if has_rtk_disabled_prefix(trimmed) {
555
- return None;
556
- }
557
-
558
- // #196: gh with --json/--jq/--template produces structured output that
559
- // rtk gh would corrupt — skip rewrite so the caller gets raw JSON.
560
- if rule.rtk_cmd == "rtk gh" {
561
- let args_lower = cmd_clean.to_lowercase();
562
- if args_lower.contains("--json")
563
- || args_lower.contains("--jq")
564
- || args_lower.contains("--template")
565
- {
566
- return None;
567
- }
568
- }
569
-
570
- // Try each rewrite prefix (longest first) with word-boundary check
571
- for &prefix in rule.rewrite_prefixes {
572
- if let Some(rest) = strip_word_prefix(cmd_clean, prefix) {
573
- let rewritten = if rest.is_empty() {
574
- format!("{}{}", env_prefix, rule.rtk_cmd)
575
- } else {
576
- format!("{}{} {}", env_prefix, rule.rtk_cmd, rest)
577
- };
578
- return Some(rewritten);
579
- }
580
- }
581
-
582
- None
583
- }
584
-
585
- /// Strip a command prefix with word-boundary check.
586
- /// Returns the remainder of the command after the prefix, or `None` if no match.
587
- fn strip_word_prefix<'a>(cmd: &'a str, prefix: &str) -> Option<&'a str> {
588
- if cmd == prefix {
589
- Some("")
590
- } else if cmd.len() > prefix.len()
591
- && cmd.starts_with(prefix)
592
- && cmd.as_bytes()[prefix.len()] == b' '
593
- {
594
- Some(cmd[prefix.len() + 1..].trim_start())
595
- } else {
596
- None
597
- }
598
- }
599
-
600
- #[cfg(test)]
601
- mod tests {
602
- use super::super::report::RtkStatus;
603
- use super::*;
604
-
605
- #[test]
606
- fn test_classify_git_status() {
607
- assert_eq!(
608
- classify_command("git status"),
609
- Classification::Supported {
610
- rtk_equivalent: "rtk git",
611
- category: "Git",
612
- estimated_savings_pct: 70.0,
613
- status: RtkStatus::Existing,
614
- }
615
- );
616
- }
617
-
618
- #[test]
619
- fn test_classify_git_diff_cached() {
620
- assert_eq!(
621
- classify_command("git diff --cached"),
622
- Classification::Supported {
623
- rtk_equivalent: "rtk git",
624
- category: "Git",
625
- estimated_savings_pct: 80.0,
626
- status: RtkStatus::Existing,
627
- }
628
- );
629
- }
630
-
631
- #[test]
632
- fn test_classify_cargo_test_filter() {
633
- assert_eq!(
634
- classify_command("cargo test filter::"),
635
- Classification::Supported {
636
- rtk_equivalent: "rtk cargo",
637
- category: "Cargo",
638
- estimated_savings_pct: 90.0,
639
- status: RtkStatus::Existing,
640
- }
641
- );
642
- }
643
-
644
- #[test]
645
- fn test_classify_npx_tsc() {
646
- assert_eq!(
647
- classify_command("npx tsc --noEmit"),
648
- Classification::Supported {
649
- rtk_equivalent: "rtk tsc",
650
- category: "Build",
651
- estimated_savings_pct: 83.0,
652
- status: RtkStatus::Existing,
653
- }
654
- );
655
- }
656
-
657
- #[test]
658
- fn test_classify_cat_file() {
659
- assert_eq!(
660
- classify_command("cat src/main.rs"),
661
- Classification::Supported {
662
- rtk_equivalent: "rtk read",
663
- category: "Files",
664
- estimated_savings_pct: 60.0,
665
- status: RtkStatus::Existing,
666
- }
667
- );
668
- }
669
-
670
- #[test]
671
- fn test_classify_cat_redirect_not_supported() {
672
- // cat > file and cat >> file are writes, not reads — should not be classified as supported
673
- let write_commands = [
674
- "cat > /tmp/output.txt",
675
- "cat >> /tmp/output.txt",
676
- "cat file.txt > output.txt",
677
- "cat -n file.txt >> log.txt",
678
- "head -10 README.md > output.txt",
679
- "tail -f app.log > /dev/null",
680
- ];
681
- for cmd in &write_commands {
682
- match classify_command(cmd) {
683
- Classification::Supported { .. } => {
684
- panic!("{} should NOT be classified as Supported", cmd)
685
- }
686
- _ => {} // Unsupported or Ignored is fine
687
- }
688
- }
689
- }
690
-
691
- #[test]
692
- fn test_classify_cd_ignored() {
693
- assert_eq!(classify_command("cd /tmp"), Classification::Ignored);
694
- }
695
-
696
- #[test]
697
- fn test_classify_rtk_already() {
698
- assert_eq!(classify_command("rtk git status"), Classification::Ignored);
699
- }
700
-
701
- #[test]
702
- fn test_classify_echo_ignored() {
703
- assert_eq!(
704
- classify_command("echo hello world"),
705
- Classification::Ignored
706
- );
707
- }
708
-
709
- #[test]
710
- fn test_classify_htop_unsupported() {
711
- match classify_command("htop -d 10") {
712
- Classification::Unsupported { base_command } => {
713
- assert_eq!(base_command, "htop");
714
- }
715
- other => panic!("expected Unsupported, got {:?}", other),
716
- }
717
- }
718
-
719
- #[test]
720
- fn test_classify_env_prefix_stripped() {
721
- assert_eq!(
722
- classify_command("GIT_SSH_COMMAND=ssh git push"),
723
- Classification::Supported {
724
- rtk_equivalent: "rtk git",
725
- category: "Git",
726
- estimated_savings_pct: 70.0,
727
- status: RtkStatus::Existing,
728
- }
729
- );
730
- }
731
-
732
- #[test]
733
- fn test_classify_sudo_stripped() {
734
- assert_eq!(
735
- classify_command("sudo docker ps"),
736
- Classification::Supported {
737
- rtk_equivalent: "rtk docker",
738
- category: "Infra",
739
- estimated_savings_pct: 85.0,
740
- status: RtkStatus::Existing,
741
- }
742
- );
743
- }
744
-
745
- #[test]
746
- fn test_classify_cargo_check() {
747
- assert_eq!(
748
- classify_command("cargo check"),
749
- Classification::Supported {
750
- rtk_equivalent: "rtk cargo",
751
- category: "Cargo",
752
- estimated_savings_pct: 80.0,
753
- status: RtkStatus::Existing,
754
- }
755
- );
756
- }
757
-
758
- #[test]
759
- fn test_classify_cargo_check_all_targets() {
760
- assert_eq!(
761
- classify_command("cargo check --all-targets"),
762
- Classification::Supported {
763
- rtk_equivalent: "rtk cargo",
764
- category: "Cargo",
765
- estimated_savings_pct: 80.0,
766
- status: RtkStatus::Existing,
767
- }
768
- );
769
- }
770
-
771
- #[test]
772
- fn test_classify_cargo_fmt_passthrough() {
773
- assert_eq!(
774
- classify_command("cargo fmt"),
775
- Classification::Supported {
776
- rtk_equivalent: "rtk cargo",
777
- category: "Cargo",
778
- estimated_savings_pct: 80.0,
779
- status: RtkStatus::Passthrough,
780
- }
781
- );
782
- }
783
-
784
- #[test]
785
- fn test_classify_cargo_clippy_savings() {
786
- assert_eq!(
787
- classify_command("cargo clippy --all-targets"),
788
- Classification::Supported {
789
- rtk_equivalent: "rtk cargo",
790
- category: "Cargo",
791
- estimated_savings_pct: 80.0,
792
- status: RtkStatus::Existing,
793
- }
794
- );
795
- }
796
-
797
- #[test]
798
- fn test_patterns_rules_length_match() {
799
- assert_eq!(
800
- PATTERNS.len(),
801
- RULES.len(),
802
- "PATTERNS and RULES must be aligned"
803
- );
804
- }
805
-
806
- #[test]
807
- fn test_registry_covers_all_cargo_subcommands() {
808
- // Verify that every CargoCommand variant (Build, Test, Clippy, Check, Fmt)
809
- // except Other has a matching pattern in the registry
810
- for subcmd in ["build", "test", "clippy", "check", "fmt"] {
811
- let cmd = format!("cargo {subcmd}");
812
- match classify_command(&cmd) {
813
- Classification::Supported { .. } => {}
814
- other => panic!("cargo {subcmd} should be Supported, got {other:?}"),
815
- }
816
- }
817
- }
818
-
819
- #[test]
820
- fn test_registry_covers_all_git_subcommands() {
821
- // Verify that every GitCommand subcommand has a matching pattern
822
- for subcmd in [
823
- "status", "log", "diff", "show", "add", "commit", "push", "pull", "branch", "fetch",
824
- "stash", "worktree",
825
- ] {
826
- let cmd = format!("git {subcmd}");
827
- match classify_command(&cmd) {
828
- Classification::Supported { .. } => {}
829
- other => panic!("git {subcmd} should be Supported, got {other:?}"),
830
- }
831
- }
832
- }
833
-
834
- #[test]
835
- fn test_classify_find_not_blocked_by_fi() {
836
- // Regression: "fi" in IGNORED_PREFIXES used to shadow "find" commands
837
- // because "find".starts_with("fi") is true. "fi" should only match exactly.
838
- assert_eq!(
839
- classify_command("find . -name foo"),
840
- Classification::Supported {
841
- rtk_equivalent: "rtk find",
842
- category: "Files",
843
- estimated_savings_pct: 70.0,
844
- status: RtkStatus::Existing,
845
- }
846
- );
847
- }
848
-
849
- #[test]
850
- fn test_fi_still_ignored_exact() {
851
- // Bare "fi" (shell keyword) should still be ignored
852
- assert_eq!(classify_command("fi"), Classification::Ignored);
853
- }
854
-
855
- #[test]
856
- fn test_done_still_ignored_exact() {
857
- // Bare "done" (shell keyword) should still be ignored
858
- assert_eq!(classify_command("done"), Classification::Ignored);
859
- }
860
-
861
- #[test]
862
- fn test_split_chain_and() {
863
- assert_eq!(split_command_chain("a && b"), vec!["a", "b"]);
864
- }
865
-
866
- #[test]
867
- fn test_split_chain_semicolon() {
868
- assert_eq!(split_command_chain("a ; b"), vec!["a", "b"]);
869
- }
870
-
871
- #[test]
872
- fn test_split_pipe_first_only() {
873
- assert_eq!(split_command_chain("a | b"), vec!["a"]);
874
- }
875
-
876
- #[test]
877
- fn test_split_single() {
878
- assert_eq!(split_command_chain("git status"), vec!["git status"]);
879
- }
880
-
881
- #[test]
882
- fn test_split_quoted_and() {
883
- assert_eq!(
884
- split_command_chain(r#"echo "a && b""#),
885
- vec![r#"echo "a && b""#]
886
- );
887
- }
888
-
889
- #[test]
890
- fn test_split_heredoc_no_split() {
891
- let cmd = "cat <<'EOF'\nhello && world\nEOF";
892
- assert_eq!(split_command_chain(cmd), vec![cmd]);
893
- }
894
-
895
- #[test]
896
- fn test_classify_mypy() {
897
- assert_eq!(
898
- classify_command("mypy src/"),
899
- Classification::Supported {
900
- rtk_equivalent: "rtk mypy",
901
- category: "Build",
902
- estimated_savings_pct: 80.0,
903
- status: RtkStatus::Existing,
904
- }
905
- );
906
- }
907
-
908
- #[test]
909
- fn test_classify_python_m_mypy() {
910
- assert_eq!(
911
- classify_command("python3 -m mypy --strict"),
912
- Classification::Supported {
913
- rtk_equivalent: "rtk mypy",
914
- category: "Build",
915
- estimated_savings_pct: 80.0,
916
- status: RtkStatus::Existing,
917
- }
918
- );
919
- }
920
-
921
- // --- rewrite_command tests ---
922
-
923
- #[test]
924
- fn test_rewrite_git_status() {
925
- assert_eq!(
926
- rewrite_command("git status", &[]),
927
- Some("rtk git status".into())
928
- );
929
- }
930
-
931
- #[test]
932
- fn test_rewrite_git_log() {
933
- assert_eq!(
934
- rewrite_command("git log -10", &[]),
935
- Some("rtk git log -10".into())
936
- );
937
- }
938
-
939
- #[test]
940
- fn test_rewrite_cargo_test() {
941
- assert_eq!(
942
- rewrite_command("cargo test", &[]),
943
- Some("rtk cargo test".into())
944
- );
945
- }
946
-
947
- #[test]
948
- fn test_rewrite_compound_and() {
949
- assert_eq!(
950
- rewrite_command("git add . && cargo test", &[]),
951
- Some("rtk git add . && rtk cargo test".into())
952
- );
953
- }
954
-
955
- #[test]
956
- fn test_rewrite_compound_three_segments() {
957
- assert_eq!(
958
- rewrite_command(
959
- "cargo fmt --all && cargo clippy --all-targets && cargo test",
960
- &[]
961
- ),
962
- Some("rtk cargo fmt --all && rtk cargo clippy --all-targets && rtk cargo test".into())
963
- );
964
- }
965
-
966
- #[test]
967
- fn test_rewrite_already_rtk() {
968
- assert_eq!(
969
- rewrite_command("rtk git status", &[]),
970
- Some("rtk git status".into())
971
- );
972
- }
973
-
974
- #[test]
975
- fn test_rewrite_background_single_amp() {
976
- assert_eq!(
977
- rewrite_command("cargo test & git status", &[]),
978
- Some("rtk cargo test & rtk git status".into())
979
- );
980
- }
981
-
982
- #[test]
983
- fn test_rewrite_background_unsupported_right() {
984
- assert_eq!(
985
- rewrite_command("cargo test & htop", &[]),
986
- Some("rtk cargo test & htop".into())
987
- );
988
- }
989
-
990
- #[test]
991
- fn test_rewrite_background_does_not_affect_double_amp() {
992
- // `&&` must still work after adding `&` support
993
- assert_eq!(
994
- rewrite_command("cargo test && git status", &[]),
995
- Some("rtk cargo test && rtk git status".into())
996
- );
997
- }
998
-
999
- #[test]
1000
- fn test_rewrite_unsupported_returns_none() {
1001
- assert_eq!(rewrite_command("htop", &[]), None);
1002
- }
1003
-
1004
- #[test]
1005
- fn test_rewrite_ignored_cd() {
1006
- assert_eq!(rewrite_command("cd /tmp", &[]), None);
1007
- }
1008
-
1009
- #[test]
1010
- fn test_rewrite_with_env_prefix() {
1011
- assert_eq!(
1012
- rewrite_command("GIT_SSH_COMMAND=ssh git push", &[]),
1013
- Some("GIT_SSH_COMMAND=ssh rtk git push".into())
1014
- );
1015
- }
1016
-
1017
- #[test]
1018
- fn test_rewrite_npx_tsc() {
1019
- assert_eq!(
1020
- rewrite_command("npx tsc --noEmit", &[]),
1021
- Some("rtk tsc --noEmit".into())
1022
- );
1023
- }
1024
-
1025
- #[test]
1026
- fn test_rewrite_pnpm_tsc() {
1027
- assert_eq!(
1028
- rewrite_command("pnpm tsc --noEmit", &[]),
1029
- Some("rtk tsc --noEmit".into())
1030
- );
1031
- }
1032
-
1033
- #[test]
1034
- fn test_rewrite_cat_file() {
1035
- assert_eq!(
1036
- rewrite_command("cat src/main.rs", &[]),
1037
- Some("rtk read src/main.rs".into())
1038
- );
1039
- }
1040
-
1041
- #[test]
1042
- fn test_rewrite_rg_pattern() {
1043
- assert_eq!(
1044
- rewrite_command("rg \"fn main\"", &[]),
1045
- Some("rtk grep \"fn main\"".into())
1046
- );
1047
- }
1048
-
1049
- #[test]
1050
- fn test_rewrite_npx_playwright() {
1051
- assert_eq!(
1052
- rewrite_command("npx playwright test", &[]),
1053
- Some("rtk playwright test".into())
1054
- );
1055
- }
1056
-
1057
- #[test]
1058
- fn test_rewrite_next_build() {
1059
- assert_eq!(
1060
- rewrite_command("next build --turbo", &[]),
1061
- Some("rtk next --turbo".into())
1062
- );
1063
- }
1064
-
1065
- #[test]
1066
- fn test_rewrite_pipe_first_only() {
1067
- // After a pipe, the filter command stays raw
1068
- assert_eq!(
1069
- rewrite_command("git log -10 | grep feat", &[]),
1070
- Some("rtk git log -10 | grep feat".into())
1071
- );
1072
- }
1073
-
1074
- #[test]
1075
- fn test_rewrite_heredoc_returns_none() {
1076
- assert_eq!(rewrite_command("cat <<'EOF'\nfoo\nEOF", &[]), None);
1077
- }
1078
-
1079
- #[test]
1080
- fn test_rewrite_empty_returns_none() {
1081
- assert_eq!(rewrite_command("", &[]), None);
1082
- assert_eq!(rewrite_command(" ", &[]), None);
1083
- }
1084
-
1085
- #[test]
1086
- fn test_rewrite_mixed_compound_partial() {
1087
- // First segment already RTK, second gets rewritten
1088
- assert_eq!(
1089
- rewrite_command("rtk git add . && cargo test", &[]),
1090
- Some("rtk git add . && rtk cargo test".into())
1091
- );
1092
- }
1093
-
1094
- // --- #345: RTK_DISABLED ---
1095
-
1096
- #[test]
1097
- fn test_rewrite_rtk_disabled_curl() {
1098
- assert_eq!(
1099
- rewrite_command("RTK_DISABLED=1 curl https://example.com", &[]),
1100
- None
1101
- );
1102
- }
1103
-
1104
- #[test]
1105
- fn test_rewrite_rtk_disabled_git_status() {
1106
- assert_eq!(rewrite_command("RTK_DISABLED=1 git status", &[]), None);
1107
- }
1108
-
1109
- #[test]
1110
- fn test_rewrite_rtk_disabled_multi_env() {
1111
- assert_eq!(
1112
- rewrite_command("FOO=1 RTK_DISABLED=1 git status", &[]),
1113
- None
1114
- );
1115
- }
1116
-
1117
- #[test]
1118
- fn test_rewrite_non_rtk_disabled_env_still_rewrites() {
1119
- assert_eq!(
1120
- rewrite_command("SOME_VAR=1 git status", &[]),
1121
- Some("SOME_VAR=1 rtk git status".into())
1122
- );
1123
- }
1124
-
1125
- // --- #346: 2>&1 and &> redirect detection ---
1126
-
1127
- #[test]
1128
- fn test_rewrite_redirect_2_gt_amp_1_with_pipe() {
1129
- assert_eq!(
1130
- rewrite_command("cargo test 2>&1 | head", &[]),
1131
- Some("rtk cargo test 2>&1 | head".into())
1132
- );
1133
- }
1134
-
1135
- #[test]
1136
- fn test_rewrite_redirect_2_gt_amp_1_trailing() {
1137
- assert_eq!(
1138
- rewrite_command("cargo test 2>&1", &[]),
1139
- Some("rtk cargo test 2>&1".into())
1140
- );
1141
- }
1142
-
1143
- #[test]
1144
- fn test_rewrite_redirect_plain_2_devnull() {
1145
- // 2>/dev/null has no `&`, never broken — non-regression
1146
- assert_eq!(
1147
- rewrite_command("git status 2>/dev/null", &[]),
1148
- Some("rtk git status 2>/dev/null".into())
1149
- );
1150
- }
1151
-
1152
- #[test]
1153
- fn test_rewrite_redirect_2_gt_amp_1_with_and() {
1154
- assert_eq!(
1155
- rewrite_command("cargo test 2>&1 && echo done", &[]),
1156
- Some("rtk cargo test 2>&1 && echo done".into())
1157
- );
1158
- }
1159
-
1160
- #[test]
1161
- fn test_rewrite_redirect_amp_gt_devnull() {
1162
- assert_eq!(
1163
- rewrite_command("cargo test &>/dev/null", &[]),
1164
- Some("rtk cargo test &>/dev/null".into())
1165
- );
1166
- }
1167
-
1168
- #[test]
1169
- fn test_rewrite_background_amp_non_regression() {
1170
- // background `&` must still work after redirect fix
1171
- assert_eq!(
1172
- rewrite_command("cargo test & git status", &[]),
1173
- Some("rtk cargo test & rtk git status".into())
1174
- );
1175
- }
1176
-
1177
- // --- P0.2: head -N rewrite ---
1178
-
1179
- #[test]
1180
- fn test_rewrite_head_numeric_flag() {
1181
- // head -20 file → rtk read file --max-lines 20 (not rtk read -20 file)
1182
- assert_eq!(
1183
- rewrite_command("head -20 src/main.rs", &[]),
1184
- Some("rtk read src/main.rs --max-lines 20".into())
1185
- );
1186
- }
1187
-
1188
- #[test]
1189
- fn test_rewrite_head_lines_long_flag() {
1190
- assert_eq!(
1191
- rewrite_command("head --lines=50 src/lib.rs", &[]),
1192
- Some("rtk read src/lib.rs --max-lines 50".into())
1193
- );
1194
- }
1195
-
1196
- #[test]
1197
- fn test_rewrite_head_no_flag_still_rewrites() {
1198
- // plain `head file` → `rtk read file` (no numeric flag)
1199
- assert_eq!(
1200
- rewrite_command("head src/main.rs", &[]),
1201
- Some("rtk read src/main.rs".into())
1202
- );
1203
- }
1204
-
1205
- #[test]
1206
- fn test_rewrite_head_other_flag_skipped() {
1207
- // head -c 100 file: unsupported flag, skip rewriting
1208
- assert_eq!(rewrite_command("head -c 100 src/main.rs", &[]), None);
1209
- }
1210
-
1211
- #[test]
1212
- fn test_rewrite_tail_numeric_flag() {
1213
- assert_eq!(
1214
- rewrite_command("tail -20 src/main.rs", &[]),
1215
- Some("rtk read src/main.rs --tail-lines 20".into())
1216
- );
1217
- }
1218
-
1219
- #[test]
1220
- fn test_rewrite_tail_n_space_flag() {
1221
- assert_eq!(
1222
- rewrite_command("tail -n 12 src/lib.rs", &[]),
1223
- Some("rtk read src/lib.rs --tail-lines 12".into())
1224
- );
1225
- }
1226
-
1227
- #[test]
1228
- fn test_rewrite_tail_lines_long_flag() {
1229
- assert_eq!(
1230
- rewrite_command("tail --lines=7 src/lib.rs", &[]),
1231
- Some("rtk read src/lib.rs --tail-lines 7".into())
1232
- );
1233
- }
1234
-
1235
- #[test]
1236
- fn test_rewrite_tail_lines_space_flag() {
1237
- assert_eq!(
1238
- rewrite_command("tail --lines 7 src/lib.rs", &[]),
1239
- Some("rtk read src/lib.rs --tail-lines 7".into())
1240
- );
1241
- }
1242
-
1243
- #[test]
1244
- fn test_rewrite_tail_other_flag_skipped() {
1245
- assert_eq!(rewrite_command("tail -c 100 src/main.rs", &[]), None);
1246
- }
1247
-
1248
- #[test]
1249
- fn test_rewrite_tail_plain_file_skipped() {
1250
- assert_eq!(rewrite_command("tail src/main.rs", &[]), None);
1251
- }
1252
-
1253
- // --- New registry entries ---
1254
-
1255
- #[test]
1256
- fn test_classify_gh_release() {
1257
- assert!(matches!(
1258
- classify_command("gh release list"),
1259
- Classification::Supported {
1260
- rtk_equivalent: "rtk gh",
1261
- ..
1262
- }
1263
- ));
1264
- }
1265
-
1266
- #[test]
1267
- fn test_classify_cargo_install() {
1268
- assert!(matches!(
1269
- classify_command("cargo install rtk"),
1270
- Classification::Supported {
1271
- rtk_equivalent: "rtk cargo",
1272
- ..
1273
- }
1274
- ));
1275
- }
1276
-
1277
- #[test]
1278
- fn test_classify_docker_run() {
1279
- assert!(matches!(
1280
- classify_command("docker run --rm ubuntu bash"),
1281
- Classification::Supported {
1282
- rtk_equivalent: "rtk docker",
1283
- ..
1284
- }
1285
- ));
1286
- }
1287
-
1288
- #[test]
1289
- fn test_classify_docker_exec() {
1290
- assert!(matches!(
1291
- classify_command("docker exec -it mycontainer bash"),
1292
- Classification::Supported {
1293
- rtk_equivalent: "rtk docker",
1294
- ..
1295
- }
1296
- ));
1297
- }
1298
-
1299
- #[test]
1300
- fn test_classify_docker_build() {
1301
- assert!(matches!(
1302
- classify_command("docker build -t myimage ."),
1303
- Classification::Supported {
1304
- rtk_equivalent: "rtk docker",
1305
- ..
1306
- }
1307
- ));
1308
- }
1309
-
1310
- #[test]
1311
- fn test_classify_kubectl_describe() {
1312
- assert!(matches!(
1313
- classify_command("kubectl describe pod mypod"),
1314
- Classification::Supported {
1315
- rtk_equivalent: "rtk kubectl",
1316
- ..
1317
- }
1318
- ));
1319
- }
1320
-
1321
- #[test]
1322
- fn test_classify_kubectl_apply() {
1323
- assert!(matches!(
1324
- classify_command("kubectl apply -f deploy.yaml"),
1325
- Classification::Supported {
1326
- rtk_equivalent: "rtk kubectl",
1327
- ..
1328
- }
1329
- ));
1330
- }
1331
-
1332
- #[test]
1333
- fn test_classify_tree() {
1334
- assert!(matches!(
1335
- classify_command("tree src/"),
1336
- Classification::Supported {
1337
- rtk_equivalent: "rtk tree",
1338
- ..
1339
- }
1340
- ));
1341
- }
1342
-
1343
- #[test]
1344
- fn test_classify_diff() {
1345
- assert!(matches!(
1346
- classify_command("diff file1.txt file2.txt"),
1347
- Classification::Supported {
1348
- rtk_equivalent: "rtk diff",
1349
- ..
1350
- }
1351
- ));
1352
- }
1353
-
1354
- #[test]
1355
- fn test_rewrite_tree() {
1356
- assert_eq!(
1357
- rewrite_command("tree src/", &[]),
1358
- Some("rtk tree src/".into())
1359
- );
1360
- }
1361
-
1362
- #[test]
1363
- fn test_rewrite_diff() {
1364
- assert_eq!(
1365
- rewrite_command("diff file1.txt file2.txt", &[]),
1366
- Some("rtk diff file1.txt file2.txt".into())
1367
- );
1368
- }
1369
-
1370
- #[test]
1371
- fn test_rewrite_gh_release() {
1372
- assert_eq!(
1373
- rewrite_command("gh release list", &[]),
1374
- Some("rtk gh release list".into())
1375
- );
1376
- }
1377
-
1378
- #[test]
1379
- fn test_rewrite_cargo_install() {
1380
- assert_eq!(
1381
- rewrite_command("cargo install rtk", &[]),
1382
- Some("rtk cargo install rtk".into())
1383
- );
1384
- }
1385
-
1386
- #[test]
1387
- fn test_rewrite_kubectl_describe() {
1388
- assert_eq!(
1389
- rewrite_command("kubectl describe pod mypod", &[]),
1390
- Some("rtk kubectl describe pod mypod".into())
1391
- );
1392
- }
1393
-
1394
- #[test]
1395
- fn test_rewrite_docker_run() {
1396
- assert_eq!(
1397
- rewrite_command("docker run --rm ubuntu bash", &[]),
1398
- Some("rtk docker run --rm ubuntu bash".into())
1399
- );
1400
- }
1401
-
1402
- // --- #336: docker compose supported subcommands rewritten, unsupported skipped ---
1403
-
1404
- #[test]
1405
- fn test_rewrite_docker_compose_ps() {
1406
- assert_eq!(
1407
- rewrite_command("docker compose ps", &[]),
1408
- Some("rtk docker compose ps".into())
1409
- );
1410
- }
1411
-
1412
- #[test]
1413
- fn test_rewrite_docker_compose_logs() {
1414
- assert_eq!(
1415
- rewrite_command("docker compose logs web", &[]),
1416
- Some("rtk docker compose logs web".into())
1417
- );
1418
- }
1419
-
1420
- #[test]
1421
- fn test_rewrite_docker_compose_build() {
1422
- assert_eq!(
1423
- rewrite_command("docker compose build", &[]),
1424
- Some("rtk docker compose build".into())
1425
- );
1426
- }
1427
-
1428
- #[test]
1429
- fn test_rewrite_docker_compose_up_skipped() {
1430
- assert_eq!(rewrite_command("docker compose up -d", &[]), None);
1431
- }
1432
-
1433
- #[test]
1434
- fn test_rewrite_docker_compose_down_skipped() {
1435
- assert_eq!(rewrite_command("docker compose down", &[]), None);
1436
- }
1437
-
1438
- #[test]
1439
- fn test_rewrite_docker_compose_config_skipped() {
1440
- assert_eq!(
1441
- rewrite_command("docker compose -f foo.yaml config --services", &[]),
1442
- None
1443
- );
1444
- }
1445
-
1446
- // --- AWS / psql (PR #216) ---
1447
-
1448
- #[test]
1449
- fn test_classify_aws() {
1450
- assert!(matches!(
1451
- classify_command("aws s3 ls"),
1452
- Classification::Supported {
1453
- rtk_equivalent: "rtk aws",
1454
- ..
1455
- }
1456
- ));
1457
- }
1458
-
1459
- #[test]
1460
- fn test_classify_aws_ec2() {
1461
- assert!(matches!(
1462
- classify_command("aws ec2 describe-instances"),
1463
- Classification::Supported {
1464
- rtk_equivalent: "rtk aws",
1465
- ..
1466
- }
1467
- ));
1468
- }
1469
-
1470
- #[test]
1471
- fn test_classify_psql() {
1472
- assert!(matches!(
1473
- classify_command("psql -U postgres"),
1474
- Classification::Supported {
1475
- rtk_equivalent: "rtk psql",
1476
- ..
1477
- }
1478
- ));
1479
- }
1480
-
1481
- #[test]
1482
- fn test_classify_psql_url() {
1483
- assert!(matches!(
1484
- classify_command("psql postgres://localhost/mydb"),
1485
- Classification::Supported {
1486
- rtk_equivalent: "rtk psql",
1487
- ..
1488
- }
1489
- ));
1490
- }
1491
-
1492
- #[test]
1493
- fn test_rewrite_aws() {
1494
- assert_eq!(
1495
- rewrite_command("aws s3 ls", &[]),
1496
- Some("rtk aws s3 ls".into())
1497
- );
1498
- }
1499
-
1500
- #[test]
1501
- fn test_rewrite_aws_ec2() {
1502
- assert_eq!(
1503
- rewrite_command("aws ec2 describe-instances --region us-east-1", &[]),
1504
- Some("rtk aws ec2 describe-instances --region us-east-1".into())
1505
- );
1506
- }
1507
-
1508
- #[test]
1509
- fn test_rewrite_psql() {
1510
- assert_eq!(
1511
- rewrite_command("psql -U postgres -d mydb", &[]),
1512
- Some("rtk psql -U postgres -d mydb".into())
1513
- );
1514
- }
1515
-
1516
- // --- Python tooling ---
1517
-
1518
- #[test]
1519
- fn test_classify_ruff_check() {
1520
- assert!(matches!(
1521
- classify_command("ruff check ."),
1522
- Classification::Supported {
1523
- rtk_equivalent: "rtk ruff",
1524
- ..
1525
- }
1526
- ));
1527
- }
1528
-
1529
- #[test]
1530
- fn test_classify_ruff_format() {
1531
- assert!(matches!(
1532
- classify_command("ruff format src/"),
1533
- Classification::Supported {
1534
- rtk_equivalent: "rtk ruff",
1535
- ..
1536
- }
1537
- ));
1538
- }
1539
-
1540
- #[test]
1541
- fn test_classify_pytest() {
1542
- assert!(matches!(
1543
- classify_command("pytest tests/"),
1544
- Classification::Supported {
1545
- rtk_equivalent: "rtk pytest",
1546
- ..
1547
- }
1548
- ));
1549
- }
1550
-
1551
- #[test]
1552
- fn test_classify_python_m_pytest() {
1553
- assert!(matches!(
1554
- classify_command("python -m pytest tests/"),
1555
- Classification::Supported {
1556
- rtk_equivalent: "rtk pytest",
1557
- ..
1558
- }
1559
- ));
1560
- }
1561
-
1562
- #[test]
1563
- fn test_classify_pip_list() {
1564
- assert!(matches!(
1565
- classify_command("pip list"),
1566
- Classification::Supported {
1567
- rtk_equivalent: "rtk pip",
1568
- ..
1569
- }
1570
- ));
1571
- }
1572
-
1573
- #[test]
1574
- fn test_classify_uv_pip_list() {
1575
- assert!(matches!(
1576
- classify_command("uv pip list"),
1577
- Classification::Supported {
1578
- rtk_equivalent: "rtk pip",
1579
- ..
1580
- }
1581
- ));
1582
- }
1583
-
1584
- #[test]
1585
- fn test_rewrite_ruff_check() {
1586
- assert_eq!(
1587
- rewrite_command("ruff check .", &[]),
1588
- Some("rtk ruff check .".into())
1589
- );
1590
- }
1591
-
1592
- #[test]
1593
- fn test_rewrite_ruff_format() {
1594
- assert_eq!(
1595
- rewrite_command("ruff format src/", &[]),
1596
- Some("rtk ruff format src/".into())
1597
- );
1598
- }
1599
-
1600
- #[test]
1601
- fn test_rewrite_pytest() {
1602
- assert_eq!(
1603
- rewrite_command("pytest tests/", &[]),
1604
- Some("rtk pytest tests/".into())
1605
- );
1606
- }
1607
-
1608
- #[test]
1609
- fn test_rewrite_python_m_pytest() {
1610
- assert_eq!(
1611
- rewrite_command("python -m pytest -x tests/", &[]),
1612
- Some("rtk pytest -x tests/".into())
1613
- );
1614
- }
1615
-
1616
- #[test]
1617
- fn test_rewrite_pip_list() {
1618
- assert_eq!(
1619
- rewrite_command("pip list", &[]),
1620
- Some("rtk pip list".into())
1621
- );
1622
- }
1623
-
1624
- #[test]
1625
- fn test_rewrite_pip_outdated() {
1626
- assert_eq!(
1627
- rewrite_command("pip outdated", &[]),
1628
- Some("rtk pip outdated".into())
1629
- );
1630
- }
1631
-
1632
- #[test]
1633
- fn test_rewrite_uv_pip_list() {
1634
- assert_eq!(
1635
- rewrite_command("uv pip list", &[]),
1636
- Some("rtk pip list".into())
1637
- );
1638
- }
1639
-
1640
- // --- Go tooling ---
1641
-
1642
- #[test]
1643
- fn test_classify_go_test() {
1644
- assert!(matches!(
1645
- classify_command("go test ./..."),
1646
- Classification::Supported {
1647
- rtk_equivalent: "rtk go",
1648
- ..
1649
- }
1650
- ));
1651
- }
1652
-
1653
- #[test]
1654
- fn test_classify_go_build() {
1655
- assert!(matches!(
1656
- classify_command("go build ./..."),
1657
- Classification::Supported {
1658
- rtk_equivalent: "rtk go",
1659
- ..
1660
- }
1661
- ));
1662
- }
1663
-
1664
- #[test]
1665
- fn test_classify_go_vet() {
1666
- assert!(matches!(
1667
- classify_command("go vet ./..."),
1668
- Classification::Supported {
1669
- rtk_equivalent: "rtk go",
1670
- ..
1671
- }
1672
- ));
1673
- }
1674
-
1675
- #[test]
1676
- fn test_classify_golangci_lint() {
1677
- assert!(matches!(
1678
- classify_command("golangci-lint run"),
1679
- Classification::Supported {
1680
- rtk_equivalent: "rtk golangci-lint",
1681
- ..
1682
- }
1683
- ));
1684
- }
1685
-
1686
- #[test]
1687
- fn test_rewrite_go_test() {
1688
- assert_eq!(
1689
- rewrite_command("go test ./...", &[]),
1690
- Some("rtk go test ./...".into())
1691
- );
1692
- }
1693
-
1694
- #[test]
1695
- fn test_rewrite_go_build() {
1696
- assert_eq!(
1697
- rewrite_command("go build ./...", &[]),
1698
- Some("rtk go build ./...".into())
1699
- );
1700
- }
1701
-
1702
- #[test]
1703
- fn test_rewrite_go_vet() {
1704
- assert_eq!(
1705
- rewrite_command("go vet ./...", &[]),
1706
- Some("rtk go vet ./...".into())
1707
- );
1708
- }
1709
-
1710
- #[test]
1711
- fn test_rewrite_golangci_lint() {
1712
- assert_eq!(
1713
- rewrite_command("golangci-lint run ./...", &[]),
1714
- Some("rtk golangci-lint run ./...".into())
1715
- );
1716
- }
1717
-
1718
- // --- JS/TS tooling ---
1719
-
1720
- #[test]
1721
- fn test_classify_vitest() {
1722
- assert!(matches!(
1723
- classify_command("vitest run"),
1724
- Classification::Supported {
1725
- rtk_equivalent: "rtk vitest",
1726
- ..
1727
- }
1728
- ));
1729
- }
1730
-
1731
- #[test]
1732
- fn test_rewrite_vitest() {
1733
- assert_eq!(
1734
- rewrite_command("vitest run", &[]),
1735
- Some("rtk vitest run".into())
1736
- );
1737
- }
1738
-
1739
- #[test]
1740
- fn test_rewrite_pnpm_vitest() {
1741
- assert_eq!(
1742
- rewrite_command("pnpm vitest run", &[]),
1743
- Some("rtk vitest run".into())
1744
- );
1745
- }
1746
-
1747
- #[test]
1748
- fn test_classify_prisma() {
1749
- assert!(matches!(
1750
- classify_command("npx prisma migrate dev"),
1751
- Classification::Supported {
1752
- rtk_equivalent: "rtk prisma",
1753
- ..
1754
- }
1755
- ));
1756
- }
1757
-
1758
- #[test]
1759
- fn test_rewrite_prisma() {
1760
- assert_eq!(
1761
- rewrite_command("npx prisma migrate dev", &[]),
1762
- Some("rtk prisma migrate dev".into())
1763
- );
1764
- }
1765
-
1766
- #[test]
1767
- fn test_rewrite_prettier() {
1768
- assert_eq!(
1769
- rewrite_command("npx prettier --check src/", &[]),
1770
- Some("rtk prettier --check src/".into())
1771
- );
1772
- }
1773
-
1774
- #[test]
1775
- fn test_rewrite_pnpm_list() {
1776
- assert_eq!(
1777
- rewrite_command("pnpm list", &[]),
1778
- Some("rtk pnpm list".into())
1779
- );
1780
- }
1781
-
1782
- // --- Compound operator edge cases ---
1783
-
1784
- #[test]
1785
- fn test_rewrite_compound_or() {
1786
- // `||` fallback: left rewritten, right rewritten
1787
- assert_eq!(
1788
- rewrite_command("cargo test || cargo build", &[]),
1789
- Some("rtk cargo test || rtk cargo build".into())
1790
- );
1791
- }
1792
-
1793
- #[test]
1794
- fn test_rewrite_compound_semicolon() {
1795
- assert_eq!(
1796
- rewrite_command("git status; cargo test", &[]),
1797
- Some("rtk git status; rtk cargo test".into())
1798
- );
1799
- }
1800
-
1801
- #[test]
1802
- fn test_rewrite_compound_pipe_raw_filter() {
1803
- // Pipe: rewrite first segment only, pass through rest unchanged
1804
- assert_eq!(
1805
- rewrite_command("cargo test | grep FAILED", &[]),
1806
- Some("rtk cargo test | grep FAILED".into())
1807
- );
1808
- }
1809
-
1810
- #[test]
1811
- fn test_rewrite_compound_pipe_git_grep() {
1812
- assert_eq!(
1813
- rewrite_command("git log -10 | grep feat", &[]),
1814
- Some("rtk git log -10 | grep feat".into())
1815
- );
1816
- }
1817
-
1818
- #[test]
1819
- fn test_rewrite_compound_four_segments() {
1820
- assert_eq!(
1821
- rewrite_command(
1822
- "cargo fmt --all && cargo clippy && cargo test && git status",
1823
- &[]
1824
- ),
1825
- Some(
1826
- "rtk cargo fmt --all && rtk cargo clippy && rtk cargo test && rtk git status"
1827
- .into()
1828
- )
1829
- );
1830
- }
1831
-
1832
- #[test]
1833
- fn test_rewrite_compound_mixed_supported_unsupported() {
1834
- // unsupported segments stay raw
1835
- assert_eq!(
1836
- rewrite_command("cargo test && htop", &[]),
1837
- Some("rtk cargo test && htop".into())
1838
- );
1839
- }
1840
-
1841
- #[test]
1842
- fn test_rewrite_compound_all_unsupported_returns_none() {
1843
- // No rewrite at all: returns None
1844
- assert_eq!(rewrite_command("htop && top", &[]), None);
1845
- }
1846
-
1847
- // --- sudo / env prefix + rewrite ---
1848
-
1849
- #[test]
1850
- fn test_rewrite_sudo_docker() {
1851
- assert_eq!(
1852
- rewrite_command("sudo docker ps", &[]),
1853
- Some("sudo rtk docker ps".into())
1854
- );
1855
- }
1856
-
1857
- #[test]
1858
- fn test_rewrite_env_var_prefix() {
1859
- assert_eq!(
1860
- rewrite_command("GIT_SSH_COMMAND=ssh git push origin main", &[]),
1861
- Some("GIT_SSH_COMMAND=ssh rtk git push origin main".into())
1862
- );
1863
- }
1864
-
1865
- // --- find with native flags ---
1866
-
1867
- #[test]
1868
- fn test_rewrite_find_with_flags() {
1869
- assert_eq!(
1870
- rewrite_command("find . -name '*.rs' -type f", &[]),
1871
- Some("rtk find . -name '*.rs' -type f".into())
1872
- );
1873
- }
1874
-
1875
- // --- Ensure PATTERNS and RULES stay aligned after modifications ---
1876
-
1877
- #[test]
1878
- fn test_patterns_rules_aligned_after_aws_psql() {
1879
- // If this fails, someone added a PATTERN without a matching RULE (or vice versa)
1880
- assert_eq!(
1881
- PATTERNS.len(),
1882
- RULES.len(),
1883
- "PATTERNS[{}] != RULES[{}] — they must stay 1:1",
1884
- PATTERNS.len(),
1885
- RULES.len()
1886
- );
1887
- }
1888
-
1889
- // --- All RULES have non-empty rtk_cmd and at least one rewrite_prefix ---
1890
-
1891
- #[test]
1892
- fn test_all_rules_have_valid_rtk_cmd() {
1893
- for rule in RULES {
1894
- assert!(!rule.rtk_cmd.is_empty(), "Rule with empty rtk_cmd found");
1895
- assert!(
1896
- rule.rtk_cmd.starts_with("rtk "),
1897
- "rtk_cmd '{}' must start with 'rtk '",
1898
- rule.rtk_cmd
1899
- );
1900
- assert!(
1901
- !rule.rewrite_prefixes.is_empty(),
1902
- "Rule '{}' has no rewrite_prefixes",
1903
- rule.rtk_cmd
1904
- );
1905
- }
1906
- }
1907
-
1908
- // --- exclude_commands (#243) ---
1909
-
1910
- #[test]
1911
- fn test_rewrite_excludes_curl() {
1912
- let excluded = vec!["curl".to_string()];
1913
- assert_eq!(
1914
- rewrite_command("curl https://api.example.com/health", &excluded),
1915
- None
1916
- );
1917
- }
1918
-
1919
- #[test]
1920
- fn test_rewrite_exclude_does_not_affect_other_commands() {
1921
- let excluded = vec!["curl".to_string()];
1922
- assert_eq!(
1923
- rewrite_command("git status", &excluded),
1924
- Some("rtk git status".into())
1925
- );
1926
- }
1927
-
1928
- #[test]
1929
- fn test_rewrite_empty_excludes_rewrites_curl() {
1930
- let excluded: Vec<String> = vec![];
1931
- assert!(rewrite_command("curl https://api.example.com", &excluded).is_some());
1932
- }
1933
-
1934
- #[test]
1935
- fn test_rewrite_compound_partial_exclude() {
1936
- // curl excluded but git still rewrites
1937
- let excluded = vec!["curl".to_string()];
1938
- assert_eq!(
1939
- rewrite_command("git status && curl https://api.example.com", &excluded),
1940
- Some("rtk git status && curl https://api.example.com".into())
1941
- );
1942
- }
1943
-
1944
- // --- Every PATTERN compiles to a valid Regex ---
1945
-
1946
- #[test]
1947
- fn test_all_patterns_are_valid_regex() {
1948
- use regex::Regex;
1949
- for (i, pattern) in PATTERNS.iter().enumerate() {
1950
- assert!(
1951
- Regex::new(pattern).is_ok(),
1952
- "PATTERNS[{i}] = '{pattern}' is not a valid regex"
1953
- );
1954
- }
1955
- }
1956
-
1957
- // --- #196: gh --json/--jq/--template passthrough ---
1958
-
1959
- #[test]
1960
- fn test_rewrite_gh_json_skipped() {
1961
- assert_eq!(rewrite_command("gh pr list --json number,title", &[]), None);
1962
- }
1963
-
1964
- #[test]
1965
- fn test_rewrite_gh_jq_skipped() {
1966
- assert_eq!(
1967
- rewrite_command("gh pr list --json number --jq '.[].number'", &[]),
1968
- None
1969
- );
1970
- }
1971
-
1972
- #[test]
1973
- fn test_rewrite_gh_template_skipped() {
1974
- assert_eq!(
1975
- rewrite_command("gh pr view 42 --template '{{.title}}'", &[]),
1976
- None
1977
- );
1978
- }
1979
-
1980
- #[test]
1981
- fn test_rewrite_gh_api_json_skipped() {
1982
- assert_eq!(
1983
- rewrite_command("gh api repos/owner/repo --jq '.name'", &[]),
1984
- None
1985
- );
1986
- }
1987
-
1988
- #[test]
1989
- fn test_rewrite_gh_without_json_still_works() {
1990
- assert_eq!(
1991
- rewrite_command("gh pr list", &[]),
1992
- Some("rtk gh pr list".into())
1993
- );
1994
- }
1995
-
1996
- // --- #508: RTK_DISABLED detection helpers ---
1997
-
1998
- #[test]
1999
- fn test_has_rtk_disabled_prefix() {
2000
- assert!(has_rtk_disabled_prefix("RTK_DISABLED=1 git status"));
2001
- assert!(has_rtk_disabled_prefix("FOO=1 RTK_DISABLED=1 cargo test"));
2002
- assert!(has_rtk_disabled_prefix(
2003
- "RTK_DISABLED=true git log --oneline"
2004
- ));
2005
- assert!(!has_rtk_disabled_prefix("git status"));
2006
- assert!(!has_rtk_disabled_prefix("rtk git status"));
2007
- assert!(!has_rtk_disabled_prefix("SOME_VAR=1 git status"));
2008
- }
2009
-
2010
- #[test]
2011
- fn test_strip_disabled_prefix() {
2012
- assert_eq!(
2013
- strip_disabled_prefix("RTK_DISABLED=1 git status"),
2014
- "git status"
2015
- );
2016
- assert_eq!(
2017
- strip_disabled_prefix("FOO=1 RTK_DISABLED=1 cargo test"),
2018
- "cargo test"
2019
- );
2020
- assert_eq!(strip_disabled_prefix("git status"), "git status");
2021
- }
2022
- }