@hasna/terminal 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/dist/cli.js +29 -12
  2. package/package.json +1 -1
  3. package/src/ai.ts +50 -36
  4. package/src/cli.tsx +29 -12
  5. package/src/context-hints.ts +89 -0
  6. package/src/discover.ts +238 -0
  7. package/src/economy.ts +53 -0
  8. package/src/output-store.ts +65 -0
  9. package/src/providers/index.ts +4 -4
  10. package/src/sessions-db.ts +81 -0
  11. package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
  12. package/temp/rtk/.claude/agents/debugger.md +519 -0
  13. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
  14. package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
  15. package/temp/rtk/.claude/agents/technical-writer.md +355 -0
  16. package/temp/rtk/.claude/commands/diagnose.md +352 -0
  17. package/temp/rtk/.claude/commands/test-routing.md +362 -0
  18. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
  19. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
  20. package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
  21. package/temp/rtk/.claude/rules/cli-testing.md +526 -0
  22. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
  23. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
  24. package/temp/rtk/.claude/skills/performance.md +435 -0
  25. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
  26. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
  27. package/temp/rtk/.claude/skills/repo-recap.md +206 -0
  28. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
  29. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
  30. package/temp/rtk/.claude/skills/security-guardian.md +503 -0
  31. package/temp/rtk/.claude/skills/ship.md +404 -0
  32. package/temp/rtk/.github/workflows/benchmark.yml +34 -0
  33. package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
  34. package/temp/rtk/.github/workflows/release-please.yml +51 -0
  35. package/temp/rtk/.github/workflows/release.yml +343 -0
  36. package/temp/rtk/.github/workflows/security-check.yml +135 -0
  37. package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
  38. package/temp/rtk/.release-please-manifest.json +3 -0
  39. package/temp/rtk/ARCHITECTURE.md +1491 -0
  40. package/temp/rtk/CHANGELOG.md +640 -0
  41. package/temp/rtk/CLAUDE.md +605 -0
  42. package/temp/rtk/CONTRIBUTING.md +199 -0
  43. package/temp/rtk/Cargo.lock +1668 -0
  44. package/temp/rtk/Cargo.toml +64 -0
  45. package/temp/rtk/Formula/rtk.rb +43 -0
  46. package/temp/rtk/INSTALL.md +390 -0
  47. package/temp/rtk/LICENSE +21 -0
  48. package/temp/rtk/README.md +386 -0
  49. package/temp/rtk/README_es.md +159 -0
  50. package/temp/rtk/README_fr.md +197 -0
  51. package/temp/rtk/README_ja.md +159 -0
  52. package/temp/rtk/README_ko.md +159 -0
  53. package/temp/rtk/README_zh.md +167 -0
  54. package/temp/rtk/ROADMAP.md +15 -0
  55. package/temp/rtk/SECURITY.md +217 -0
  56. package/temp/rtk/TEST_EXEC_TIME.md +102 -0
  57. package/temp/rtk/build.rs +57 -0
  58. package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
  59. package/temp/rtk/docs/FEATURES.md +1410 -0
  60. package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
  61. package/temp/rtk/docs/filter-workflow.md +102 -0
  62. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  63. package/temp/rtk/docs/tracking.md +583 -0
  64. package/temp/rtk/hooks/opencode-rtk.ts +39 -0
  65. package/temp/rtk/hooks/rtk-awareness.md +29 -0
  66. package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
  67. package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
  68. package/temp/rtk/install.sh +124 -0
  69. package/temp/rtk/release-please-config.json +10 -0
  70. package/temp/rtk/scripts/benchmark.sh +592 -0
  71. package/temp/rtk/scripts/check-installation.sh +162 -0
  72. package/temp/rtk/scripts/install-local.sh +37 -0
  73. package/temp/rtk/scripts/rtk-economics.sh +137 -0
  74. package/temp/rtk/scripts/test-all.sh +561 -0
  75. package/temp/rtk/scripts/test-aristote.sh +227 -0
  76. package/temp/rtk/scripts/test-tracking.sh +79 -0
  77. package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
  78. package/temp/rtk/scripts/validate-docs.sh +73 -0
  79. package/temp/rtk/src/aws_cmd.rs +880 -0
  80. package/temp/rtk/src/binlog.rs +1645 -0
  81. package/temp/rtk/src/cargo_cmd.rs +1727 -0
  82. package/temp/rtk/src/cc_economics.rs +1157 -0
  83. package/temp/rtk/src/ccusage.rs +340 -0
  84. package/temp/rtk/src/config.rs +187 -0
  85. package/temp/rtk/src/container.rs +855 -0
  86. package/temp/rtk/src/curl_cmd.rs +134 -0
  87. package/temp/rtk/src/deps.rs +268 -0
  88. package/temp/rtk/src/diff_cmd.rs +367 -0
  89. package/temp/rtk/src/discover/mod.rs +274 -0
  90. package/temp/rtk/src/discover/provider.rs +388 -0
  91. package/temp/rtk/src/discover/registry.rs +2022 -0
  92. package/temp/rtk/src/discover/report.rs +202 -0
  93. package/temp/rtk/src/discover/rules.rs +667 -0
  94. package/temp/rtk/src/display_helpers.rs +402 -0
  95. package/temp/rtk/src/dotnet_cmd.rs +1771 -0
  96. package/temp/rtk/src/dotnet_format_report.rs +133 -0
  97. package/temp/rtk/src/dotnet_trx.rs +593 -0
  98. package/temp/rtk/src/env_cmd.rs +204 -0
  99. package/temp/rtk/src/filter.rs +462 -0
  100. package/temp/rtk/src/filters/README.md +52 -0
  101. package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
  102. package/temp/rtk/src/filters/basedpyright.toml +47 -0
  103. package/temp/rtk/src/filters/biome.toml +45 -0
  104. package/temp/rtk/src/filters/brew-install.toml +37 -0
  105. package/temp/rtk/src/filters/composer-install.toml +40 -0
  106. package/temp/rtk/src/filters/df.toml +16 -0
  107. package/temp/rtk/src/filters/dotnet-build.toml +64 -0
  108. package/temp/rtk/src/filters/du.toml +16 -0
  109. package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
  110. package/temp/rtk/src/filters/gcc.toml +49 -0
  111. package/temp/rtk/src/filters/gcloud.toml +22 -0
  112. package/temp/rtk/src/filters/hadolint.toml +24 -0
  113. package/temp/rtk/src/filters/helm.toml +29 -0
  114. package/temp/rtk/src/filters/iptables.toml +27 -0
  115. package/temp/rtk/src/filters/jj.toml +28 -0
  116. package/temp/rtk/src/filters/jq.toml +24 -0
  117. package/temp/rtk/src/filters/make.toml +41 -0
  118. package/temp/rtk/src/filters/markdownlint.toml +24 -0
  119. package/temp/rtk/src/filters/mix-compile.toml +27 -0
  120. package/temp/rtk/src/filters/mix-format.toml +15 -0
  121. package/temp/rtk/src/filters/mvn-build.toml +44 -0
  122. package/temp/rtk/src/filters/oxlint.toml +43 -0
  123. package/temp/rtk/src/filters/ping.toml +63 -0
  124. package/temp/rtk/src/filters/pio-run.toml +40 -0
  125. package/temp/rtk/src/filters/poetry-install.toml +50 -0
  126. package/temp/rtk/src/filters/pre-commit.toml +35 -0
  127. package/temp/rtk/src/filters/ps.toml +16 -0
  128. package/temp/rtk/src/filters/quarto-render.toml +41 -0
  129. package/temp/rtk/src/filters/rsync.toml +48 -0
  130. package/temp/rtk/src/filters/shellcheck.toml +27 -0
  131. package/temp/rtk/src/filters/shopify-theme.toml +29 -0
  132. package/temp/rtk/src/filters/skopeo.toml +45 -0
  133. package/temp/rtk/src/filters/sops.toml +16 -0
  134. package/temp/rtk/src/filters/ssh.toml +44 -0
  135. package/temp/rtk/src/filters/stat.toml +34 -0
  136. package/temp/rtk/src/filters/swift-build.toml +41 -0
  137. package/temp/rtk/src/filters/systemctl-status.toml +33 -0
  138. package/temp/rtk/src/filters/terraform-plan.toml +35 -0
  139. package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
  140. package/temp/rtk/src/filters/tofu-init.toml +38 -0
  141. package/temp/rtk/src/filters/tofu-plan.toml +35 -0
  142. package/temp/rtk/src/filters/tofu-validate.toml +17 -0
  143. package/temp/rtk/src/filters/trunk-build.toml +39 -0
  144. package/temp/rtk/src/filters/ty.toml +50 -0
  145. package/temp/rtk/src/filters/uv-sync.toml +37 -0
  146. package/temp/rtk/src/filters/xcodebuild.toml +99 -0
  147. package/temp/rtk/src/filters/yamllint.toml +25 -0
  148. package/temp/rtk/src/find_cmd.rs +598 -0
  149. package/temp/rtk/src/format_cmd.rs +386 -0
  150. package/temp/rtk/src/gain.rs +723 -0
  151. package/temp/rtk/src/gh_cmd.rs +1651 -0
  152. package/temp/rtk/src/git.rs +2012 -0
  153. package/temp/rtk/src/go_cmd.rs +592 -0
  154. package/temp/rtk/src/golangci_cmd.rs +254 -0
  155. package/temp/rtk/src/grep_cmd.rs +288 -0
  156. package/temp/rtk/src/gt_cmd.rs +810 -0
  157. package/temp/rtk/src/hook_audit_cmd.rs +283 -0
  158. package/temp/rtk/src/hook_check.rs +171 -0
  159. package/temp/rtk/src/init.rs +1859 -0
  160. package/temp/rtk/src/integrity.rs +537 -0
  161. package/temp/rtk/src/json_cmd.rs +231 -0
  162. package/temp/rtk/src/learn/detector.rs +628 -0
  163. package/temp/rtk/src/learn/mod.rs +119 -0
  164. package/temp/rtk/src/learn/report.rs +184 -0
  165. package/temp/rtk/src/lint_cmd.rs +694 -0
  166. package/temp/rtk/src/local_llm.rs +316 -0
  167. package/temp/rtk/src/log_cmd.rs +248 -0
  168. package/temp/rtk/src/ls.rs +324 -0
  169. package/temp/rtk/src/main.rs +2482 -0
  170. package/temp/rtk/src/mypy_cmd.rs +389 -0
  171. package/temp/rtk/src/next_cmd.rs +241 -0
  172. package/temp/rtk/src/npm_cmd.rs +236 -0
  173. package/temp/rtk/src/parser/README.md +267 -0
  174. package/temp/rtk/src/parser/error.rs +46 -0
  175. package/temp/rtk/src/parser/formatter.rs +336 -0
  176. package/temp/rtk/src/parser/mod.rs +311 -0
  177. package/temp/rtk/src/parser/types.rs +119 -0
  178. package/temp/rtk/src/pip_cmd.rs +302 -0
  179. package/temp/rtk/src/playwright_cmd.rs +479 -0
  180. package/temp/rtk/src/pnpm_cmd.rs +573 -0
  181. package/temp/rtk/src/prettier_cmd.rs +221 -0
  182. package/temp/rtk/src/prisma_cmd.rs +482 -0
  183. package/temp/rtk/src/psql_cmd.rs +382 -0
  184. package/temp/rtk/src/pytest_cmd.rs +384 -0
  185. package/temp/rtk/src/read.rs +217 -0
  186. package/temp/rtk/src/rewrite_cmd.rs +50 -0
  187. package/temp/rtk/src/ruff_cmd.rs +402 -0
  188. package/temp/rtk/src/runner.rs +271 -0
  189. package/temp/rtk/src/summary.rs +297 -0
  190. package/temp/rtk/src/tee.rs +405 -0
  191. package/temp/rtk/src/telemetry.rs +248 -0
  192. package/temp/rtk/src/toml_filter.rs +1655 -0
  193. package/temp/rtk/src/tracking.rs +1416 -0
  194. package/temp/rtk/src/tree.rs +209 -0
  195. package/temp/rtk/src/tsc_cmd.rs +259 -0
  196. package/temp/rtk/src/utils.rs +432 -0
  197. package/temp/rtk/src/verify_cmd.rs +47 -0
  198. package/temp/rtk/src/vitest_cmd.rs +385 -0
  199. package/temp/rtk/src/wc_cmd.rs +401 -0
  200. package/temp/rtk/src/wget_cmd.rs +260 -0
  201. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
  202. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
  203. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
  204. package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
  205. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +18 -0
@@ -0,0 +1,336 @@
1
+ /// Token-efficient formatting trait for canonical types
2
+ use super::types::*;
3
+
4
+ /// Output formatting modes
5
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
6
+ pub enum FormatMode {
7
+ /// Ultra-compact: Summary only (default)
8
+ Compact,
9
+ /// Verbose: Include details
10
+ Verbose,
11
+ /// Ultra-compressed: Symbols and abbreviations
12
+ Ultra,
13
+ }
14
+
15
+ impl FormatMode {
16
+ pub fn from_verbosity(verbosity: u8) -> Self {
17
+ match verbosity {
18
+ 0 => FormatMode::Compact,
19
+ 1 => FormatMode::Verbose,
20
+ _ => FormatMode::Ultra,
21
+ }
22
+ }
23
+ }
24
+
25
+ /// Trait for formatting canonical types into token-efficient strings
26
+ pub trait TokenFormatter {
27
+ /// Format as compact summary (default)
28
+ fn format_compact(&self) -> String;
29
+
30
+ /// Format with details (verbose mode)
31
+ fn format_verbose(&self) -> String;
32
+
33
+ /// Format with symbols (ultra-compressed mode)
34
+ fn format_ultra(&self) -> String;
35
+
36
+ /// Format according to mode
37
+ fn format(&self, mode: FormatMode) -> String {
38
+ match mode {
39
+ FormatMode::Compact => self.format_compact(),
40
+ FormatMode::Verbose => self.format_verbose(),
41
+ FormatMode::Ultra => self.format_ultra(),
42
+ }
43
+ }
44
+ }
45
+
46
+ impl TokenFormatter for TestResult {
47
+ fn format_compact(&self) -> String {
48
+ let mut lines = vec![format!("PASS ({}) FAIL ({})", self.passed, self.failed)];
49
+
50
+ if !self.failures.is_empty() {
51
+ lines.push(String::new());
52
+ for (idx, failure) in self.failures.iter().enumerate().take(5) {
53
+ lines.push(format!("{}. {}", idx + 1, failure.test_name));
54
+ let error_preview: String = failure
55
+ .error_message
56
+ .lines()
57
+ .take(2)
58
+ .collect::<Vec<_>>()
59
+ .join(" ");
60
+ lines.push(format!(" {}", error_preview));
61
+ }
62
+
63
+ if self.failures.len() > 5 {
64
+ lines.push(format!("\n... +{} more failures", self.failures.len() - 5));
65
+ }
66
+ }
67
+
68
+ if let Some(duration) = self.duration_ms {
69
+ lines.push(format!("\nTime: {}ms", duration));
70
+ }
71
+
72
+ lines.join("\n")
73
+ }
74
+
75
+ fn format_verbose(&self) -> String {
76
+ let mut lines = vec![format!(
77
+ "Tests: {} passed, {} failed, {} skipped (total: {})",
78
+ self.passed, self.failed, self.skipped, self.total
79
+ )];
80
+
81
+ if !self.failures.is_empty() {
82
+ lines.push("\nFailures:".to_string());
83
+ for (idx, failure) in self.failures.iter().enumerate() {
84
+ lines.push(format!(
85
+ "\n{}. {} ({})",
86
+ idx + 1,
87
+ failure.test_name,
88
+ failure.file_path
89
+ ));
90
+ lines.push(format!(" {}", failure.error_message));
91
+ if let Some(stack) = &failure.stack_trace {
92
+ let stack_preview: String =
93
+ stack.lines().take(3).collect::<Vec<_>>().join("\n ");
94
+ lines.push(format!(" {}", stack_preview));
95
+ }
96
+ }
97
+ }
98
+
99
+ if let Some(duration) = self.duration_ms {
100
+ lines.push(format!("\nDuration: {}ms", duration));
101
+ }
102
+
103
+ lines.join("\n")
104
+ }
105
+
106
+ fn format_ultra(&self) -> String {
107
+ format!(
108
+ "✓{} ✗{} ⊘{} ({}ms)",
109
+ self.passed,
110
+ self.failed,
111
+ self.skipped,
112
+ self.duration_ms.unwrap_or(0)
113
+ )
114
+ }
115
+ }
116
+
117
+ impl TokenFormatter for LintResult {
118
+ fn format_compact(&self) -> String {
119
+ let mut lines = vec![format!(
120
+ "Errors: {} | Warnings: {} | Files: {}",
121
+ self.errors, self.warnings, self.files_with_issues
122
+ )];
123
+
124
+ if !self.issues.is_empty() {
125
+ // Group by rule_id
126
+ let mut by_rule: std::collections::HashMap<String, Vec<&LintIssue>> =
127
+ std::collections::HashMap::new();
128
+ for issue in &self.issues {
129
+ by_rule
130
+ .entry(issue.rule_id.clone())
131
+ .or_default()
132
+ .push(issue);
133
+ }
134
+
135
+ let mut rules: Vec<_> = by_rule.iter().collect();
136
+ rules.sort_by_key(|(_, issues)| std::cmp::Reverse(issues.len()));
137
+
138
+ lines.push(String::new());
139
+ for (rule, issues) in rules.iter().take(5) {
140
+ lines.push(format!("{}: {} occurrences", rule, issues.len()));
141
+ for issue in issues.iter().take(2) {
142
+ lines.push(format!(" {}:{}", issue.file_path, issue.line));
143
+ }
144
+ }
145
+
146
+ if by_rule.len() > 5 {
147
+ lines.push(format!("\n... +{} more rule violations", by_rule.len() - 5));
148
+ }
149
+ }
150
+
151
+ lines.join("\n")
152
+ }
153
+
154
+ fn format_verbose(&self) -> String {
155
+ let mut lines = vec![format!(
156
+ "Total issues: {} ({} errors, {} warnings) in {} files",
157
+ self.total_issues, self.errors, self.warnings, self.files_with_issues
158
+ )];
159
+
160
+ if !self.issues.is_empty() {
161
+ lines.push("\nIssues:".to_string());
162
+ for issue in self.issues.iter().take(20) {
163
+ let severity_symbol = match issue.severity {
164
+ LintSeverity::Error => "✗",
165
+ LintSeverity::Warning => "⚠",
166
+ LintSeverity::Info => "ℹ",
167
+ };
168
+ lines.push(format!(
169
+ "{} {}:{}:{} [{}] {}",
170
+ severity_symbol,
171
+ issue.file_path,
172
+ issue.line,
173
+ issue.column,
174
+ issue.rule_id,
175
+ issue.message
176
+ ));
177
+ }
178
+
179
+ if self.issues.len() > 20 {
180
+ lines.push(format!("\n... +{} more issues", self.issues.len() - 20));
181
+ }
182
+ }
183
+
184
+ lines.join("\n")
185
+ }
186
+
187
+ fn format_ultra(&self) -> String {
188
+ format!(
189
+ "✗{} ⚠{} 📁{}",
190
+ self.errors, self.warnings, self.files_with_issues
191
+ )
192
+ }
193
+ }
194
+
195
+ impl TokenFormatter for DependencyState {
196
+ fn format_compact(&self) -> String {
197
+ if self.outdated_count == 0 {
198
+ return "All packages up-to-date ✓".to_string();
199
+ }
200
+
201
+ let mut lines = vec![format!(
202
+ "{} outdated packages (of {})",
203
+ self.outdated_count, self.total_packages
204
+ )];
205
+
206
+ for dep in self.dependencies.iter().take(10) {
207
+ if let Some(latest) = &dep.latest_version {
208
+ if &dep.current_version != latest {
209
+ lines.push(format!(
210
+ "{}: {} → {}",
211
+ dep.name, dep.current_version, latest
212
+ ));
213
+ }
214
+ }
215
+ }
216
+
217
+ if self.outdated_count > 10 {
218
+ lines.push(format!("\n... +{} more", self.outdated_count - 10));
219
+ }
220
+
221
+ lines.join("\n")
222
+ }
223
+
224
+ fn format_verbose(&self) -> String {
225
+ let mut lines = vec![format!(
226
+ "Total packages: {} ({} outdated)",
227
+ self.total_packages, self.outdated_count
228
+ )];
229
+
230
+ if self.outdated_count > 0 {
231
+ lines.push("\nOutdated packages:".to_string());
232
+ for dep in &self.dependencies {
233
+ if let Some(latest) = &dep.latest_version {
234
+ if &dep.current_version != latest {
235
+ let dev_marker = if dep.dev_dependency { " (dev)" } else { "" };
236
+ lines.push(format!(
237
+ " {}: {} → {}{}",
238
+ dep.name, dep.current_version, latest, dev_marker
239
+ ));
240
+ if let Some(wanted) = &dep.wanted_version {
241
+ if wanted != latest {
242
+ lines.push(format!(" (wanted: {})", wanted));
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ lines.join("\n")
251
+ }
252
+
253
+ fn format_ultra(&self) -> String {
254
+ format!("📦{} ⬆️{}", self.total_packages, self.outdated_count)
255
+ }
256
+ }
257
+
258
+ impl TokenFormatter for BuildOutput {
259
+ fn format_compact(&self) -> String {
260
+ let status = if self.success { "✓" } else { "✗" };
261
+ let mut lines = vec![format!(
262
+ "{} Build: {} errors, {} warnings",
263
+ status, self.errors, self.warnings
264
+ )];
265
+
266
+ if !self.bundles.is_empty() {
267
+ let total_size: u64 = self.bundles.iter().map(|b| b.size_bytes).sum();
268
+ lines.push(format!(
269
+ "Bundles: {} ({:.1} KB)",
270
+ self.bundles.len(),
271
+ total_size as f64 / 1024.0
272
+ ));
273
+ }
274
+
275
+ if !self.routes.is_empty() {
276
+ lines.push(format!("Routes: {}", self.routes.len()));
277
+ }
278
+
279
+ if let Some(duration) = self.duration_ms {
280
+ lines.push(format!("Time: {}ms", duration));
281
+ }
282
+
283
+ lines.join("\n")
284
+ }
285
+
286
+ fn format_verbose(&self) -> String {
287
+ let status = if self.success { "Success" } else { "Failed" };
288
+ let mut lines = vec![format!(
289
+ "Build {}: {} errors, {} warnings",
290
+ status, self.errors, self.warnings
291
+ )];
292
+
293
+ if !self.bundles.is_empty() {
294
+ lines.push("\nBundles:".to_string());
295
+ for bundle in &self.bundles {
296
+ let gzip_info = bundle
297
+ .gzip_size_bytes
298
+ .map(|gz| format!(" (gzip: {:.1} KB)", gz as f64 / 1024.0))
299
+ .unwrap_or_default();
300
+ lines.push(format!(
301
+ " {}: {:.1} KB{}",
302
+ bundle.name,
303
+ bundle.size_bytes as f64 / 1024.0,
304
+ gzip_info
305
+ ));
306
+ }
307
+ }
308
+
309
+ if !self.routes.is_empty() {
310
+ lines.push("\nRoutes:".to_string());
311
+ for route in self.routes.iter().take(10) {
312
+ lines.push(format!(" {}: {:.1} KB", route.path, route.size_kb));
313
+ }
314
+ if self.routes.len() > 10 {
315
+ lines.push(format!(" ... +{} more routes", self.routes.len() - 10));
316
+ }
317
+ }
318
+
319
+ if let Some(duration) = self.duration_ms {
320
+ lines.push(format!("\nDuration: {}ms", duration));
321
+ }
322
+
323
+ lines.join("\n")
324
+ }
325
+
326
+ fn format_ultra(&self) -> String {
327
+ let status = if self.success { "✓" } else { "✗" };
328
+ format!(
329
+ "{} ✗{} ⚠{} ({}ms)",
330
+ status,
331
+ self.errors,
332
+ self.warnings,
333
+ self.duration_ms.unwrap_or(0)
334
+ )
335
+ }
336
+ }
@@ -0,0 +1,311 @@
1
+ //! Parser infrastructure for tool output transformation
2
+ //!
3
+ //! This module provides a unified interface for parsing tool outputs with graceful degradation:
4
+ //! - Tier 1 (Full): Complete JSON parsing with all fields
5
+ //! - Tier 2 (Degraded): Partial parsing with warnings
6
+ //! - Tier 3 (Passthrough): Raw output truncation with error marker
7
+ //!
8
+ //! The three-tier system ensures RTK never returns false data silently.
9
+
10
+ pub mod error;
11
+ pub mod formatter;
12
+ pub mod types;
13
+
14
+ pub use formatter::{FormatMode, TokenFormatter};
15
+ pub use types::*;
16
+
17
+ /// Parse result with degradation tier
18
+ #[derive(Debug)]
19
+ pub enum ParseResult<T> {
20
+ /// Tier 1: Full parse with complete structured data
21
+ Full(T),
22
+
23
+ /// Tier 2: Degraded parse with partial data and warnings
24
+ Degraded(T, Vec<String>),
25
+
26
+ /// Tier 3: Passthrough - parsing failed, returning truncated raw output
27
+ Passthrough(String),
28
+ }
29
+
30
+ impl<T> ParseResult<T> {
31
+ /// Unwrap the parsed data, panicking on Passthrough
32
+ pub fn unwrap(self) -> T {
33
+ match self {
34
+ ParseResult::Full(data) => data,
35
+ ParseResult::Degraded(data, _) => data,
36
+ ParseResult::Passthrough(_) => panic!("Called unwrap on Passthrough result"),
37
+ }
38
+ }
39
+
40
+ /// Get the tier level (1 = Full, 2 = Degraded, 3 = Passthrough)
41
+ pub fn tier(&self) -> u8 {
42
+ match self {
43
+ ParseResult::Full(_) => 1,
44
+ ParseResult::Degraded(_, _) => 2,
45
+ ParseResult::Passthrough(_) => 3,
46
+ }
47
+ }
48
+
49
+ /// Check if parsing succeeded (Full or Degraded)
50
+ pub fn is_ok(&self) -> bool {
51
+ !matches!(self, ParseResult::Passthrough(_))
52
+ }
53
+
54
+ /// Map the parsed data while preserving tier
55
+ pub fn map<U, F>(self, f: F) -> ParseResult<U>
56
+ where
57
+ F: FnOnce(T) -> U,
58
+ {
59
+ match self {
60
+ ParseResult::Full(data) => ParseResult::Full(f(data)),
61
+ ParseResult::Degraded(data, warnings) => ParseResult::Degraded(f(data), warnings),
62
+ ParseResult::Passthrough(raw) => ParseResult::Passthrough(raw),
63
+ }
64
+ }
65
+
66
+ /// Get warnings if Degraded tier
67
+ pub fn warnings(&self) -> Vec<String> {
68
+ match self {
69
+ ParseResult::Degraded(_, warnings) => warnings.clone(),
70
+ _ => vec![],
71
+ }
72
+ }
73
+ }
74
+
75
+ /// Unified parser trait for tool outputs
76
+ pub trait OutputParser: Sized {
77
+ type Output;
78
+
79
+ /// Parse raw output into structured format
80
+ ///
81
+ /// Implementation should follow three-tier fallback:
82
+ /// 1. Try JSON parsing (if tool supports --json/--format json)
83
+ /// 2. Try regex/text extraction with partial data
84
+ /// 3. Return truncated passthrough with `[RTK:PASSTHROUGH]` marker
85
+ fn parse(input: &str) -> ParseResult<Self::Output>;
86
+
87
+ /// Parse with explicit tier preference (for testing/debugging)
88
+ fn parse_with_tier(input: &str, max_tier: u8) -> ParseResult<Self::Output> {
89
+ let result = Self::parse(input);
90
+ if result.tier() > max_tier {
91
+ // Force degradation to passthrough if exceeds max tier
92
+ return ParseResult::Passthrough(truncate_output(input, 500));
93
+ }
94
+ result
95
+ }
96
+ }
97
+
98
+ /// Truncate output to max length with ellipsis
99
+ pub fn truncate_output(output: &str, max_chars: usize) -> String {
100
+ let chars: Vec<char> = output.chars().collect();
101
+ if chars.len() <= max_chars {
102
+ return output.to_string();
103
+ }
104
+
105
+ let truncated: String = chars[..max_chars].iter().collect();
106
+ format!(
107
+ "{}\n\n[RTK:PASSTHROUGH] Output truncated ({} chars → {} chars)",
108
+ truncated,
109
+ chars.len(),
110
+ max_chars
111
+ )
112
+ }
113
+
114
+ /// Helper to emit degradation warning
115
+ pub fn emit_degradation_warning(tool: &str, reason: &str) {
116
+ eprintln!("[RTK:DEGRADED] {} parser: {}", tool, reason);
117
+ }
118
+
119
+ /// Helper to emit passthrough warning
120
+ pub fn emit_passthrough_warning(tool: &str, reason: &str) {
121
+ eprintln!("[RTK:PASSTHROUGH] {} parser: {}", tool, reason);
122
+ }
123
+
124
+ /// Extract a complete JSON object from input that may have non-JSON prefix (pnpm banner, dotenv messages, etc.)
125
+ ///
126
+ /// Strategy:
127
+ /// 1. Find `"numTotalTests"` (vitest-specific marker) or first standalone `{`
128
+ /// 2. Brace-balance forward to find matching `}`
129
+ /// 3. Return slice containing complete JSON object
130
+ ///
131
+ /// Handles: nested braces, string escapes, pnpm prefixes, dotenv banners
132
+ ///
133
+ /// Returns `None` if no valid JSON object found.
134
+ pub fn extract_json_object(input: &str) -> Option<&str> {
135
+ // Try vitest-specific marker first (most reliable)
136
+ let start_pos = if let Some(pos) = input.find("\"numTotalTests\"") {
137
+ // Walk backward to find opening brace of this object
138
+ input[..pos].rfind('{').unwrap_or(0)
139
+ } else {
140
+ // Fallback: find first `{` on its own line or after whitespace
141
+ let mut found_start = None;
142
+ for (idx, line) in input.lines().enumerate() {
143
+ let trimmed = line.trim();
144
+ if trimmed.starts_with('{') {
145
+ // Calculate byte offset
146
+ found_start = Some(
147
+ input[..]
148
+ .lines()
149
+ .take(idx)
150
+ .map(|l| l.len() + 1)
151
+ .sum::<usize>(),
152
+ );
153
+ break;
154
+ }
155
+ }
156
+ found_start?
157
+ };
158
+
159
+ // Brace-balance forward from start_pos
160
+ let mut depth = 0;
161
+ let mut in_string = false;
162
+ let mut escape_next = false;
163
+ let chars: Vec<char> = input[start_pos..].chars().collect();
164
+
165
+ for (i, &ch) in chars.iter().enumerate() {
166
+ if escape_next {
167
+ escape_next = false;
168
+ continue;
169
+ }
170
+
171
+ match ch {
172
+ '\\' if in_string => escape_next = true,
173
+ '"' => in_string = !in_string,
174
+ '{' if !in_string => depth += 1,
175
+ '}' if !in_string => {
176
+ depth -= 1;
177
+ if depth == 0 {
178
+ // Found matching closing brace
179
+ let end_pos = start_pos + i + 1; // +1 to include the `}`
180
+ return Some(&input[start_pos..end_pos]);
181
+ }
182
+ }
183
+ _ => {}
184
+ }
185
+ }
186
+
187
+ None
188
+ }
189
+
190
+ #[cfg(test)]
191
+ mod tests {
192
+ use super::*;
193
+
194
+ #[test]
195
+ fn test_parse_result_tier() {
196
+ let full: ParseResult<i32> = ParseResult::Full(42);
197
+ assert_eq!(full.tier(), 1);
198
+ assert!(full.is_ok());
199
+
200
+ let degraded: ParseResult<i32> = ParseResult::Degraded(42, vec!["warning".to_string()]);
201
+ assert_eq!(degraded.tier(), 2);
202
+ assert!(degraded.is_ok());
203
+ assert_eq!(degraded.warnings().len(), 1);
204
+
205
+ let passthrough: ParseResult<i32> = ParseResult::Passthrough("raw".to_string());
206
+ assert_eq!(passthrough.tier(), 3);
207
+ assert!(!passthrough.is_ok());
208
+ }
209
+
210
+ #[test]
211
+ fn test_parse_result_map() {
212
+ let full: ParseResult<i32> = ParseResult::Full(42);
213
+ let mapped = full.map(|x| x * 2);
214
+ assert_eq!(mapped.tier(), 1);
215
+ assert_eq!(mapped.unwrap(), 84);
216
+
217
+ let degraded: ParseResult<i32> = ParseResult::Degraded(42, vec!["warn".to_string()]);
218
+ let mapped = degraded.map(|x| x * 2);
219
+ assert_eq!(mapped.tier(), 2);
220
+ assert_eq!(mapped.warnings().len(), 1);
221
+ assert_eq!(mapped.unwrap(), 84);
222
+ }
223
+
224
+ #[test]
225
+ fn test_truncate_output() {
226
+ let short = "hello";
227
+ assert_eq!(truncate_output(short, 10), "hello");
228
+
229
+ let long = "a".repeat(1000);
230
+ let truncated = truncate_output(&long, 100);
231
+ assert!(truncated.contains("[RTK:PASSTHROUGH]"));
232
+ assert!(truncated.contains("1000 chars → 100 chars"));
233
+ }
234
+
235
+ #[test]
236
+ fn test_truncate_output_multibyte() {
237
+ // Thai text: each char is 3 bytes
238
+ let thai = "สวัสดีครับ".repeat(100);
239
+ // Try truncating at a byte offset that might land mid-character
240
+ let result = truncate_output(&thai, 50);
241
+ assert!(result.contains("[RTK:PASSTHROUGH]"));
242
+ // Should be valid UTF-8 (no panic)
243
+ let _ = result.len();
244
+ }
245
+
246
+ #[test]
247
+ fn test_truncate_output_emoji() {
248
+ let emoji = "🎉".repeat(200);
249
+ let result = truncate_output(&emoji, 100);
250
+ assert!(result.contains("[RTK:PASSTHROUGH]"));
251
+ }
252
+
253
+ #[test]
254
+ fn test_extract_json_object_clean() {
255
+ let input = r#"{"numTotalTests": 13, "numPassedTests": 13}"#;
256
+ let extracted = extract_json_object(input);
257
+ assert_eq!(extracted, Some(input));
258
+ }
259
+
260
+ #[test]
261
+ fn test_extract_json_object_with_pnpm_prefix() {
262
+ let input = r#"
263
+ Scope: all 6 workspace projects
264
+ WARN deprecated inflight@1.0.6: This module is not supported
265
+
266
+ {"numTotalTests": 13, "numPassedTests": 13, "numFailedTests": 0}
267
+ "#;
268
+ let extracted = extract_json_object(input).expect("Should extract JSON");
269
+ assert!(extracted.contains("numTotalTests"));
270
+ assert!(extracted.starts_with('{'));
271
+ assert!(extracted.ends_with('}'));
272
+ }
273
+
274
+ #[test]
275
+ fn test_extract_json_object_with_dotenv_prefix() {
276
+ let input = r#"[dotenv] Loading environment variables from .env
277
+ [dotenv] Injected 5 variables
278
+
279
+ {"numTotalTests": 5, "testResults": [{"name": "test.js"}]}
280
+ "#;
281
+ let extracted = extract_json_object(input).expect("Should extract JSON");
282
+ assert!(extracted.contains("numTotalTests"));
283
+ assert!(extracted.contains("testResults"));
284
+ }
285
+
286
+ #[test]
287
+ fn test_extract_json_object_nested_braces() {
288
+ let input = r#"prefix text
289
+ {"numTotalTests": 2, "testResults": [{"name": "test", "data": {"nested": true}}]}
290
+ "#;
291
+ let extracted = extract_json_object(input).expect("Should extract JSON");
292
+ assert!(extracted.contains("\"nested\": true"));
293
+ assert!(extracted.starts_with('{'));
294
+ assert!(extracted.ends_with('}'));
295
+ }
296
+
297
+ #[test]
298
+ fn test_extract_json_object_no_json() {
299
+ let input = "Just plain text with no JSON";
300
+ let extracted = extract_json_object(input);
301
+ assert_eq!(extracted, None);
302
+ }
303
+
304
+ #[test]
305
+ fn test_extract_json_object_string_with_braces() {
306
+ let input = r#"{"numTotalTests": 1, "message": "test {should} not confuse parser"}"#;
307
+ let extracted = extract_json_object(input).expect("Should extract JSON");
308
+ assert!(extracted.contains("test {should} not confuse parser"));
309
+ assert_eq!(extracted, input);
310
+ }
311
+ }