@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,297 +0,0 @@
1
- use crate::tracking;
2
- use crate::utils::truncate;
3
- use anyhow::{Context, Result};
4
- use regex::Regex;
5
- use std::process::{Command, Stdio};
6
-
7
- /// Run a command and provide a heuristic summary
8
- pub fn run(command: &str, verbose: u8) -> Result<()> {
9
- let timer = tracking::TimedExecution::start();
10
-
11
- if verbose > 0 {
12
- eprintln!("Running and summarizing: {}", command);
13
- }
14
-
15
- let output = if cfg!(target_os = "windows") {
16
- Command::new("cmd")
17
- .args(["/C", command])
18
- .stdout(Stdio::piped())
19
- .stderr(Stdio::piped())
20
- .output()
21
- } else {
22
- Command::new("sh")
23
- .args(["-c", command])
24
- .stdout(Stdio::piped())
25
- .stderr(Stdio::piped())
26
- .output()
27
- }
28
- .context("Failed to execute command")?;
29
-
30
- let stdout = String::from_utf8_lossy(&output.stdout);
31
- let stderr = String::from_utf8_lossy(&output.stderr);
32
- let raw = format!("{}\n{}", stdout, stderr);
33
-
34
- let summary = summarize_output(&raw, command, output.status.success());
35
- println!("{}", summary);
36
- timer.track(command, "rtk summary", &raw, &summary);
37
- Ok(())
38
- }
39
-
40
- fn summarize_output(output: &str, command: &str, success: bool) -> String {
41
- let lines: Vec<&str> = output.lines().collect();
42
- let mut result = Vec::new();
43
-
44
- // Status
45
- let status_icon = if success { "✅" } else { "❌" };
46
- result.push(format!(
47
- "{} Command: {}",
48
- status_icon,
49
- truncate(command, 60)
50
- ));
51
- result.push(format!(" {} lines of output", lines.len()));
52
- result.push(String::new());
53
-
54
- // Detect type of output and summarize accordingly
55
- let output_type = detect_output_type(output, command);
56
-
57
- match output_type {
58
- OutputType::TestResults => summarize_tests(output, &mut result),
59
- OutputType::BuildOutput => summarize_build(output, &mut result),
60
- OutputType::LogOutput => summarize_logs_quick(output, &mut result),
61
- OutputType::ListOutput => summarize_list(output, &mut result),
62
- OutputType::JsonOutput => summarize_json(output, &mut result),
63
- OutputType::Generic => summarize_generic(output, &mut result),
64
- }
65
-
66
- result.join("\n")
67
- }
68
-
69
- #[derive(Debug)]
70
- enum OutputType {
71
- TestResults,
72
- BuildOutput,
73
- LogOutput,
74
- ListOutput,
75
- JsonOutput,
76
- Generic,
77
- }
78
-
79
- fn detect_output_type(output: &str, command: &str) -> OutputType {
80
- let cmd_lower = command.to_lowercase();
81
- let out_lower = output.to_lowercase();
82
-
83
- if cmd_lower.contains("test") || out_lower.contains("passed") && out_lower.contains("failed") {
84
- OutputType::TestResults
85
- } else if cmd_lower.contains("build")
86
- || cmd_lower.contains("compile")
87
- || out_lower.contains("compiling")
88
- {
89
- OutputType::BuildOutput
90
- } else if out_lower.contains("error:")
91
- || out_lower.contains("warn:")
92
- || out_lower.contains("[info]")
93
- {
94
- OutputType::LogOutput
95
- } else if output.trim_start().starts_with('{') || output.trim_start().starts_with('[') {
96
- OutputType::JsonOutput
97
- } else if output.lines().all(|l| {
98
- l.len() < 200
99
- && !l
100
- .contains('\t')
101
- .then_some(true)
102
- .unwrap_or(l.split_whitespace().count() < 10)
103
- }) {
104
- OutputType::ListOutput
105
- } else {
106
- OutputType::Generic
107
- }
108
- }
109
-
110
- fn summarize_tests(output: &str, result: &mut Vec<String>) {
111
- result.push("📋 Test Results:".to_string());
112
-
113
- let mut passed = 0;
114
- let mut failed = 0;
115
- let mut skipped = 0;
116
- let mut failures = Vec::new();
117
-
118
- for line in output.lines() {
119
- let lower = line.to_lowercase();
120
- if lower.contains("passed") || lower.contains("✓") || lower.contains("ok") {
121
- // Try to extract number
122
- if let Some(n) = extract_number(&lower, "passed") {
123
- passed = n;
124
- } else {
125
- passed += 1;
126
- }
127
- }
128
- if lower.contains("failed") || lower.contains("✗") || lower.contains("fail") {
129
- if let Some(n) = extract_number(&lower, "failed") {
130
- failed = n;
131
- }
132
- if !line.contains("0 failed") {
133
- failures.push(line.to_string());
134
- }
135
- }
136
- if lower.contains("skipped") || lower.contains("ignored") {
137
- if let Some(n) = extract_number(&lower, "skipped").or(extract_number(&lower, "ignored"))
138
- {
139
- skipped = n;
140
- }
141
- }
142
- }
143
-
144
- result.push(format!(" ✅ {} passed", passed));
145
- if failed > 0 {
146
- result.push(format!(" ❌ {} failed", failed));
147
- }
148
- if skipped > 0 {
149
- result.push(format!(" ⏭️ {} skipped", skipped));
150
- }
151
-
152
- if !failures.is_empty() {
153
- result.push(String::new());
154
- result.push(" Failures:".to_string());
155
- for f in failures.iter().take(5) {
156
- result.push(format!(" • {}", truncate(f, 70)));
157
- }
158
- }
159
- }
160
-
161
- fn summarize_build(output: &str, result: &mut Vec<String>) {
162
- result.push("🔨 Build Summary:".to_string());
163
-
164
- let mut errors = 0;
165
- let mut warnings = 0;
166
- let mut compiled = 0;
167
- let mut error_msgs = Vec::new();
168
-
169
- for line in output.lines() {
170
- let lower = line.to_lowercase();
171
- if lower.contains("error") && !lower.contains("0 error") {
172
- errors += 1;
173
- if error_msgs.len() < 5 {
174
- error_msgs.push(line.to_string());
175
- }
176
- }
177
- if lower.contains("warning") && !lower.contains("0 warning") {
178
- warnings += 1;
179
- }
180
- if lower.contains("compiling") || lower.contains("compiled") {
181
- compiled += 1;
182
- }
183
- }
184
-
185
- if compiled > 0 {
186
- result.push(format!(" 📦 {} crates/files compiled", compiled));
187
- }
188
- if errors > 0 {
189
- result.push(format!(" ❌ {} errors", errors));
190
- }
191
- if warnings > 0 {
192
- result.push(format!(" ⚠️ {} warnings", warnings));
193
- }
194
- if errors == 0 && warnings == 0 {
195
- result.push(" ✅ Build successful".to_string());
196
- }
197
-
198
- if !error_msgs.is_empty() {
199
- result.push(String::new());
200
- result.push(" Errors:".to_string());
201
- for e in &error_msgs {
202
- result.push(format!(" • {}", truncate(e, 70)));
203
- }
204
- }
205
- }
206
-
207
- fn summarize_logs_quick(output: &str, result: &mut Vec<String>) {
208
- result.push("📝 Log Summary:".to_string());
209
-
210
- let mut errors = 0;
211
- let mut warnings = 0;
212
- let mut info = 0;
213
-
214
- for line in output.lines() {
215
- let lower = line.to_lowercase();
216
- if lower.contains("error") || lower.contains("fatal") {
217
- errors += 1;
218
- } else if lower.contains("warn") {
219
- warnings += 1;
220
- } else if lower.contains("info") {
221
- info += 1;
222
- }
223
- }
224
-
225
- result.push(format!(" ❌ {} errors", errors));
226
- result.push(format!(" ⚠️ {} warnings", warnings));
227
- result.push(format!(" ℹ️ {} info", info));
228
- }
229
-
230
- fn summarize_list(output: &str, result: &mut Vec<String>) {
231
- let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
232
- result.push(format!("📋 List ({} items):", lines.len()));
233
-
234
- for line in lines.iter().take(10) {
235
- result.push(format!(" • {}", truncate(line, 70)));
236
- }
237
- if lines.len() > 10 {
238
- result.push(format!(" ... +{} more", lines.len() - 10));
239
- }
240
- }
241
-
242
- fn summarize_json(output: &str, result: &mut Vec<String>) {
243
- result.push("📋 JSON Output:".to_string());
244
-
245
- // Try to parse and show structure
246
- if let Ok(value) = serde_json::from_str::<serde_json::Value>(output) {
247
- match &value {
248
- serde_json::Value::Array(arr) => {
249
- result.push(format!(" Array with {} items", arr.len()));
250
- }
251
- serde_json::Value::Object(obj) => {
252
- result.push(format!(" Object with {} keys:", obj.len()));
253
- for key in obj.keys().take(10) {
254
- result.push(format!(" • {}", key));
255
- }
256
- if obj.len() > 10 {
257
- result.push(format!(" ... +{} more keys", obj.len() - 10));
258
- }
259
- }
260
- _ => {
261
- result.push(format!(" {}", truncate(&value.to_string(), 100)));
262
- }
263
- }
264
- } else {
265
- result.push(" (Invalid JSON)".to_string());
266
- }
267
- }
268
-
269
- fn summarize_generic(output: &str, result: &mut Vec<String>) {
270
- let lines: Vec<&str> = output.lines().collect();
271
-
272
- result.push("📋 Output:".to_string());
273
-
274
- // First few lines
275
- for line in lines.iter().take(5) {
276
- if !line.trim().is_empty() {
277
- result.push(format!(" {}", truncate(line, 75)));
278
- }
279
- }
280
-
281
- if lines.len() > 10 {
282
- result.push(" ...".to_string());
283
- // Last few lines
284
- for line in lines.iter().skip(lines.len() - 3) {
285
- if !line.trim().is_empty() {
286
- result.push(format!(" {}", truncate(line, 75)));
287
- }
288
- }
289
- }
290
- }
291
-
292
- fn extract_number(text: &str, after: &str) -> Option<usize> {
293
- let re = Regex::new(&format!(r"(\d+)\s*{}", after)).ok()?;
294
- re.captures(text)
295
- .and_then(|c| c.get(1))
296
- .and_then(|m| m.as_str().parse().ok())
297
- }
@@ -1,405 +0,0 @@
1
- use crate::config::Config;
2
- use std::path::PathBuf;
3
-
4
- /// Minimum output size to tee (smaller outputs don't need recovery)
5
- const MIN_TEE_SIZE: usize = 500;
6
-
7
- /// Default max files to keep in tee directory
8
- const DEFAULT_MAX_FILES: usize = 20;
9
-
10
- /// Default max file size (1MB)
11
- const DEFAULT_MAX_FILE_SIZE: usize = 1_048_576;
12
-
13
- /// Sanitize a command slug for use in filenames.
14
- /// Replaces non-alphanumeric chars (except underscore/hyphen) with underscore,
15
- /// truncates at 40 chars.
16
- fn sanitize_slug(slug: &str) -> String {
17
- let sanitized: String = slug
18
- .chars()
19
- .map(|c| {
20
- if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
21
- c
22
- } else {
23
- '_'
24
- }
25
- })
26
- .collect();
27
- if sanitized.len() > 40 {
28
- sanitized[..40].to_string()
29
- } else {
30
- sanitized
31
- }
32
- }
33
-
34
- /// Get the tee directory, respecting config and env overrides.
35
- fn get_tee_dir(config: &Config) -> Option<PathBuf> {
36
- // Env var override
37
- if let Ok(dir) = std::env::var("RTK_TEE_DIR") {
38
- return Some(PathBuf::from(dir));
39
- }
40
-
41
- // Config override
42
- if let Some(ref dir) = config.tee.directory {
43
- return Some(dir.clone());
44
- }
45
-
46
- // Default: ~/.local/share/rtk/tee/
47
- dirs::data_local_dir().map(|d| d.join("rtk").join("tee"))
48
- }
49
-
50
- /// Rotate old tee files: keep only the last `max_files`, delete oldest.
51
- fn cleanup_old_files(dir: &std::path::Path, max_files: usize) {
52
- let mut entries: Vec<_> = std::fs::read_dir(dir)
53
- .ok()
54
- .into_iter()
55
- .flatten()
56
- .filter_map(|e| e.ok())
57
- .filter(|e| e.path().extension().is_some_and(|ext| ext == "log"))
58
- .collect();
59
-
60
- if entries.len() <= max_files {
61
- return;
62
- }
63
-
64
- // Sort by filename (which starts with epoch timestamp = chronological)
65
- entries.sort_by_key(|e| e.file_name());
66
-
67
- let to_remove = entries.len() - max_files;
68
- for entry in entries.iter().take(to_remove) {
69
- let _ = std::fs::remove_file(entry.path());
70
- }
71
- }
72
-
73
- /// Check if tee should be skipped based on config, mode, exit code, and size.
74
- /// Returns None if should skip, Some(tee_dir) if should proceed.
75
- fn should_tee(
76
- config: &TeeConfig,
77
- raw_len: usize,
78
- exit_code: i32,
79
- tee_dir: Option<PathBuf>,
80
- ) -> Option<PathBuf> {
81
- if !config.enabled {
82
- return None;
83
- }
84
-
85
- match config.mode {
86
- TeeMode::Never => return None,
87
- TeeMode::Failures => {
88
- if exit_code == 0 {
89
- return None;
90
- }
91
- }
92
- TeeMode::Always => {}
93
- }
94
-
95
- if raw_len < MIN_TEE_SIZE {
96
- return None;
97
- }
98
-
99
- tee_dir
100
- }
101
-
102
- /// Write raw output to a tee file in the given directory.
103
- /// Returns file path on success.
104
- fn write_tee_file(
105
- raw: &str,
106
- command_slug: &str,
107
- tee_dir: &std::path::Path,
108
- max_file_size: usize,
109
- max_files: usize,
110
- ) -> Option<PathBuf> {
111
- std::fs::create_dir_all(tee_dir).ok()?;
112
-
113
- let slug = sanitize_slug(command_slug);
114
- let epoch = std::time::SystemTime::now()
115
- .duration_since(std::time::UNIX_EPOCH)
116
- .ok()?
117
- .as_secs();
118
- let filename = format!("{}_{}.log", epoch, slug);
119
- let filepath = tee_dir.join(filename);
120
-
121
- // Truncate at max_file_size
122
- let content = if raw.len() > max_file_size {
123
- format!(
124
- "{}\n\n--- truncated at {} bytes ---",
125
- &raw[..max_file_size],
126
- max_file_size
127
- )
128
- } else {
129
- raw.to_string()
130
- };
131
-
132
- std::fs::write(&filepath, content).ok()?;
133
-
134
- // Rotate old files
135
- cleanup_old_files(tee_dir, max_files);
136
-
137
- Some(filepath)
138
- }
139
-
140
- /// Write raw output to tee file if conditions are met.
141
- /// Returns file path on success, None if skipped/failed.
142
- pub fn tee_raw(raw: &str, command_slug: &str, exit_code: i32) -> Option<PathBuf> {
143
- // Check RTK_TEE=0 env override (disable)
144
- if std::env::var("RTK_TEE").ok().as_deref() == Some("0") {
145
- return None;
146
- }
147
-
148
- let config = Config::load().ok()?;
149
- let tee_dir = get_tee_dir(&config)?;
150
-
151
- let tee_dir = should_tee(&config.tee, raw.len(), exit_code, Some(tee_dir))?;
152
-
153
- write_tee_file(
154
- raw,
155
- command_slug,
156
- &tee_dir,
157
- config.tee.max_file_size,
158
- config.tee.max_files,
159
- )
160
- }
161
-
162
- /// Format the hint line with ~ shorthand for home directory.
163
- fn format_hint(path: &std::path::Path) -> String {
164
- let display = if let Some(home) = dirs::home_dir() {
165
- if let Ok(relative) = path.strip_prefix(&home) {
166
- format!("~/{}", relative.display())
167
- } else {
168
- path.display().to_string()
169
- }
170
- } else {
171
- path.display().to_string()
172
- };
173
-
174
- format!("[full output: {}]", display)
175
- }
176
-
177
- /// Convenience: tee + format hint in one call.
178
- /// Returns hint string if file was written, None if skipped.
179
- pub fn tee_and_hint(raw: &str, command_slug: &str, exit_code: i32) -> Option<String> {
180
- let path = tee_raw(raw, command_slug, exit_code)?;
181
- Some(format_hint(&path))
182
- }
183
-
184
- /// TeeMode controls when tee writes files.
185
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
186
- #[serde(rename_all = "lowercase")]
187
- pub enum TeeMode {
188
- Failures,
189
- Always,
190
- Never,
191
- }
192
-
193
- impl Default for TeeMode {
194
- fn default() -> Self {
195
- Self::Failures
196
- }
197
- }
198
-
199
- /// Configuration for the tee feature.
200
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
201
- pub struct TeeConfig {
202
- pub enabled: bool,
203
- pub mode: TeeMode,
204
- pub max_files: usize,
205
- pub max_file_size: usize,
206
- #[serde(skip_serializing_if = "Option::is_none")]
207
- pub directory: Option<PathBuf>,
208
- }
209
-
210
- impl Default for TeeConfig {
211
- fn default() -> Self {
212
- Self {
213
- enabled: true,
214
- mode: TeeMode::default(),
215
- max_files: DEFAULT_MAX_FILES,
216
- max_file_size: DEFAULT_MAX_FILE_SIZE,
217
- directory: None,
218
- }
219
- }
220
- }
221
-
222
- #[cfg(test)]
223
- mod tests {
224
- use super::*;
225
- use std::fs;
226
-
227
- #[test]
228
- fn test_sanitize_slug() {
229
- assert_eq!(sanitize_slug("cargo_test"), "cargo_test");
230
- assert_eq!(sanitize_slug("cargo test"), "cargo_test");
231
- assert_eq!(sanitize_slug("cargo-test"), "cargo-test");
232
- assert_eq!(sanitize_slug("go/test/./pkg"), "go_test___pkg");
233
- // Truncate at 40
234
- let long = "a".repeat(50);
235
- assert_eq!(sanitize_slug(&long).len(), 40);
236
- }
237
-
238
- #[test]
239
- fn test_should_tee_disabled() {
240
- let config = TeeConfig {
241
- enabled: false,
242
- ..TeeConfig::default()
243
- };
244
- let dir = PathBuf::from("/tmp/tee");
245
- assert!(should_tee(&config, 1000, 1, Some(dir)).is_none());
246
- }
247
-
248
- #[test]
249
- fn test_should_tee_never_mode() {
250
- let config = TeeConfig {
251
- mode: TeeMode::Never,
252
- ..TeeConfig::default()
253
- };
254
- let dir = PathBuf::from("/tmp/tee");
255
- assert!(should_tee(&config, 1000, 1, Some(dir)).is_none());
256
- }
257
-
258
- #[test]
259
- fn test_should_tee_skip_small_output() {
260
- let config = TeeConfig::default();
261
- let dir = PathBuf::from("/tmp/tee");
262
- // Below MIN_TEE_SIZE (500)
263
- assert!(should_tee(&config, 100, 1, Some(dir)).is_none());
264
- }
265
-
266
- #[test]
267
- fn test_should_tee_skip_success_in_failures_mode() {
268
- let config = TeeConfig::default(); // mode = Failures
269
- let dir = PathBuf::from("/tmp/tee");
270
- assert!(should_tee(&config, 1000, 0, Some(dir)).is_none());
271
- }
272
-
273
- #[test]
274
- fn test_should_tee_proceed_on_failure() {
275
- let config = TeeConfig::default(); // mode = Failures
276
- let dir = PathBuf::from("/tmp/tee");
277
- assert!(should_tee(&config, 1000, 1, Some(dir)).is_some());
278
- }
279
-
280
- #[test]
281
- fn test_should_tee_always_mode_success() {
282
- let config = TeeConfig {
283
- mode: TeeMode::Always,
284
- ..TeeConfig::default()
285
- };
286
- let dir = PathBuf::from("/tmp/tee");
287
- assert!(should_tee(&config, 1000, 0, Some(dir)).is_some());
288
- }
289
-
290
- #[test]
291
- fn test_write_tee_file_creates_file() {
292
- let tmpdir = tempfile::tempdir().unwrap();
293
- let content = "error: test failed\n".repeat(50);
294
- let result = write_tee_file(
295
- &content,
296
- "cargo_test",
297
- tmpdir.path(),
298
- DEFAULT_MAX_FILE_SIZE,
299
- 20,
300
- );
301
- assert!(result.is_some());
302
-
303
- let path = result.unwrap();
304
- assert!(path.exists());
305
- let written = fs::read_to_string(&path).unwrap();
306
- assert!(written.contains("error: test failed"));
307
- }
308
-
309
- #[test]
310
- fn test_write_tee_file_truncation() {
311
- let tmpdir = tempfile::tempdir().unwrap();
312
- let big_output = "x".repeat(2000);
313
- // Set max_file_size to 1000 bytes
314
- let result = write_tee_file(&big_output, "test", tmpdir.path(), 1000, 20);
315
- assert!(result.is_some());
316
-
317
- let path = result.unwrap();
318
- let content = fs::read_to_string(&path).unwrap();
319
- assert!(content.contains("--- truncated at 1000 bytes ---"));
320
- assert!(content.len() < 2000);
321
- }
322
-
323
- #[test]
324
- fn test_cleanup_old_files() {
325
- let tmpdir = tempfile::tempdir().unwrap();
326
- let dir = tmpdir.path();
327
-
328
- // Create 25 .log files
329
- for i in 0..25 {
330
- let filename = format!("{:010}_{}.log", 1000000 + i, "test");
331
- fs::write(dir.join(&filename), "content").unwrap();
332
- }
333
-
334
- cleanup_old_files(dir, 20);
335
-
336
- let remaining: Vec<_> = fs::read_dir(dir).unwrap().filter_map(|e| e.ok()).collect();
337
- assert_eq!(remaining.len(), 20);
338
-
339
- // Oldest 5 should be removed
340
- for i in 0..5 {
341
- let filename = format!("{:010}_{}.log", 1000000 + i, "test");
342
- assert!(!dir.join(&filename).exists());
343
- }
344
- // Newest 20 should remain
345
- for i in 5..25 {
346
- let filename = format!("{:010}_{}.log", 1000000 + i, "test");
347
- assert!(dir.join(&filename).exists());
348
- }
349
- }
350
-
351
- #[test]
352
- fn test_format_hint() {
353
- let path = PathBuf::from("/tmp/rtk/tee/123_cargo_test.log");
354
- let hint = format_hint(&path);
355
- assert!(hint.starts_with("[full output: "));
356
- assert!(hint.ends_with(']'));
357
- assert!(hint.contains("123_cargo_test.log"));
358
- }
359
-
360
- #[test]
361
- fn test_tee_config_default() {
362
- let config = TeeConfig::default();
363
- assert!(config.enabled);
364
- assert_eq!(config.mode, TeeMode::Failures);
365
- assert_eq!(config.max_files, 20);
366
- assert_eq!(config.max_file_size, 1_048_576);
367
- assert!(config.directory.is_none());
368
- }
369
-
370
- #[test]
371
- fn test_tee_config_deserialize() {
372
- let toml_str = r#"
373
- enabled = true
374
- mode = "always"
375
- max_files = 10
376
- max_file_size = 524288
377
- directory = "/tmp/rtk-tee"
378
- "#;
379
- let config: TeeConfig = toml::from_str(toml_str).unwrap();
380
- assert!(config.enabled);
381
- assert_eq!(config.mode, TeeMode::Always);
382
- assert_eq!(config.max_files, 10);
383
- assert_eq!(config.max_file_size, 524288);
384
- assert_eq!(config.directory, Some(PathBuf::from("/tmp/rtk-tee")));
385
-
386
- // Round-trip
387
- let serialized = toml::to_string_pretty(&config).unwrap();
388
- let deserialized: TeeConfig = toml::from_str(&serialized).unwrap();
389
- assert_eq!(deserialized.mode, TeeMode::Always);
390
- assert_eq!(deserialized.max_files, 10);
391
- }
392
-
393
- #[test]
394
- fn test_tee_mode_serde() {
395
- // Test all modes via JSON
396
- let mode: TeeMode = serde_json::from_str(r#""always""#).unwrap();
397
- assert_eq!(mode, TeeMode::Always);
398
-
399
- let mode: TeeMode = serde_json::from_str(r#""failures""#).unwrap();
400
- assert_eq!(mode, TeeMode::Failures);
401
-
402
- let mode: TeeMode = serde_json::from_str(r#""never""#).unwrap();
403
- assert_eq!(mode, TeeMode::Never);
404
- }
405
- }