@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,254 +0,0 @@
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 Position {
10
- #[serde(rename = "Filename")]
11
- filename: String,
12
- #[serde(rename = "Line")]
13
- line: usize,
14
- #[serde(rename = "Column")]
15
- column: usize,
16
- }
17
-
18
- #[derive(Debug, Deserialize)]
19
- struct Issue {
20
- #[serde(rename = "FromLinter")]
21
- from_linter: String,
22
- #[serde(rename = "Text")]
23
- text: String,
24
- #[serde(rename = "Pos")]
25
- pos: Position,
26
- }
27
-
28
- #[derive(Debug, Deserialize)]
29
- struct GolangciOutput {
30
- #[serde(rename = "Issues")]
31
- issues: Vec<Issue>,
32
- }
33
-
34
- pub fn run(args: &[String], verbose: u8) -> Result<()> {
35
- let timer = tracking::TimedExecution::start();
36
-
37
- let mut cmd = Command::new("golangci-lint");
38
-
39
- // Force JSON output
40
- let has_format = args
41
- .iter()
42
- .any(|a| a == "--out-format" || a.starts_with("--out-format="));
43
-
44
- if !has_format {
45
- cmd.arg("run").arg("--out-format=json");
46
- } else {
47
- cmd.arg("run");
48
- }
49
-
50
- for arg in args {
51
- cmd.arg(arg);
52
- }
53
-
54
- if verbose > 0 {
55
- eprintln!("Running: golangci-lint run --out-format=json");
56
- }
57
-
58
- let output = cmd.output().context(
59
- "Failed to run golangci-lint. Is it installed? Try: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest",
60
- )?;
61
-
62
- let stdout = String::from_utf8_lossy(&output.stdout);
63
- let stderr = String::from_utf8_lossy(&output.stderr);
64
- let raw = format!("{}\n{}", stdout, stderr);
65
-
66
- let filtered = filter_golangci_json(&stdout);
67
-
68
- println!("{}", filtered);
69
-
70
- // Include stderr if present (config errors, etc.)
71
- if !stderr.trim().is_empty() && verbose > 0 {
72
- eprintln!("{}", stderr.trim());
73
- }
74
-
75
- timer.track(
76
- &format!("golangci-lint {}", args.join(" ")),
77
- &format!("rtk golangci-lint {}", args.join(" ")),
78
- &raw,
79
- &filtered,
80
- );
81
-
82
- // golangci-lint returns exit code 1 when issues found (expected behavior)
83
- // Don't exit with error code in that case
84
- Ok(())
85
- }
86
-
87
- /// Filter golangci-lint JSON output - group by linter and file
88
- fn filter_golangci_json(output: &str) -> String {
89
- let result: Result<GolangciOutput, _> = serde_json::from_str(output);
90
-
91
- let golangci_output = match result {
92
- Ok(o) => o,
93
- Err(e) => {
94
- // Fallback if JSON parsing fails
95
- return format!(
96
- "golangci-lint (JSON parse failed: {})\n{}",
97
- e,
98
- truncate(output, 500)
99
- );
100
- }
101
- };
102
-
103
- let issues = golangci_output.issues;
104
-
105
- if issues.is_empty() {
106
- return "✓ golangci-lint: No issues found".to_string();
107
- }
108
-
109
- let total_issues = issues.len();
110
-
111
- // Count unique files
112
- let unique_files: std::collections::HashSet<_> =
113
- issues.iter().map(|i| &i.pos.filename).collect();
114
- let total_files = unique_files.len();
115
-
116
- // Group by linter
117
- let mut by_linter: HashMap<String, usize> = HashMap::new();
118
- for issue in &issues {
119
- *by_linter.entry(issue.from_linter.clone()).or_insert(0) += 1;
120
- }
121
-
122
- // Group by file
123
- let mut by_file: HashMap<&str, usize> = HashMap::new();
124
- for issue in &issues {
125
- *by_file.entry(&issue.pos.filename).or_insert(0) += 1;
126
- }
127
-
128
- let mut file_counts: Vec<_> = by_file.iter().collect();
129
- file_counts.sort_by(|a, b| b.1.cmp(a.1));
130
-
131
- // Build output
132
- let mut result = String::new();
133
- result.push_str(&format!(
134
- "golangci-lint: {} issues in {} files\n",
135
- total_issues, total_files
136
- ));
137
- result.push_str("═══════════════════════════════════════\n");
138
-
139
- // Show top linters
140
- let mut linter_counts: Vec<_> = by_linter.iter().collect();
141
- linter_counts.sort_by(|a, b| b.1.cmp(a.1));
142
-
143
- if !linter_counts.is_empty() {
144
- result.push_str("Top linters:\n");
145
- for (linter, count) in linter_counts.iter().take(10) {
146
- result.push_str(&format!(" {} ({}x)\n", linter, count));
147
- }
148
- result.push('\n');
149
- }
150
-
151
- // Show top files
152
- result.push_str("Top files:\n");
153
- for (file, count) in file_counts.iter().take(10) {
154
- let short_path = compact_path(file);
155
- result.push_str(&format!(" {} ({} issues)\n", short_path, count));
156
-
157
- // Show top 3 linters in this file
158
- let mut file_linters: HashMap<String, usize> = HashMap::new();
159
- for issue in issues.iter().filter(|i| &i.pos.filename == *file) {
160
- *file_linters.entry(issue.from_linter.clone()).or_insert(0) += 1;
161
- }
162
-
163
- let mut file_linter_counts: Vec<_> = file_linters.iter().collect();
164
- file_linter_counts.sort_by(|a, b| b.1.cmp(a.1));
165
-
166
- for (linter, count) in file_linter_counts.iter().take(3) {
167
- result.push_str(&format!(" {} ({})\n", linter, count));
168
- }
169
- }
170
-
171
- if file_counts.len() > 10 {
172
- result.push_str(&format!("\n... +{} more files\n", file_counts.len() - 10));
173
- }
174
-
175
- result.trim().to_string()
176
- }
177
-
178
- /// Compact file path (remove common prefixes)
179
- fn compact_path(path: &str) -> String {
180
- let path = path.replace('\\', "/");
181
-
182
- if let Some(pos) = path.rfind("/pkg/") {
183
- format!("pkg/{}", &path[pos + 5..])
184
- } else if let Some(pos) = path.rfind("/cmd/") {
185
- format!("cmd/{}", &path[pos + 5..])
186
- } else if let Some(pos) = path.rfind("/internal/") {
187
- format!("internal/{}", &path[pos + 10..])
188
- } else if let Some(pos) = path.rfind('/') {
189
- path[pos + 1..].to_string()
190
- } else {
191
- path
192
- }
193
- }
194
-
195
- #[cfg(test)]
196
- mod tests {
197
- use super::*;
198
-
199
- #[test]
200
- fn test_filter_golangci_no_issues() {
201
- let output = r#"{"Issues":[]}"#;
202
- let result = filter_golangci_json(output);
203
- assert!(result.contains("✓ golangci-lint"));
204
- assert!(result.contains("No issues found"));
205
- }
206
-
207
- #[test]
208
- fn test_filter_golangci_with_issues() {
209
- let output = r#"{
210
- "Issues": [
211
- {
212
- "FromLinter": "errcheck",
213
- "Text": "Error return value not checked",
214
- "Pos": {"Filename": "main.go", "Line": 42, "Column": 5}
215
- },
216
- {
217
- "FromLinter": "errcheck",
218
- "Text": "Error return value not checked",
219
- "Pos": {"Filename": "main.go", "Line": 50, "Column": 10}
220
- },
221
- {
222
- "FromLinter": "gosimple",
223
- "Text": "Should use strings.Contains",
224
- "Pos": {"Filename": "utils.go", "Line": 15, "Column": 2}
225
- }
226
- ]
227
- }"#;
228
-
229
- let result = filter_golangci_json(output);
230
- assert!(result.contains("3 issues"));
231
- assert!(result.contains("2 files"));
232
- assert!(result.contains("errcheck"));
233
- assert!(result.contains("gosimple"));
234
- assert!(result.contains("main.go"));
235
- assert!(result.contains("utils.go"));
236
- }
237
-
238
- #[test]
239
- fn test_compact_path() {
240
- assert_eq!(
241
- compact_path("/Users/foo/project/pkg/handler/server.go"),
242
- "pkg/handler/server.go"
243
- );
244
- assert_eq!(
245
- compact_path("/home/user/app/cmd/main/main.go"),
246
- "cmd/main/main.go"
247
- );
248
- assert_eq!(
249
- compact_path("/project/internal/config/loader.go"),
250
- "internal/config/loader.go"
251
- );
252
- assert_eq!(compact_path("relative/file.go"), "file.go");
253
- }
254
- }
@@ -1,288 +0,0 @@
1
- use crate::tracking;
2
- use anyhow::{Context, Result};
3
- use regex::Regex;
4
- use std::collections::HashMap;
5
- use std::process::Command;
6
-
7
- pub fn run(
8
- pattern: &str,
9
- path: &str,
10
- max_line_len: usize,
11
- max_results: usize,
12
- context_only: bool,
13
- file_type: Option<&str>,
14
- extra_args: &[String],
15
- verbose: u8,
16
- ) -> Result<()> {
17
- let timer = tracking::TimedExecution::start();
18
-
19
- if verbose > 0 {
20
- eprintln!("grep: '{}' in {}", pattern, path);
21
- }
22
-
23
- // Fix: convert BRE alternation \| → | for rg (which uses PCRE-style regex)
24
- let rg_pattern = pattern.replace(r"\|", "|");
25
-
26
- let mut rg_cmd = Command::new("rg");
27
- rg_cmd.args(["-n", "--no-heading", &rg_pattern, path]);
28
-
29
- if let Some(ft) = file_type {
30
- rg_cmd.arg("--type").arg(ft);
31
- }
32
-
33
- for arg in extra_args {
34
- // Fix: skip grep-ism -r flag (rg is recursive by default; rg -r means --replace)
35
- if arg == "-r" || arg == "--recursive" {
36
- continue;
37
- }
38
- rg_cmd.arg(arg);
39
- }
40
-
41
- let output = rg_cmd
42
- .output()
43
- .or_else(|_| Command::new("grep").args(["-rn", pattern, path]).output())
44
- .context("grep/rg failed")?;
45
-
46
- let stdout = String::from_utf8_lossy(&output.stdout);
47
- let exit_code = output.status.code().unwrap_or(1);
48
-
49
- let raw_output = stdout.to_string();
50
-
51
- if stdout.trim().is_empty() {
52
- // Show stderr for errors (bad regex, missing file, etc.)
53
- if exit_code == 2 {
54
- let stderr = String::from_utf8_lossy(&output.stderr);
55
- if !stderr.trim().is_empty() {
56
- eprintln!("{}", stderr.trim());
57
- }
58
- }
59
- let msg = format!("🔍 0 for '{}'", pattern);
60
- println!("{}", msg);
61
- timer.track(
62
- &format!("grep -rn '{}' {}", pattern, path),
63
- "rtk grep",
64
- &raw_output,
65
- &msg,
66
- );
67
- if exit_code != 0 {
68
- std::process::exit(exit_code);
69
- }
70
- return Ok(());
71
- }
72
-
73
- let mut by_file: HashMap<String, Vec<(usize, String)>> = HashMap::new();
74
- let mut total = 0;
75
-
76
- for line in stdout.lines() {
77
- let parts: Vec<&str> = line.splitn(3, ':').collect();
78
-
79
- let (file, line_num, content) = if parts.len() == 3 {
80
- let ln = parts[1].parse().unwrap_or(0);
81
- (parts[0].to_string(), ln, parts[2])
82
- } else if parts.len() == 2 {
83
- let ln = parts[0].parse().unwrap_or(0);
84
- (path.to_string(), ln, parts[1])
85
- } else {
86
- continue;
87
- };
88
-
89
- total += 1;
90
- let cleaned = clean_line(content, max_line_len, context_only, pattern);
91
- by_file.entry(file).or_default().push((line_num, cleaned));
92
- }
93
-
94
- let mut rtk_output = String::new();
95
- rtk_output.push_str(&format!("🔍 {} in {}F:\n\n", total, by_file.len()));
96
-
97
- let mut shown = 0;
98
- let mut files: Vec<_> = by_file.iter().collect();
99
- files.sort_by_key(|(f, _)| *f);
100
-
101
- for (file, matches) in files {
102
- if shown >= max_results {
103
- break;
104
- }
105
-
106
- let file_display = compact_path(file);
107
- rtk_output.push_str(&format!("📄 {} ({}):\n", file_display, matches.len()));
108
-
109
- for (line_num, content) in matches.iter().take(10) {
110
- rtk_output.push_str(&format!(" {:>4}: {}\n", line_num, content));
111
- shown += 1;
112
- if shown >= max_results {
113
- break;
114
- }
115
- }
116
-
117
- if matches.len() > 10 {
118
- rtk_output.push_str(&format!(" +{}\n", matches.len() - 10));
119
- }
120
- rtk_output.push('\n');
121
- }
122
-
123
- if total > shown {
124
- rtk_output.push_str(&format!("... +{}\n", total - shown));
125
- }
126
-
127
- print!("{}", rtk_output);
128
- timer.track(
129
- &format!("grep -rn '{}' {}", pattern, path),
130
- "rtk grep",
131
- &raw_output,
132
- &rtk_output,
133
- );
134
-
135
- if exit_code != 0 {
136
- std::process::exit(exit_code);
137
- }
138
-
139
- Ok(())
140
- }
141
-
142
- fn clean_line(line: &str, max_len: usize, context_only: bool, pattern: &str) -> String {
143
- let trimmed = line.trim();
144
-
145
- if context_only {
146
- if let Ok(re) = Regex::new(&format!("(?i).{{0,20}}{}.*", regex::escape(pattern))) {
147
- if let Some(m) = re.find(trimmed) {
148
- let matched = m.as_str();
149
- if matched.len() <= max_len {
150
- return matched.to_string();
151
- }
152
- }
153
- }
154
- }
155
-
156
- if trimmed.len() <= max_len {
157
- trimmed.to_string()
158
- } else {
159
- let lower = trimmed.to_lowercase();
160
- let pattern_lower = pattern.to_lowercase();
161
-
162
- if let Some(pos) = lower.find(&pattern_lower) {
163
- let char_pos = lower[..pos].chars().count();
164
- let chars: Vec<char> = trimmed.chars().collect();
165
- let char_len = chars.len();
166
-
167
- let start = char_pos.saturating_sub(max_len / 3);
168
- let end = (start + max_len).min(char_len);
169
- let start = if end == char_len {
170
- end.saturating_sub(max_len)
171
- } else {
172
- start
173
- };
174
-
175
- let slice: String = chars[start..end].iter().collect();
176
- if start > 0 && end < char_len {
177
- format!("...{}...", slice)
178
- } else if start > 0 {
179
- format!("...{}", slice)
180
- } else {
181
- format!("{}...", slice)
182
- }
183
- } else {
184
- let t: String = trimmed.chars().take(max_len - 3).collect();
185
- format!("{}...", t)
186
- }
187
- }
188
- }
189
-
190
- fn compact_path(path: &str) -> String {
191
- if path.len() <= 50 {
192
- return path.to_string();
193
- }
194
-
195
- let parts: Vec<&str> = path.split('/').collect();
196
- if parts.len() <= 3 {
197
- return path.to_string();
198
- }
199
-
200
- format!(
201
- "{}/.../{}/{}",
202
- parts[0],
203
- parts[parts.len() - 2],
204
- parts[parts.len() - 1]
205
- )
206
- }
207
-
208
- #[cfg(test)]
209
- mod tests {
210
- use super::*;
211
-
212
- #[test]
213
- fn test_clean_line() {
214
- let line = " const result = someFunction();";
215
- let cleaned = clean_line(line, 50, false, "result");
216
- assert!(!cleaned.starts_with(' '));
217
- assert!(cleaned.len() <= 50);
218
- }
219
-
220
- #[test]
221
- fn test_compact_path() {
222
- let path = "/Users/patrick/dev/project/src/components/Button.tsx";
223
- let compact = compact_path(path);
224
- assert!(compact.len() <= 60);
225
- }
226
-
227
- #[test]
228
- fn test_extra_args_accepted() {
229
- // Test that the function signature accepts extra_args
230
- // This is a compile-time test - if it compiles, the signature is correct
231
- let _extra: Vec<String> = vec!["-i".to_string(), "-A".to_string(), "3".to_string()];
232
- // No need to actually run - we're verifying the parameter exists
233
- }
234
-
235
- #[test]
236
- fn test_clean_line_multibyte() {
237
- // Thai text that exceeds max_len in bytes
238
- let line = " สวัสดีครับ นี่คือข้อความที่ยาวมากสำหรับทดสอบ ";
239
- let cleaned = clean_line(line, 20, false, "ครับ");
240
- // Should not panic
241
- assert!(!cleaned.is_empty());
242
- }
243
-
244
- #[test]
245
- fn test_clean_line_emoji() {
246
- let line = "🎉🎊🎈🎁🎂🎄 some text 🎃🎆🎇✨";
247
- let cleaned = clean_line(line, 15, false, "text");
248
- assert!(!cleaned.is_empty());
249
- }
250
-
251
- // Fix: BRE \| alternation is translated to PCRE | for rg
252
- #[test]
253
- fn test_bre_alternation_translated() {
254
- let pattern = r"fn foo\|pub.*bar";
255
- let rg_pattern = pattern.replace(r"\|", "|");
256
- assert_eq!(rg_pattern, "fn foo|pub.*bar");
257
- }
258
-
259
- // Fix: -r flag (grep recursive) is stripped from extra_args (rg is recursive by default)
260
- #[test]
261
- fn test_recursive_flag_stripped() {
262
- let extra_args: Vec<String> = vec!["-r".to_string(), "-i".to_string()];
263
- let filtered: Vec<&String> = extra_args
264
- .iter()
265
- .filter(|a| *a != "-r" && *a != "--recursive")
266
- .collect();
267
- assert_eq!(filtered.len(), 1);
268
- assert_eq!(filtered[0], "-i");
269
- }
270
-
271
- // Verify line numbers are always enabled in rg invocation (grep_cmd.rs:24).
272
- // The -n/--line-numbers clap flag in main.rs is a no-op accepted for compat.
273
- #[test]
274
- fn test_rg_always_has_line_numbers() {
275
- // grep_cmd::run() always passes "-n" to rg (line 24).
276
- // This test documents that -n is built-in, so the clap flag is safe to ignore.
277
- let mut cmd = std::process::Command::new("rg");
278
- cmd.args(["-n", "--no-heading", "NONEXISTENT_PATTERN_12345", "."]);
279
- // If rg is available, it should accept -n without error (exit 1 = no match, not error)
280
- if let Ok(output) = cmd.output() {
281
- assert!(
282
- output.status.code() == Some(1) || output.status.success(),
283
- "rg -n should be accepted"
284
- );
285
- }
286
- // If rg is not installed, skip gracefully (test still passes)
287
- }
288
- }