@hasna/terminal 2.3.0 → 2.3.1

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 (202) hide show
  1. package/dist/cli.js +64 -16
  2. package/package.json +1 -1
  3. package/src/ai.ts +8 -0
  4. package/src/cli.tsx +57 -18
  5. package/src/output-processor.ts +6 -1
  6. package/src/output-store.ts +58 -12
  7. package/src/tool-profiles.ts +139 -0
  8. package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
  9. package/temp/rtk/.claude/agents/debugger.md +0 -519
  10. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
  11. package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
  12. package/temp/rtk/.claude/agents/technical-writer.md +0 -355
  13. package/temp/rtk/.claude/commands/diagnose.md +0 -352
  14. package/temp/rtk/.claude/commands/test-routing.md +0 -362
  15. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
  16. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
  17. package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
  18. package/temp/rtk/.claude/rules/cli-testing.md +0 -526
  19. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
  20. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
  21. package/temp/rtk/.claude/skills/performance.md +0 -435
  22. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
  23. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
  24. package/temp/rtk/.claude/skills/repo-recap.md +0 -206
  25. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
  26. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
  27. package/temp/rtk/.claude/skills/security-guardian.md +0 -503
  28. package/temp/rtk/.claude/skills/ship.md +0 -404
  29. package/temp/rtk/.github/workflows/benchmark.yml +0 -34
  30. package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
  31. package/temp/rtk/.github/workflows/release-please.yml +0 -51
  32. package/temp/rtk/.github/workflows/release.yml +0 -343
  33. package/temp/rtk/.github/workflows/security-check.yml +0 -135
  34. package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
  35. package/temp/rtk/.release-please-manifest.json +0 -3
  36. package/temp/rtk/ARCHITECTURE.md +0 -1491
  37. package/temp/rtk/CHANGELOG.md +0 -640
  38. package/temp/rtk/CLAUDE.md +0 -605
  39. package/temp/rtk/CONTRIBUTING.md +0 -199
  40. package/temp/rtk/Cargo.lock +0 -1668
  41. package/temp/rtk/Cargo.toml +0 -64
  42. package/temp/rtk/Formula/rtk.rb +0 -43
  43. package/temp/rtk/INSTALL.md +0 -390
  44. package/temp/rtk/LICENSE +0 -21
  45. package/temp/rtk/README.md +0 -386
  46. package/temp/rtk/README_es.md +0 -159
  47. package/temp/rtk/README_fr.md +0 -197
  48. package/temp/rtk/README_ja.md +0 -159
  49. package/temp/rtk/README_ko.md +0 -159
  50. package/temp/rtk/README_zh.md +0 -167
  51. package/temp/rtk/ROADMAP.md +0 -15
  52. package/temp/rtk/SECURITY.md +0 -217
  53. package/temp/rtk/TEST_EXEC_TIME.md +0 -102
  54. package/temp/rtk/build.rs +0 -57
  55. package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
  56. package/temp/rtk/docs/FEATURES.md +0 -1410
  57. package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
  58. package/temp/rtk/docs/filter-workflow.md +0 -102
  59. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  60. package/temp/rtk/docs/tracking.md +0 -583
  61. package/temp/rtk/hooks/opencode-rtk.ts +0 -39
  62. package/temp/rtk/hooks/rtk-awareness.md +0 -29
  63. package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
  64. package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
  65. package/temp/rtk/install.sh +0 -124
  66. package/temp/rtk/release-please-config.json +0 -10
  67. package/temp/rtk/scripts/benchmark.sh +0 -592
  68. package/temp/rtk/scripts/check-installation.sh +0 -162
  69. package/temp/rtk/scripts/install-local.sh +0 -37
  70. package/temp/rtk/scripts/rtk-economics.sh +0 -137
  71. package/temp/rtk/scripts/test-all.sh +0 -561
  72. package/temp/rtk/scripts/test-aristote.sh +0 -227
  73. package/temp/rtk/scripts/test-tracking.sh +0 -79
  74. package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
  75. package/temp/rtk/scripts/validate-docs.sh +0 -73
  76. package/temp/rtk/src/aws_cmd.rs +0 -880
  77. package/temp/rtk/src/binlog.rs +0 -1645
  78. package/temp/rtk/src/cargo_cmd.rs +0 -1727
  79. package/temp/rtk/src/cc_economics.rs +0 -1157
  80. package/temp/rtk/src/ccusage.rs +0 -340
  81. package/temp/rtk/src/config.rs +0 -187
  82. package/temp/rtk/src/container.rs +0 -855
  83. package/temp/rtk/src/curl_cmd.rs +0 -134
  84. package/temp/rtk/src/deps.rs +0 -268
  85. package/temp/rtk/src/diff_cmd.rs +0 -367
  86. package/temp/rtk/src/discover/mod.rs +0 -274
  87. package/temp/rtk/src/discover/provider.rs +0 -388
  88. package/temp/rtk/src/discover/registry.rs +0 -2022
  89. package/temp/rtk/src/discover/report.rs +0 -202
  90. package/temp/rtk/src/discover/rules.rs +0 -667
  91. package/temp/rtk/src/display_helpers.rs +0 -402
  92. package/temp/rtk/src/dotnet_cmd.rs +0 -1771
  93. package/temp/rtk/src/dotnet_format_report.rs +0 -133
  94. package/temp/rtk/src/dotnet_trx.rs +0 -593
  95. package/temp/rtk/src/env_cmd.rs +0 -204
  96. package/temp/rtk/src/filter.rs +0 -462
  97. package/temp/rtk/src/filters/README.md +0 -52
  98. package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
  99. package/temp/rtk/src/filters/basedpyright.toml +0 -47
  100. package/temp/rtk/src/filters/biome.toml +0 -45
  101. package/temp/rtk/src/filters/brew-install.toml +0 -37
  102. package/temp/rtk/src/filters/composer-install.toml +0 -40
  103. package/temp/rtk/src/filters/df.toml +0 -16
  104. package/temp/rtk/src/filters/dotnet-build.toml +0 -64
  105. package/temp/rtk/src/filters/du.toml +0 -16
  106. package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
  107. package/temp/rtk/src/filters/gcc.toml +0 -49
  108. package/temp/rtk/src/filters/gcloud.toml +0 -22
  109. package/temp/rtk/src/filters/hadolint.toml +0 -24
  110. package/temp/rtk/src/filters/helm.toml +0 -29
  111. package/temp/rtk/src/filters/iptables.toml +0 -27
  112. package/temp/rtk/src/filters/jj.toml +0 -28
  113. package/temp/rtk/src/filters/jq.toml +0 -24
  114. package/temp/rtk/src/filters/make.toml +0 -41
  115. package/temp/rtk/src/filters/markdownlint.toml +0 -24
  116. package/temp/rtk/src/filters/mix-compile.toml +0 -27
  117. package/temp/rtk/src/filters/mix-format.toml +0 -15
  118. package/temp/rtk/src/filters/mvn-build.toml +0 -44
  119. package/temp/rtk/src/filters/oxlint.toml +0 -43
  120. package/temp/rtk/src/filters/ping.toml +0 -63
  121. package/temp/rtk/src/filters/pio-run.toml +0 -40
  122. package/temp/rtk/src/filters/poetry-install.toml +0 -50
  123. package/temp/rtk/src/filters/pre-commit.toml +0 -35
  124. package/temp/rtk/src/filters/ps.toml +0 -16
  125. package/temp/rtk/src/filters/quarto-render.toml +0 -41
  126. package/temp/rtk/src/filters/rsync.toml +0 -48
  127. package/temp/rtk/src/filters/shellcheck.toml +0 -27
  128. package/temp/rtk/src/filters/shopify-theme.toml +0 -29
  129. package/temp/rtk/src/filters/skopeo.toml +0 -45
  130. package/temp/rtk/src/filters/sops.toml +0 -16
  131. package/temp/rtk/src/filters/ssh.toml +0 -44
  132. package/temp/rtk/src/filters/stat.toml +0 -34
  133. package/temp/rtk/src/filters/swift-build.toml +0 -41
  134. package/temp/rtk/src/filters/systemctl-status.toml +0 -33
  135. package/temp/rtk/src/filters/terraform-plan.toml +0 -35
  136. package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
  137. package/temp/rtk/src/filters/tofu-init.toml +0 -38
  138. package/temp/rtk/src/filters/tofu-plan.toml +0 -35
  139. package/temp/rtk/src/filters/tofu-validate.toml +0 -17
  140. package/temp/rtk/src/filters/trunk-build.toml +0 -39
  141. package/temp/rtk/src/filters/ty.toml +0 -50
  142. package/temp/rtk/src/filters/uv-sync.toml +0 -37
  143. package/temp/rtk/src/filters/xcodebuild.toml +0 -99
  144. package/temp/rtk/src/filters/yamllint.toml +0 -25
  145. package/temp/rtk/src/find_cmd.rs +0 -598
  146. package/temp/rtk/src/format_cmd.rs +0 -386
  147. package/temp/rtk/src/gain.rs +0 -723
  148. package/temp/rtk/src/gh_cmd.rs +0 -1651
  149. package/temp/rtk/src/git.rs +0 -2012
  150. package/temp/rtk/src/go_cmd.rs +0 -592
  151. package/temp/rtk/src/golangci_cmd.rs +0 -254
  152. package/temp/rtk/src/grep_cmd.rs +0 -288
  153. package/temp/rtk/src/gt_cmd.rs +0 -810
  154. package/temp/rtk/src/hook_audit_cmd.rs +0 -283
  155. package/temp/rtk/src/hook_check.rs +0 -171
  156. package/temp/rtk/src/init.rs +0 -1859
  157. package/temp/rtk/src/integrity.rs +0 -537
  158. package/temp/rtk/src/json_cmd.rs +0 -231
  159. package/temp/rtk/src/learn/detector.rs +0 -628
  160. package/temp/rtk/src/learn/mod.rs +0 -119
  161. package/temp/rtk/src/learn/report.rs +0 -184
  162. package/temp/rtk/src/lint_cmd.rs +0 -694
  163. package/temp/rtk/src/local_llm.rs +0 -316
  164. package/temp/rtk/src/log_cmd.rs +0 -248
  165. package/temp/rtk/src/ls.rs +0 -324
  166. package/temp/rtk/src/main.rs +0 -2482
  167. package/temp/rtk/src/mypy_cmd.rs +0 -389
  168. package/temp/rtk/src/next_cmd.rs +0 -241
  169. package/temp/rtk/src/npm_cmd.rs +0 -236
  170. package/temp/rtk/src/parser/README.md +0 -267
  171. package/temp/rtk/src/parser/error.rs +0 -46
  172. package/temp/rtk/src/parser/formatter.rs +0 -336
  173. package/temp/rtk/src/parser/mod.rs +0 -311
  174. package/temp/rtk/src/parser/types.rs +0 -119
  175. package/temp/rtk/src/pip_cmd.rs +0 -302
  176. package/temp/rtk/src/playwright_cmd.rs +0 -479
  177. package/temp/rtk/src/pnpm_cmd.rs +0 -573
  178. package/temp/rtk/src/prettier_cmd.rs +0 -221
  179. package/temp/rtk/src/prisma_cmd.rs +0 -482
  180. package/temp/rtk/src/psql_cmd.rs +0 -382
  181. package/temp/rtk/src/pytest_cmd.rs +0 -384
  182. package/temp/rtk/src/read.rs +0 -217
  183. package/temp/rtk/src/rewrite_cmd.rs +0 -50
  184. package/temp/rtk/src/ruff_cmd.rs +0 -402
  185. package/temp/rtk/src/runner.rs +0 -271
  186. package/temp/rtk/src/summary.rs +0 -297
  187. package/temp/rtk/src/tee.rs +0 -405
  188. package/temp/rtk/src/telemetry.rs +0 -248
  189. package/temp/rtk/src/toml_filter.rs +0 -1655
  190. package/temp/rtk/src/tracking.rs +0 -1416
  191. package/temp/rtk/src/tree.rs +0 -209
  192. package/temp/rtk/src/tsc_cmd.rs +0 -259
  193. package/temp/rtk/src/utils.rs +0 -432
  194. package/temp/rtk/src/verify_cmd.rs +0 -47
  195. package/temp/rtk/src/vitest_cmd.rs +0 -385
  196. package/temp/rtk/src/wc_cmd.rs +0 -401
  197. package/temp/rtk/src/wget_cmd.rs +0 -260
  198. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
  199. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
  200. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
  201. package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
  202. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
@@ -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
- }