@hasna/terminal 2.2.0 → 2.3.0

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 (205) hide show
  1. package/dist/cli.js +29 -12
  2. package/package.json +1 -1
  3. package/src/ai.ts +50 -36
  4. package/src/cli.tsx +29 -12
  5. package/src/context-hints.ts +89 -0
  6. package/src/discover.ts +238 -0
  7. package/src/economy.ts +53 -0
  8. package/src/output-store.ts +65 -0
  9. package/src/providers/index.ts +4 -4
  10. package/src/sessions-db.ts +81 -0
  11. package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
  12. package/temp/rtk/.claude/agents/debugger.md +519 -0
  13. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
  14. package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
  15. package/temp/rtk/.claude/agents/technical-writer.md +355 -0
  16. package/temp/rtk/.claude/commands/diagnose.md +352 -0
  17. package/temp/rtk/.claude/commands/test-routing.md +362 -0
  18. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
  19. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
  20. package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
  21. package/temp/rtk/.claude/rules/cli-testing.md +526 -0
  22. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
  23. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
  24. package/temp/rtk/.claude/skills/performance.md +435 -0
  25. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
  26. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
  27. package/temp/rtk/.claude/skills/repo-recap.md +206 -0
  28. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
  29. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
  30. package/temp/rtk/.claude/skills/security-guardian.md +503 -0
  31. package/temp/rtk/.claude/skills/ship.md +404 -0
  32. package/temp/rtk/.github/workflows/benchmark.yml +34 -0
  33. package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
  34. package/temp/rtk/.github/workflows/release-please.yml +51 -0
  35. package/temp/rtk/.github/workflows/release.yml +343 -0
  36. package/temp/rtk/.github/workflows/security-check.yml +135 -0
  37. package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
  38. package/temp/rtk/.release-please-manifest.json +3 -0
  39. package/temp/rtk/ARCHITECTURE.md +1491 -0
  40. package/temp/rtk/CHANGELOG.md +640 -0
  41. package/temp/rtk/CLAUDE.md +605 -0
  42. package/temp/rtk/CONTRIBUTING.md +199 -0
  43. package/temp/rtk/Cargo.lock +1668 -0
  44. package/temp/rtk/Cargo.toml +64 -0
  45. package/temp/rtk/Formula/rtk.rb +43 -0
  46. package/temp/rtk/INSTALL.md +390 -0
  47. package/temp/rtk/LICENSE +21 -0
  48. package/temp/rtk/README.md +386 -0
  49. package/temp/rtk/README_es.md +159 -0
  50. package/temp/rtk/README_fr.md +197 -0
  51. package/temp/rtk/README_ja.md +159 -0
  52. package/temp/rtk/README_ko.md +159 -0
  53. package/temp/rtk/README_zh.md +167 -0
  54. package/temp/rtk/ROADMAP.md +15 -0
  55. package/temp/rtk/SECURITY.md +217 -0
  56. package/temp/rtk/TEST_EXEC_TIME.md +102 -0
  57. package/temp/rtk/build.rs +57 -0
  58. package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
  59. package/temp/rtk/docs/FEATURES.md +1410 -0
  60. package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
  61. package/temp/rtk/docs/filter-workflow.md +102 -0
  62. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  63. package/temp/rtk/docs/tracking.md +583 -0
  64. package/temp/rtk/hooks/opencode-rtk.ts +39 -0
  65. package/temp/rtk/hooks/rtk-awareness.md +29 -0
  66. package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
  67. package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
  68. package/temp/rtk/install.sh +124 -0
  69. package/temp/rtk/release-please-config.json +10 -0
  70. package/temp/rtk/scripts/benchmark.sh +592 -0
  71. package/temp/rtk/scripts/check-installation.sh +162 -0
  72. package/temp/rtk/scripts/install-local.sh +37 -0
  73. package/temp/rtk/scripts/rtk-economics.sh +137 -0
  74. package/temp/rtk/scripts/test-all.sh +561 -0
  75. package/temp/rtk/scripts/test-aristote.sh +227 -0
  76. package/temp/rtk/scripts/test-tracking.sh +79 -0
  77. package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
  78. package/temp/rtk/scripts/validate-docs.sh +73 -0
  79. package/temp/rtk/src/aws_cmd.rs +880 -0
  80. package/temp/rtk/src/binlog.rs +1645 -0
  81. package/temp/rtk/src/cargo_cmd.rs +1727 -0
  82. package/temp/rtk/src/cc_economics.rs +1157 -0
  83. package/temp/rtk/src/ccusage.rs +340 -0
  84. package/temp/rtk/src/config.rs +187 -0
  85. package/temp/rtk/src/container.rs +855 -0
  86. package/temp/rtk/src/curl_cmd.rs +134 -0
  87. package/temp/rtk/src/deps.rs +268 -0
  88. package/temp/rtk/src/diff_cmd.rs +367 -0
  89. package/temp/rtk/src/discover/mod.rs +274 -0
  90. package/temp/rtk/src/discover/provider.rs +388 -0
  91. package/temp/rtk/src/discover/registry.rs +2022 -0
  92. package/temp/rtk/src/discover/report.rs +202 -0
  93. package/temp/rtk/src/discover/rules.rs +667 -0
  94. package/temp/rtk/src/display_helpers.rs +402 -0
  95. package/temp/rtk/src/dotnet_cmd.rs +1771 -0
  96. package/temp/rtk/src/dotnet_format_report.rs +133 -0
  97. package/temp/rtk/src/dotnet_trx.rs +593 -0
  98. package/temp/rtk/src/env_cmd.rs +204 -0
  99. package/temp/rtk/src/filter.rs +462 -0
  100. package/temp/rtk/src/filters/README.md +52 -0
  101. package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
  102. package/temp/rtk/src/filters/basedpyright.toml +47 -0
  103. package/temp/rtk/src/filters/biome.toml +45 -0
  104. package/temp/rtk/src/filters/brew-install.toml +37 -0
  105. package/temp/rtk/src/filters/composer-install.toml +40 -0
  106. package/temp/rtk/src/filters/df.toml +16 -0
  107. package/temp/rtk/src/filters/dotnet-build.toml +64 -0
  108. package/temp/rtk/src/filters/du.toml +16 -0
  109. package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
  110. package/temp/rtk/src/filters/gcc.toml +49 -0
  111. package/temp/rtk/src/filters/gcloud.toml +22 -0
  112. package/temp/rtk/src/filters/hadolint.toml +24 -0
  113. package/temp/rtk/src/filters/helm.toml +29 -0
  114. package/temp/rtk/src/filters/iptables.toml +27 -0
  115. package/temp/rtk/src/filters/jj.toml +28 -0
  116. package/temp/rtk/src/filters/jq.toml +24 -0
  117. package/temp/rtk/src/filters/make.toml +41 -0
  118. package/temp/rtk/src/filters/markdownlint.toml +24 -0
  119. package/temp/rtk/src/filters/mix-compile.toml +27 -0
  120. package/temp/rtk/src/filters/mix-format.toml +15 -0
  121. package/temp/rtk/src/filters/mvn-build.toml +44 -0
  122. package/temp/rtk/src/filters/oxlint.toml +43 -0
  123. package/temp/rtk/src/filters/ping.toml +63 -0
  124. package/temp/rtk/src/filters/pio-run.toml +40 -0
  125. package/temp/rtk/src/filters/poetry-install.toml +50 -0
  126. package/temp/rtk/src/filters/pre-commit.toml +35 -0
  127. package/temp/rtk/src/filters/ps.toml +16 -0
  128. package/temp/rtk/src/filters/quarto-render.toml +41 -0
  129. package/temp/rtk/src/filters/rsync.toml +48 -0
  130. package/temp/rtk/src/filters/shellcheck.toml +27 -0
  131. package/temp/rtk/src/filters/shopify-theme.toml +29 -0
  132. package/temp/rtk/src/filters/skopeo.toml +45 -0
  133. package/temp/rtk/src/filters/sops.toml +16 -0
  134. package/temp/rtk/src/filters/ssh.toml +44 -0
  135. package/temp/rtk/src/filters/stat.toml +34 -0
  136. package/temp/rtk/src/filters/swift-build.toml +41 -0
  137. package/temp/rtk/src/filters/systemctl-status.toml +33 -0
  138. package/temp/rtk/src/filters/terraform-plan.toml +35 -0
  139. package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
  140. package/temp/rtk/src/filters/tofu-init.toml +38 -0
  141. package/temp/rtk/src/filters/tofu-plan.toml +35 -0
  142. package/temp/rtk/src/filters/tofu-validate.toml +17 -0
  143. package/temp/rtk/src/filters/trunk-build.toml +39 -0
  144. package/temp/rtk/src/filters/ty.toml +50 -0
  145. package/temp/rtk/src/filters/uv-sync.toml +37 -0
  146. package/temp/rtk/src/filters/xcodebuild.toml +99 -0
  147. package/temp/rtk/src/filters/yamllint.toml +25 -0
  148. package/temp/rtk/src/find_cmd.rs +598 -0
  149. package/temp/rtk/src/format_cmd.rs +386 -0
  150. package/temp/rtk/src/gain.rs +723 -0
  151. package/temp/rtk/src/gh_cmd.rs +1651 -0
  152. package/temp/rtk/src/git.rs +2012 -0
  153. package/temp/rtk/src/go_cmd.rs +592 -0
  154. package/temp/rtk/src/golangci_cmd.rs +254 -0
  155. package/temp/rtk/src/grep_cmd.rs +288 -0
  156. package/temp/rtk/src/gt_cmd.rs +810 -0
  157. package/temp/rtk/src/hook_audit_cmd.rs +283 -0
  158. package/temp/rtk/src/hook_check.rs +171 -0
  159. package/temp/rtk/src/init.rs +1859 -0
  160. package/temp/rtk/src/integrity.rs +537 -0
  161. package/temp/rtk/src/json_cmd.rs +231 -0
  162. package/temp/rtk/src/learn/detector.rs +628 -0
  163. package/temp/rtk/src/learn/mod.rs +119 -0
  164. package/temp/rtk/src/learn/report.rs +184 -0
  165. package/temp/rtk/src/lint_cmd.rs +694 -0
  166. package/temp/rtk/src/local_llm.rs +316 -0
  167. package/temp/rtk/src/log_cmd.rs +248 -0
  168. package/temp/rtk/src/ls.rs +324 -0
  169. package/temp/rtk/src/main.rs +2482 -0
  170. package/temp/rtk/src/mypy_cmd.rs +389 -0
  171. package/temp/rtk/src/next_cmd.rs +241 -0
  172. package/temp/rtk/src/npm_cmd.rs +236 -0
  173. package/temp/rtk/src/parser/README.md +267 -0
  174. package/temp/rtk/src/parser/error.rs +46 -0
  175. package/temp/rtk/src/parser/formatter.rs +336 -0
  176. package/temp/rtk/src/parser/mod.rs +311 -0
  177. package/temp/rtk/src/parser/types.rs +119 -0
  178. package/temp/rtk/src/pip_cmd.rs +302 -0
  179. package/temp/rtk/src/playwright_cmd.rs +479 -0
  180. package/temp/rtk/src/pnpm_cmd.rs +573 -0
  181. package/temp/rtk/src/prettier_cmd.rs +221 -0
  182. package/temp/rtk/src/prisma_cmd.rs +482 -0
  183. package/temp/rtk/src/psql_cmd.rs +382 -0
  184. package/temp/rtk/src/pytest_cmd.rs +384 -0
  185. package/temp/rtk/src/read.rs +217 -0
  186. package/temp/rtk/src/rewrite_cmd.rs +50 -0
  187. package/temp/rtk/src/ruff_cmd.rs +402 -0
  188. package/temp/rtk/src/runner.rs +271 -0
  189. package/temp/rtk/src/summary.rs +297 -0
  190. package/temp/rtk/src/tee.rs +405 -0
  191. package/temp/rtk/src/telemetry.rs +248 -0
  192. package/temp/rtk/src/toml_filter.rs +1655 -0
  193. package/temp/rtk/src/tracking.rs +1416 -0
  194. package/temp/rtk/src/tree.rs +209 -0
  195. package/temp/rtk/src/tsc_cmd.rs +259 -0
  196. package/temp/rtk/src/utils.rs +432 -0
  197. package/temp/rtk/src/verify_cmd.rs +47 -0
  198. package/temp/rtk/src/vitest_cmd.rs +385 -0
  199. package/temp/rtk/src/wc_cmd.rs +401 -0
  200. package/temp/rtk/src/wget_cmd.rs +260 -0
  201. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
  202. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
  203. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
  204. package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
  205. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +18 -0
@@ -0,0 +1,402 @@
1
+ use crate::tracking;
2
+ use crate::utils::truncate;
3
+ use anyhow::{Context, Result};
4
+ use serde::Deserialize;
5
+ use std::collections::HashMap;
6
+ use std::process::Command;
7
+
8
+ #[derive(Debug, Deserialize)]
9
+ struct RuffLocation {
10
+ row: usize,
11
+ column: usize,
12
+ }
13
+
14
+ #[derive(Debug, Deserialize)]
15
+ struct RuffFix {
16
+ #[allow(dead_code)]
17
+ applicability: Option<String>,
18
+ }
19
+
20
+ #[derive(Debug, Deserialize)]
21
+ struct RuffDiagnostic {
22
+ code: String,
23
+ message: String,
24
+ location: RuffLocation,
25
+ #[allow(dead_code)]
26
+ end_location: Option<RuffLocation>,
27
+ filename: String,
28
+ fix: Option<RuffFix>,
29
+ }
30
+
31
+ pub fn run(args: &[String], verbose: u8) -> Result<()> {
32
+ let timer = tracking::TimedExecution::start();
33
+
34
+ // Detect subcommand: check, format, or version
35
+ let is_check = args.is_empty()
36
+ || args[0] == "check"
37
+ || (!args[0].starts_with('-') && args[0] != "format" && args[0] != "version");
38
+
39
+ let is_format = args.iter().any(|a| a == "format");
40
+
41
+ let mut cmd = Command::new("ruff");
42
+
43
+ if is_check {
44
+ // Force JSON output for check command
45
+ if !args.contains(&"--output-format".to_string()) {
46
+ cmd.arg("check").arg("--output-format=json");
47
+ } else {
48
+ cmd.arg("check");
49
+ }
50
+
51
+ // Add user arguments (skip "check" if it was the first arg)
52
+ let start_idx = if !args.is_empty() && args[0] == "check" {
53
+ 1
54
+ } else {
55
+ 0
56
+ };
57
+ for arg in &args[start_idx..] {
58
+ cmd.arg(arg);
59
+ }
60
+
61
+ // Default to current directory if no path specified
62
+ if args
63
+ .iter()
64
+ .skip(start_idx)
65
+ .all(|a| a.starts_with('-') || a.contains('='))
66
+ {
67
+ cmd.arg(".");
68
+ }
69
+ } else {
70
+ // Format or other commands - pass through
71
+ for arg in args {
72
+ cmd.arg(arg);
73
+ }
74
+ }
75
+
76
+ if verbose > 0 {
77
+ eprintln!("Running: ruff {}", args.join(" "));
78
+ }
79
+
80
+ let output = cmd
81
+ .output()
82
+ .context("Failed to run ruff. Is it installed? Try: pip install ruff")?;
83
+
84
+ let stdout = String::from_utf8_lossy(&output.stdout);
85
+ let stderr = String::from_utf8_lossy(&output.stderr);
86
+ let raw = format!("{}\n{}", stdout, stderr);
87
+
88
+ let filtered = if is_check && !stdout.trim().is_empty() {
89
+ filter_ruff_check_json(&stdout)
90
+ } else if is_format {
91
+ filter_ruff_format(&raw)
92
+ } else {
93
+ // Fallback for other commands (version, etc.)
94
+ raw.trim().to_string()
95
+ };
96
+
97
+ println!("{}", filtered);
98
+
99
+ timer.track(
100
+ &format!("ruff {}", args.join(" ")),
101
+ &format!("rtk ruff {}", args.join(" ")),
102
+ &raw,
103
+ &filtered,
104
+ );
105
+
106
+ // Preserve exit code for CI/CD
107
+ if !output.status.success() {
108
+ std::process::exit(output.status.code().unwrap_or(1));
109
+ }
110
+
111
+ Ok(())
112
+ }
113
+
114
+ /// Filter ruff check JSON output - group by rule and file
115
+ pub fn filter_ruff_check_json(output: &str) -> String {
116
+ let diagnostics: Result<Vec<RuffDiagnostic>, _> = serde_json::from_str(output);
117
+
118
+ let diagnostics = match diagnostics {
119
+ Ok(d) => d,
120
+ Err(e) => {
121
+ // Fallback if JSON parsing fails
122
+ return format!(
123
+ "Ruff check (JSON parse failed: {})\n{}",
124
+ e,
125
+ truncate(output, 500)
126
+ );
127
+ }
128
+ };
129
+
130
+ if diagnostics.is_empty() {
131
+ return "✓ Ruff: No issues found".to_string();
132
+ }
133
+
134
+ let total_issues = diagnostics.len();
135
+ let fixable_count = diagnostics.iter().filter(|d| d.fix.is_some()).count();
136
+
137
+ // Count unique files
138
+ let unique_files: std::collections::HashSet<_> =
139
+ diagnostics.iter().map(|d| &d.filename).collect();
140
+ let total_files = unique_files.len();
141
+
142
+ // Group by rule code
143
+ let mut by_rule: HashMap<String, usize> = HashMap::new();
144
+ for diag in &diagnostics {
145
+ *by_rule.entry(diag.code.clone()).or_insert(0) += 1;
146
+ }
147
+
148
+ // Group by file
149
+ let mut by_file: HashMap<&str, usize> = HashMap::new();
150
+ for diag in &diagnostics {
151
+ *by_file.entry(&diag.filename).or_insert(0) += 1;
152
+ }
153
+
154
+ let mut file_counts: Vec<_> = by_file.iter().collect();
155
+ file_counts.sort_by(|a, b| b.1.cmp(a.1));
156
+
157
+ // Build output
158
+ let mut result = String::new();
159
+ result.push_str(&format!(
160
+ "Ruff: {} issues in {} files",
161
+ total_issues, total_files
162
+ ));
163
+
164
+ if fixable_count > 0 {
165
+ result.push_str(&format!(" ({} fixable)", fixable_count));
166
+ }
167
+ result.push('\n');
168
+ result.push_str("═══════════════════════════════════════\n");
169
+
170
+ // Show top rules
171
+ let mut rule_counts: Vec<_> = by_rule.iter().collect();
172
+ rule_counts.sort_by(|a, b| b.1.cmp(a.1));
173
+
174
+ if !rule_counts.is_empty() {
175
+ result.push_str("Top rules:\n");
176
+ for (rule, count) in rule_counts.iter().take(10) {
177
+ result.push_str(&format!(" {} ({}x)\n", rule, count));
178
+ }
179
+ result.push('\n');
180
+ }
181
+
182
+ // Show top files
183
+ result.push_str("Top files:\n");
184
+ for (file, count) in file_counts.iter().take(10) {
185
+ let short_path = compact_path(file);
186
+ result.push_str(&format!(" {} ({} issues)\n", short_path, count));
187
+
188
+ // Show top 3 rules in this file
189
+ let mut file_rules: HashMap<String, usize> = HashMap::new();
190
+ for diag in diagnostics.iter().filter(|d| &d.filename == *file) {
191
+ *file_rules.entry(diag.code.clone()).or_insert(0) += 1;
192
+ }
193
+
194
+ let mut file_rule_counts: Vec<_> = file_rules.iter().collect();
195
+ file_rule_counts.sort_by(|a, b| b.1.cmp(a.1));
196
+
197
+ for (rule, count) in file_rule_counts.iter().take(3) {
198
+ result.push_str(&format!(" {} ({})\n", rule, count));
199
+ }
200
+ }
201
+
202
+ if file_counts.len() > 10 {
203
+ result.push_str(&format!("\n... +{} more files\n", file_counts.len() - 10));
204
+ }
205
+
206
+ if fixable_count > 0 {
207
+ result.push_str(&format!(
208
+ "\n💡 Run `ruff check --fix` to auto-fix {} issues\n",
209
+ fixable_count
210
+ ));
211
+ }
212
+
213
+ result.trim().to_string()
214
+ }
215
+
216
+ /// Filter ruff format output - show files that need formatting
217
+ pub fn filter_ruff_format(output: &str) -> String {
218
+ let mut files_to_format: Vec<String> = Vec::new();
219
+ let mut files_checked = 0;
220
+
221
+ for line in output.lines() {
222
+ let trimmed = line.trim();
223
+ let lower = trimmed.to_lowercase();
224
+
225
+ // Count "would reformat" lines (check mode) - case insensitive
226
+ if lower.contains("would reformat:") {
227
+ // Extract filename from "Would reformat: path/to/file.py"
228
+ if let Some(filename) = trimmed.split(':').nth(1) {
229
+ files_to_format.push(filename.trim().to_string());
230
+ }
231
+ }
232
+
233
+ // Count total checked files - look for patterns like "3 files left unchanged"
234
+ if lower.contains("left unchanged") {
235
+ // Find "X file(s) left unchanged" pattern specifically
236
+ // Split by comma to handle "2 files would be reformatted, 3 files left unchanged"
237
+ let parts: Vec<&str> = trimmed.split(',').collect();
238
+ for part in parts {
239
+ let part_lower = part.to_lowercase();
240
+ if part_lower.contains("left unchanged") {
241
+ let words: Vec<&str> = part.trim().split_whitespace().collect();
242
+ // Look for number before "file" or "files"
243
+ for (i, word) in words.iter().enumerate() {
244
+ if (word == &"file" || word == &"files") && i > 0 {
245
+ if let Ok(count) = words[i - 1].parse::<usize>() {
246
+ files_checked = count;
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ let output_lower = output.to_lowercase();
258
+
259
+ // Check if all files are formatted
260
+ if files_to_format.is_empty() && output_lower.contains("left unchanged") {
261
+ return "✓ Ruff format: All files formatted correctly".to_string();
262
+ }
263
+
264
+ let mut result = String::new();
265
+
266
+ if output_lower.contains("would reformat") {
267
+ // Check mode: show files that need formatting
268
+ if files_to_format.is_empty() {
269
+ result.push_str("✓ Ruff format: All files formatted correctly\n");
270
+ } else {
271
+ result.push_str(&format!(
272
+ "Ruff format: {} files need formatting\n",
273
+ files_to_format.len()
274
+ ));
275
+ result.push_str("═══════════════════════════════════════\n");
276
+
277
+ for (i, file) in files_to_format.iter().take(10).enumerate() {
278
+ result.push_str(&format!("{}. {}\n", i + 1, compact_path(file)));
279
+ }
280
+
281
+ if files_to_format.len() > 10 {
282
+ result.push_str(&format!(
283
+ "\n... +{} more files\n",
284
+ files_to_format.len() - 10
285
+ ));
286
+ }
287
+
288
+ if files_checked > 0 {
289
+ result.push_str(&format!("\n✓ {} files already formatted\n", files_checked));
290
+ }
291
+
292
+ result.push_str("\n💡 Run `ruff format` to format these files\n");
293
+ }
294
+ } else {
295
+ // Write mode or other output - show summary
296
+ result.push_str(output.trim());
297
+ }
298
+
299
+ result.trim().to_string()
300
+ }
301
+
302
+ /// Compact file path (remove common prefixes)
303
+ fn compact_path(path: &str) -> String {
304
+ let path = path.replace('\\', "/");
305
+
306
+ if let Some(pos) = path.rfind("/src/") {
307
+ format!("src/{}", &path[pos + 5..])
308
+ } else if let Some(pos) = path.rfind("/lib/") {
309
+ format!("lib/{}", &path[pos + 5..])
310
+ } else if let Some(pos) = path.rfind("/tests/") {
311
+ format!("tests/{}", &path[pos + 7..])
312
+ } else if let Some(pos) = path.rfind('/') {
313
+ path[pos + 1..].to_string()
314
+ } else {
315
+ path
316
+ }
317
+ }
318
+
319
+ #[cfg(test)]
320
+ mod tests {
321
+ use super::*;
322
+
323
+ #[test]
324
+ fn test_filter_ruff_check_no_issues() {
325
+ let output = "[]";
326
+ let result = filter_ruff_check_json(output);
327
+ assert!(result.contains("✓ Ruff"));
328
+ assert!(result.contains("No issues found"));
329
+ }
330
+
331
+ #[test]
332
+ fn test_filter_ruff_check_with_issues() {
333
+ let output = r#"[
334
+ {
335
+ "code": "F401",
336
+ "message": "`os` imported but unused",
337
+ "location": {"row": 1, "column": 8},
338
+ "end_location": {"row": 1, "column": 10},
339
+ "filename": "src/main.py",
340
+ "fix": {"applicability": "safe"}
341
+ },
342
+ {
343
+ "code": "F401",
344
+ "message": "`sys` imported but unused",
345
+ "location": {"row": 2, "column": 8},
346
+ "end_location": {"row": 2, "column": 11},
347
+ "filename": "src/main.py",
348
+ "fix": null
349
+ },
350
+ {
351
+ "code": "E501",
352
+ "message": "Line too long (100 > 88 characters)",
353
+ "location": {"row": 10, "column": 89},
354
+ "end_location": {"row": 10, "column": 100},
355
+ "filename": "src/utils.py",
356
+ "fix": null
357
+ }
358
+ ]"#;
359
+ let result = filter_ruff_check_json(output);
360
+ assert!(result.contains("3 issues"));
361
+ assert!(result.contains("2 files"));
362
+ assert!(result.contains("1 fixable"));
363
+ assert!(result.contains("F401"));
364
+ assert!(result.contains("E501"));
365
+ assert!(result.contains("main.py"));
366
+ assert!(result.contains("utils.py"));
367
+ }
368
+
369
+ #[test]
370
+ fn test_filter_ruff_format_all_formatted() {
371
+ let output = "5 files left unchanged";
372
+ let result = filter_ruff_format(output);
373
+ assert!(result.contains("✓ Ruff format"));
374
+ assert!(result.contains("All files formatted correctly"));
375
+ }
376
+
377
+ #[test]
378
+ fn test_filter_ruff_format_needs_formatting() {
379
+ let output = r#"Would reformat: src/main.py
380
+ Would reformat: tests/test_utils.py
381
+ 2 files would be reformatted, 3 files left unchanged"#;
382
+ let result = filter_ruff_format(output);
383
+ assert!(result.contains("2 files need formatting"));
384
+ assert!(result.contains("main.py"));
385
+ assert!(result.contains("test_utils.py"));
386
+ assert!(result.contains("3 files already formatted"));
387
+ }
388
+
389
+ #[test]
390
+ fn test_compact_path() {
391
+ assert_eq!(
392
+ compact_path("/Users/foo/project/src/main.py"),
393
+ "src/main.py"
394
+ );
395
+ assert_eq!(compact_path("/home/user/app/lib/utils.py"), "lib/utils.py");
396
+ assert_eq!(
397
+ compact_path("C:\\Users\\foo\\project\\tests\\test.py"),
398
+ "tests/test.py"
399
+ );
400
+ assert_eq!(compact_path("relative/file.py"), "file.py");
401
+ }
402
+ }
@@ -0,0 +1,271 @@
1
+ use crate::tracking;
2
+ use anyhow::{Context, Result};
3
+ use regex::Regex;
4
+ use std::process::{Command, Stdio};
5
+
6
+ /// Run a command and filter output to show only errors/warnings
7
+ pub fn run_err(command: &str, verbose: u8) -> Result<()> {
8
+ let timer = tracking::TimedExecution::start();
9
+
10
+ if verbose > 0 {
11
+ eprintln!("Running: {}", command);
12
+ }
13
+
14
+ let output = if cfg!(target_os = "windows") {
15
+ Command::new("cmd")
16
+ .args(["/C", command])
17
+ .stdout(Stdio::piped())
18
+ .stderr(Stdio::piped())
19
+ .output()
20
+ } else {
21
+ Command::new("sh")
22
+ .args(["-c", command])
23
+ .stdout(Stdio::piped())
24
+ .stderr(Stdio::piped())
25
+ .output()
26
+ }
27
+ .context("Failed to execute command")?;
28
+
29
+ let stdout = String::from_utf8_lossy(&output.stdout);
30
+ let stderr = String::from_utf8_lossy(&output.stderr);
31
+ let raw = format!("{}\n{}", stdout, stderr);
32
+ let filtered = filter_errors(&raw);
33
+ let mut rtk = String::new();
34
+
35
+ if filtered.is_empty() {
36
+ if output.status.success() {
37
+ rtk.push_str("✅ Command completed successfully (no errors)");
38
+ } else {
39
+ rtk.push_str(&format!(
40
+ "❌ Command failed (exit code: {:?})\n",
41
+ output.status.code()
42
+ ));
43
+ let lines: Vec<&str> = raw.lines().collect();
44
+ for line in lines.iter().rev().take(10).rev() {
45
+ rtk.push_str(&format!(" {}\n", line));
46
+ }
47
+ }
48
+ } else {
49
+ rtk.push_str(&filtered);
50
+ }
51
+
52
+ let exit_code = output
53
+ .status
54
+ .code()
55
+ .unwrap_or(if output.status.success() { 0 } else { 1 });
56
+ if let Some(hint) = crate::tee::tee_and_hint(&raw, "err", exit_code) {
57
+ println!("{}\n{}", rtk, hint);
58
+ } else {
59
+ println!("{}", rtk);
60
+ }
61
+ timer.track(command, "rtk run-err", &raw, &rtk);
62
+ Ok(())
63
+ }
64
+
65
+ /// Run tests and show only failures
66
+ pub fn run_test(command: &str, verbose: u8) -> Result<()> {
67
+ let timer = tracking::TimedExecution::start();
68
+
69
+ if verbose > 0 {
70
+ eprintln!("Running tests: {}", command);
71
+ }
72
+
73
+ let output = if cfg!(target_os = "windows") {
74
+ Command::new("cmd")
75
+ .args(["/C", command])
76
+ .stdout(Stdio::piped())
77
+ .stderr(Stdio::piped())
78
+ .output()
79
+ } else {
80
+ Command::new("sh")
81
+ .args(["-c", command])
82
+ .stdout(Stdio::piped())
83
+ .stderr(Stdio::piped())
84
+ .output()
85
+ }
86
+ .context("Failed to execute test command")?;
87
+
88
+ let stdout = String::from_utf8_lossy(&output.stdout);
89
+ let stderr = String::from_utf8_lossy(&output.stderr);
90
+ let raw = format!("{}\n{}", stdout, stderr);
91
+
92
+ let exit_code = output
93
+ .status
94
+ .code()
95
+ .unwrap_or(if output.status.success() { 0 } else { 1 });
96
+ let summary = extract_test_summary(&raw, command);
97
+ if let Some(hint) = crate::tee::tee_and_hint(&raw, "test", exit_code) {
98
+ println!("{}\n{}", summary, hint);
99
+ } else {
100
+ println!("{}", summary);
101
+ }
102
+ timer.track(command, "rtk run-test", &raw, &summary);
103
+ Ok(())
104
+ }
105
+
106
+ fn filter_errors(output: &str) -> String {
107
+ lazy_static::lazy_static! {
108
+ static ref ERROR_PATTERNS: Vec<Regex> = vec![
109
+ // Generic errors
110
+ Regex::new(r"(?i)^.*error[\s:\[].*$").unwrap(),
111
+ Regex::new(r"(?i)^.*\berr\b.*$").unwrap(),
112
+ Regex::new(r"(?i)^.*warning[\s:\[].*$").unwrap(),
113
+ Regex::new(r"(?i)^.*\bwarn\b.*$").unwrap(),
114
+ Regex::new(r"(?i)^.*failed.*$").unwrap(),
115
+ Regex::new(r"(?i)^.*failure.*$").unwrap(),
116
+ Regex::new(r"(?i)^.*exception.*$").unwrap(),
117
+ Regex::new(r"(?i)^.*panic.*$").unwrap(),
118
+ // Rust specific
119
+ Regex::new(r"^error\[E\d+\]:.*$").unwrap(),
120
+ Regex::new(r"^\s*--> .*:\d+:\d+$").unwrap(),
121
+ // Python
122
+ Regex::new(r"^Traceback.*$").unwrap(),
123
+ Regex::new(r#"^\s*File ".*", line \d+.*$"#).unwrap(),
124
+ // JavaScript/TypeScript
125
+ Regex::new(r"^\s*at .*:\d+:\d+.*$").unwrap(),
126
+ // Go
127
+ Regex::new(r"^.*\.go:\d+:.*$").unwrap(),
128
+ ];
129
+ }
130
+
131
+ let mut result = Vec::new();
132
+ let mut in_error_block = false;
133
+ let mut blank_count = 0;
134
+
135
+ for line in output.lines() {
136
+ let is_error_line = ERROR_PATTERNS.iter().any(|p| p.is_match(line));
137
+
138
+ if is_error_line {
139
+ in_error_block = true;
140
+ blank_count = 0;
141
+ result.push(line.to_string());
142
+ } else if in_error_block {
143
+ if line.trim().is_empty() {
144
+ blank_count += 1;
145
+ if blank_count >= 2 {
146
+ in_error_block = false;
147
+ } else {
148
+ result.push(line.to_string());
149
+ }
150
+ } else if line.starts_with(' ') || line.starts_with('\t') {
151
+ // Continuation of error
152
+ result.push(line.to_string());
153
+ blank_count = 0;
154
+ } else {
155
+ in_error_block = false;
156
+ }
157
+ }
158
+ }
159
+
160
+ result.join("\n")
161
+ }
162
+
163
+ fn extract_test_summary(output: &str, command: &str) -> String {
164
+ let mut result = Vec::new();
165
+ let lines: Vec<&str> = output.lines().collect();
166
+
167
+ // Detect test framework
168
+ let is_cargo = command.contains("cargo test");
169
+ let is_pytest = command.contains("pytest");
170
+ let is_jest =
171
+ command.contains("jest") || command.contains("npm test") || command.contains("yarn test");
172
+ let is_go = command.contains("go test");
173
+
174
+ // Collect failures
175
+ let mut failures = Vec::new();
176
+ let mut in_failure = false;
177
+ let mut failure_lines = Vec::new();
178
+
179
+ for line in lines.iter() {
180
+ // Cargo test
181
+ if is_cargo {
182
+ if line.contains("test result:") {
183
+ result.push(line.to_string());
184
+ }
185
+ if line.contains("FAILED") && !line.contains("test result") {
186
+ failures.push(line.to_string());
187
+ }
188
+ if line.starts_with("failures:") {
189
+ in_failure = true;
190
+ }
191
+ if in_failure && line.starts_with(" ") {
192
+ failure_lines.push(line.to_string());
193
+ }
194
+ }
195
+
196
+ // Pytest
197
+ if is_pytest {
198
+ if line.contains(" passed") || line.contains(" failed") || line.contains(" error") {
199
+ result.push(line.to_string());
200
+ }
201
+ if line.contains("FAILED") {
202
+ failures.push(line.to_string());
203
+ }
204
+ }
205
+
206
+ // Jest
207
+ if is_jest {
208
+ if line.contains("Tests:") || line.contains("Test Suites:") {
209
+ result.push(line.to_string());
210
+ }
211
+ if line.contains("✕") || line.contains("FAIL") {
212
+ failures.push(line.to_string());
213
+ }
214
+ }
215
+
216
+ // Go test
217
+ if is_go {
218
+ if line.starts_with("ok") || line.starts_with("FAIL") || line.starts_with("---") {
219
+ result.push(line.to_string());
220
+ }
221
+ if line.contains("FAIL") {
222
+ failures.push(line.to_string());
223
+ }
224
+ }
225
+ }
226
+
227
+ // Build output
228
+ let mut output = String::new();
229
+
230
+ if !failures.is_empty() {
231
+ output.push_str("❌ FAILURES:\n");
232
+ for f in failures.iter().take(10) {
233
+ output.push_str(&format!(" {}\n", f));
234
+ }
235
+ if failures.len() > 10 {
236
+ output.push_str(&format!(" ... +{} more failures\n", failures.len() - 10));
237
+ }
238
+ output.push('\n');
239
+ }
240
+
241
+ if !result.is_empty() {
242
+ output.push_str("📊 SUMMARY:\n");
243
+ for r in &result {
244
+ output.push_str(&format!(" {}\n", r));
245
+ }
246
+ } else {
247
+ // Fallback: show last few lines
248
+ output.push_str("📊 OUTPUT (last 5 lines):\n");
249
+ let start = lines.len().saturating_sub(5);
250
+ for line in &lines[start..] {
251
+ if !line.trim().is_empty() {
252
+ output.push_str(&format!(" {}\n", line));
253
+ }
254
+ }
255
+ }
256
+
257
+ output
258
+ }
259
+
260
+ #[cfg(test)]
261
+ mod tests {
262
+ use super::*;
263
+
264
+ #[test]
265
+ fn test_filter_errors() {
266
+ let output = "info: compiling\nerror: something failed\n at line 10\ninfo: done";
267
+ let filtered = filter_errors(output);
268
+ assert!(filtered.contains("error"));
269
+ assert!(!filtered.contains("info"));
270
+ }
271
+ }