@hasna/terminal 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/dist/App.js +404 -0
  2. package/dist/Browse.js +79 -0
  3. package/dist/FuzzyPicker.js +47 -0
  4. package/dist/Onboarding.js +51 -0
  5. package/dist/Spinner.js +12 -0
  6. package/dist/StatusBar.js +49 -0
  7. package/dist/ai.js +322 -0
  8. package/dist/cache.js +41 -0
  9. package/dist/cli.js +64 -16
  10. package/dist/command-rewriter.js +64 -0
  11. package/dist/command-validator.js +86 -0
  12. package/dist/compression.js +107 -0
  13. package/dist/context-hints.js +275 -0
  14. package/dist/diff-cache.js +107 -0
  15. package/dist/discover.js +212 -0
  16. package/dist/economy.js +123 -0
  17. package/dist/expand-store.js +38 -0
  18. package/dist/file-cache.js +72 -0
  19. package/dist/file-index.js +62 -0
  20. package/dist/history.js +62 -0
  21. package/dist/lazy-executor.js +54 -0
  22. package/dist/line-dedup.js +59 -0
  23. package/dist/loop-detector.js +75 -0
  24. package/dist/mcp/install.js +98 -0
  25. package/dist/mcp/server.js +569 -0
  26. package/dist/noise-filter.js +86 -0
  27. package/dist/output-processor.js +129 -0
  28. package/dist/output-router.js +41 -0
  29. package/dist/output-store.js +111 -0
  30. package/dist/parsers/base.js +2 -0
  31. package/dist/parsers/build.js +64 -0
  32. package/dist/parsers/errors.js +101 -0
  33. package/dist/parsers/files.js +78 -0
  34. package/dist/parsers/git.js +99 -0
  35. package/dist/parsers/index.js +48 -0
  36. package/dist/parsers/tests.js +89 -0
  37. package/dist/providers/anthropic.js +39 -0
  38. package/dist/providers/base.js +4 -0
  39. package/dist/providers/cerebras.js +95 -0
  40. package/dist/providers/groq.js +95 -0
  41. package/dist/providers/index.js +73 -0
  42. package/dist/providers/xai.js +95 -0
  43. package/dist/recipes/model.js +20 -0
  44. package/dist/recipes/storage.js +136 -0
  45. package/dist/search/content-search.js +68 -0
  46. package/dist/search/file-search.js +61 -0
  47. package/dist/search/filters.js +34 -0
  48. package/dist/search/index.js +5 -0
  49. package/dist/search/semantic.js +320 -0
  50. package/dist/session-boot.js +59 -0
  51. package/dist/session-context.js +55 -0
  52. package/dist/sessions-db.js +173 -0
  53. package/dist/smart-display.js +286 -0
  54. package/dist/snapshots.js +51 -0
  55. package/dist/supervisor.js +112 -0
  56. package/dist/test-watchlist.js +131 -0
  57. package/dist/tool-profiles.js +122 -0
  58. package/dist/tree.js +94 -0
  59. package/dist/usage-cache.js +65 -0
  60. package/package.json +8 -1
  61. package/src/ai.ts +8 -0
  62. package/src/cli.tsx +57 -18
  63. package/src/output-processor.ts +6 -1
  64. package/src/output-store.ts +58 -12
  65. package/src/tool-profiles.ts +139 -0
  66. package/.claude/scheduled_tasks.lock +0 -1
  67. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
  68. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
  69. package/CONTRIBUTING.md +0 -80
  70. package/benchmarks/benchmark.mjs +0 -115
  71. package/imported_modules.txt +0 -0
  72. package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
  73. package/temp/rtk/.claude/agents/debugger.md +0 -519
  74. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
  75. package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
  76. package/temp/rtk/.claude/agents/technical-writer.md +0 -355
  77. package/temp/rtk/.claude/commands/diagnose.md +0 -352
  78. package/temp/rtk/.claude/commands/test-routing.md +0 -362
  79. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
  80. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
  81. package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
  82. package/temp/rtk/.claude/rules/cli-testing.md +0 -526
  83. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
  84. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
  85. package/temp/rtk/.claude/skills/performance.md +0 -435
  86. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
  87. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
  88. package/temp/rtk/.claude/skills/repo-recap.md +0 -206
  89. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
  90. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
  91. package/temp/rtk/.claude/skills/security-guardian.md +0 -503
  92. package/temp/rtk/.claude/skills/ship.md +0 -404
  93. package/temp/rtk/.github/workflows/benchmark.yml +0 -34
  94. package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
  95. package/temp/rtk/.github/workflows/release-please.yml +0 -51
  96. package/temp/rtk/.github/workflows/release.yml +0 -343
  97. package/temp/rtk/.github/workflows/security-check.yml +0 -135
  98. package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
  99. package/temp/rtk/.release-please-manifest.json +0 -3
  100. package/temp/rtk/ARCHITECTURE.md +0 -1491
  101. package/temp/rtk/CHANGELOG.md +0 -640
  102. package/temp/rtk/CLAUDE.md +0 -605
  103. package/temp/rtk/CONTRIBUTING.md +0 -199
  104. package/temp/rtk/Cargo.lock +0 -1668
  105. package/temp/rtk/Cargo.toml +0 -64
  106. package/temp/rtk/Formula/rtk.rb +0 -43
  107. package/temp/rtk/INSTALL.md +0 -390
  108. package/temp/rtk/LICENSE +0 -21
  109. package/temp/rtk/README.md +0 -386
  110. package/temp/rtk/README_es.md +0 -159
  111. package/temp/rtk/README_fr.md +0 -197
  112. package/temp/rtk/README_ja.md +0 -159
  113. package/temp/rtk/README_ko.md +0 -159
  114. package/temp/rtk/README_zh.md +0 -167
  115. package/temp/rtk/ROADMAP.md +0 -15
  116. package/temp/rtk/SECURITY.md +0 -217
  117. package/temp/rtk/TEST_EXEC_TIME.md +0 -102
  118. package/temp/rtk/build.rs +0 -57
  119. package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
  120. package/temp/rtk/docs/FEATURES.md +0 -1410
  121. package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
  122. package/temp/rtk/docs/filter-workflow.md +0 -102
  123. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  124. package/temp/rtk/docs/tracking.md +0 -583
  125. package/temp/rtk/hooks/opencode-rtk.ts +0 -39
  126. package/temp/rtk/hooks/rtk-awareness.md +0 -29
  127. package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
  128. package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
  129. package/temp/rtk/install.sh +0 -124
  130. package/temp/rtk/release-please-config.json +0 -10
  131. package/temp/rtk/scripts/benchmark.sh +0 -592
  132. package/temp/rtk/scripts/check-installation.sh +0 -162
  133. package/temp/rtk/scripts/install-local.sh +0 -37
  134. package/temp/rtk/scripts/rtk-economics.sh +0 -137
  135. package/temp/rtk/scripts/test-all.sh +0 -561
  136. package/temp/rtk/scripts/test-aristote.sh +0 -227
  137. package/temp/rtk/scripts/test-tracking.sh +0 -79
  138. package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
  139. package/temp/rtk/scripts/validate-docs.sh +0 -73
  140. package/temp/rtk/src/aws_cmd.rs +0 -880
  141. package/temp/rtk/src/binlog.rs +0 -1645
  142. package/temp/rtk/src/cargo_cmd.rs +0 -1727
  143. package/temp/rtk/src/cc_economics.rs +0 -1157
  144. package/temp/rtk/src/ccusage.rs +0 -340
  145. package/temp/rtk/src/config.rs +0 -187
  146. package/temp/rtk/src/container.rs +0 -855
  147. package/temp/rtk/src/curl_cmd.rs +0 -134
  148. package/temp/rtk/src/deps.rs +0 -268
  149. package/temp/rtk/src/diff_cmd.rs +0 -367
  150. package/temp/rtk/src/discover/mod.rs +0 -274
  151. package/temp/rtk/src/discover/provider.rs +0 -388
  152. package/temp/rtk/src/discover/registry.rs +0 -2022
  153. package/temp/rtk/src/discover/report.rs +0 -202
  154. package/temp/rtk/src/discover/rules.rs +0 -667
  155. package/temp/rtk/src/display_helpers.rs +0 -402
  156. package/temp/rtk/src/dotnet_cmd.rs +0 -1771
  157. package/temp/rtk/src/dotnet_format_report.rs +0 -133
  158. package/temp/rtk/src/dotnet_trx.rs +0 -593
  159. package/temp/rtk/src/env_cmd.rs +0 -204
  160. package/temp/rtk/src/filter.rs +0 -462
  161. package/temp/rtk/src/filters/README.md +0 -52
  162. package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
  163. package/temp/rtk/src/filters/basedpyright.toml +0 -47
  164. package/temp/rtk/src/filters/biome.toml +0 -45
  165. package/temp/rtk/src/filters/brew-install.toml +0 -37
  166. package/temp/rtk/src/filters/composer-install.toml +0 -40
  167. package/temp/rtk/src/filters/df.toml +0 -16
  168. package/temp/rtk/src/filters/dotnet-build.toml +0 -64
  169. package/temp/rtk/src/filters/du.toml +0 -16
  170. package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
  171. package/temp/rtk/src/filters/gcc.toml +0 -49
  172. package/temp/rtk/src/filters/gcloud.toml +0 -22
  173. package/temp/rtk/src/filters/hadolint.toml +0 -24
  174. package/temp/rtk/src/filters/helm.toml +0 -29
  175. package/temp/rtk/src/filters/iptables.toml +0 -27
  176. package/temp/rtk/src/filters/jj.toml +0 -28
  177. package/temp/rtk/src/filters/jq.toml +0 -24
  178. package/temp/rtk/src/filters/make.toml +0 -41
  179. package/temp/rtk/src/filters/markdownlint.toml +0 -24
  180. package/temp/rtk/src/filters/mix-compile.toml +0 -27
  181. package/temp/rtk/src/filters/mix-format.toml +0 -15
  182. package/temp/rtk/src/filters/mvn-build.toml +0 -44
  183. package/temp/rtk/src/filters/oxlint.toml +0 -43
  184. package/temp/rtk/src/filters/ping.toml +0 -63
  185. package/temp/rtk/src/filters/pio-run.toml +0 -40
  186. package/temp/rtk/src/filters/poetry-install.toml +0 -50
  187. package/temp/rtk/src/filters/pre-commit.toml +0 -35
  188. package/temp/rtk/src/filters/ps.toml +0 -16
  189. package/temp/rtk/src/filters/quarto-render.toml +0 -41
  190. package/temp/rtk/src/filters/rsync.toml +0 -48
  191. package/temp/rtk/src/filters/shellcheck.toml +0 -27
  192. package/temp/rtk/src/filters/shopify-theme.toml +0 -29
  193. package/temp/rtk/src/filters/skopeo.toml +0 -45
  194. package/temp/rtk/src/filters/sops.toml +0 -16
  195. package/temp/rtk/src/filters/ssh.toml +0 -44
  196. package/temp/rtk/src/filters/stat.toml +0 -34
  197. package/temp/rtk/src/filters/swift-build.toml +0 -41
  198. package/temp/rtk/src/filters/systemctl-status.toml +0 -33
  199. package/temp/rtk/src/filters/terraform-plan.toml +0 -35
  200. package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
  201. package/temp/rtk/src/filters/tofu-init.toml +0 -38
  202. package/temp/rtk/src/filters/tofu-plan.toml +0 -35
  203. package/temp/rtk/src/filters/tofu-validate.toml +0 -17
  204. package/temp/rtk/src/filters/trunk-build.toml +0 -39
  205. package/temp/rtk/src/filters/ty.toml +0 -50
  206. package/temp/rtk/src/filters/uv-sync.toml +0 -37
  207. package/temp/rtk/src/filters/xcodebuild.toml +0 -99
  208. package/temp/rtk/src/filters/yamllint.toml +0 -25
  209. package/temp/rtk/src/find_cmd.rs +0 -598
  210. package/temp/rtk/src/format_cmd.rs +0 -386
  211. package/temp/rtk/src/gain.rs +0 -723
  212. package/temp/rtk/src/gh_cmd.rs +0 -1651
  213. package/temp/rtk/src/git.rs +0 -2012
  214. package/temp/rtk/src/go_cmd.rs +0 -592
  215. package/temp/rtk/src/golangci_cmd.rs +0 -254
  216. package/temp/rtk/src/grep_cmd.rs +0 -288
  217. package/temp/rtk/src/gt_cmd.rs +0 -810
  218. package/temp/rtk/src/hook_audit_cmd.rs +0 -283
  219. package/temp/rtk/src/hook_check.rs +0 -171
  220. package/temp/rtk/src/init.rs +0 -1859
  221. package/temp/rtk/src/integrity.rs +0 -537
  222. package/temp/rtk/src/json_cmd.rs +0 -231
  223. package/temp/rtk/src/learn/detector.rs +0 -628
  224. package/temp/rtk/src/learn/mod.rs +0 -119
  225. package/temp/rtk/src/learn/report.rs +0 -184
  226. package/temp/rtk/src/lint_cmd.rs +0 -694
  227. package/temp/rtk/src/local_llm.rs +0 -316
  228. package/temp/rtk/src/log_cmd.rs +0 -248
  229. package/temp/rtk/src/ls.rs +0 -324
  230. package/temp/rtk/src/main.rs +0 -2482
  231. package/temp/rtk/src/mypy_cmd.rs +0 -389
  232. package/temp/rtk/src/next_cmd.rs +0 -241
  233. package/temp/rtk/src/npm_cmd.rs +0 -236
  234. package/temp/rtk/src/parser/README.md +0 -267
  235. package/temp/rtk/src/parser/error.rs +0 -46
  236. package/temp/rtk/src/parser/formatter.rs +0 -336
  237. package/temp/rtk/src/parser/mod.rs +0 -311
  238. package/temp/rtk/src/parser/types.rs +0 -119
  239. package/temp/rtk/src/pip_cmd.rs +0 -302
  240. package/temp/rtk/src/playwright_cmd.rs +0 -479
  241. package/temp/rtk/src/pnpm_cmd.rs +0 -573
  242. package/temp/rtk/src/prettier_cmd.rs +0 -221
  243. package/temp/rtk/src/prisma_cmd.rs +0 -482
  244. package/temp/rtk/src/psql_cmd.rs +0 -382
  245. package/temp/rtk/src/pytest_cmd.rs +0 -384
  246. package/temp/rtk/src/read.rs +0 -217
  247. package/temp/rtk/src/rewrite_cmd.rs +0 -50
  248. package/temp/rtk/src/ruff_cmd.rs +0 -402
  249. package/temp/rtk/src/runner.rs +0 -271
  250. package/temp/rtk/src/summary.rs +0 -297
  251. package/temp/rtk/src/tee.rs +0 -405
  252. package/temp/rtk/src/telemetry.rs +0 -248
  253. package/temp/rtk/src/toml_filter.rs +0 -1655
  254. package/temp/rtk/src/tracking.rs +0 -1416
  255. package/temp/rtk/src/tree.rs +0 -209
  256. package/temp/rtk/src/tsc_cmd.rs +0 -259
  257. package/temp/rtk/src/utils.rs +0 -432
  258. package/temp/rtk/src/verify_cmd.rs +0 -47
  259. package/temp/rtk/src/vitest_cmd.rs +0 -385
  260. package/temp/rtk/src/wc_cmd.rs +0 -401
  261. package/temp/rtk/src/wget_cmd.rs +0 -260
  262. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
  263. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
  264. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
  265. package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
  266. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
  267. package/tsconfig.json +0 -15
@@ -1,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
- }