@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.
- package/dist/cli.js +29 -12
- package/package.json +1 -1
- package/src/ai.ts +50 -36
- package/src/cli.tsx +29 -12
- package/src/context-hints.ts +89 -0
- package/src/discover.ts +238 -0
- package/src/economy.ts +53 -0
- package/src/output-store.ts +65 -0
- package/src/providers/index.ts +4 -4
- package/src/sessions-db.ts +81 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
- package/temp/rtk/.claude/agents/debugger.md +519 -0
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
- package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
- package/temp/rtk/.claude/agents/technical-writer.md +355 -0
- package/temp/rtk/.claude/commands/diagnose.md +352 -0
- package/temp/rtk/.claude/commands/test-routing.md +362 -0
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
- package/temp/rtk/.claude/rules/cli-testing.md +526 -0
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
- package/temp/rtk/.claude/skills/performance.md +435 -0
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
- package/temp/rtk/.claude/skills/repo-recap.md +206 -0
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
- package/temp/rtk/.claude/skills/security-guardian.md +503 -0
- package/temp/rtk/.claude/skills/ship.md +404 -0
- package/temp/rtk/.github/workflows/benchmark.yml +34 -0
- package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
- package/temp/rtk/.github/workflows/release-please.yml +51 -0
- package/temp/rtk/.github/workflows/release.yml +343 -0
- package/temp/rtk/.github/workflows/security-check.yml +135 -0
- package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
- package/temp/rtk/.release-please-manifest.json +3 -0
- package/temp/rtk/ARCHITECTURE.md +1491 -0
- package/temp/rtk/CHANGELOG.md +640 -0
- package/temp/rtk/CLAUDE.md +605 -0
- package/temp/rtk/CONTRIBUTING.md +199 -0
- package/temp/rtk/Cargo.lock +1668 -0
- package/temp/rtk/Cargo.toml +64 -0
- package/temp/rtk/Formula/rtk.rb +43 -0
- package/temp/rtk/INSTALL.md +390 -0
- package/temp/rtk/LICENSE +21 -0
- package/temp/rtk/README.md +386 -0
- package/temp/rtk/README_es.md +159 -0
- package/temp/rtk/README_fr.md +197 -0
- package/temp/rtk/README_ja.md +159 -0
- package/temp/rtk/README_ko.md +159 -0
- package/temp/rtk/README_zh.md +167 -0
- package/temp/rtk/ROADMAP.md +15 -0
- package/temp/rtk/SECURITY.md +217 -0
- package/temp/rtk/TEST_EXEC_TIME.md +102 -0
- package/temp/rtk/build.rs +57 -0
- package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
- package/temp/rtk/docs/FEATURES.md +1410 -0
- package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
- package/temp/rtk/docs/filter-workflow.md +102 -0
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +583 -0
- package/temp/rtk/hooks/opencode-rtk.ts +39 -0
- package/temp/rtk/hooks/rtk-awareness.md +29 -0
- package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
- package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
- package/temp/rtk/install.sh +124 -0
- package/temp/rtk/release-please-config.json +10 -0
- package/temp/rtk/scripts/benchmark.sh +592 -0
- package/temp/rtk/scripts/check-installation.sh +162 -0
- package/temp/rtk/scripts/install-local.sh +37 -0
- package/temp/rtk/scripts/rtk-economics.sh +137 -0
- package/temp/rtk/scripts/test-all.sh +561 -0
- package/temp/rtk/scripts/test-aristote.sh +227 -0
- package/temp/rtk/scripts/test-tracking.sh +79 -0
- package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
- package/temp/rtk/scripts/validate-docs.sh +73 -0
- package/temp/rtk/src/aws_cmd.rs +880 -0
- package/temp/rtk/src/binlog.rs +1645 -0
- package/temp/rtk/src/cargo_cmd.rs +1727 -0
- package/temp/rtk/src/cc_economics.rs +1157 -0
- package/temp/rtk/src/ccusage.rs +340 -0
- package/temp/rtk/src/config.rs +187 -0
- package/temp/rtk/src/container.rs +855 -0
- package/temp/rtk/src/curl_cmd.rs +134 -0
- package/temp/rtk/src/deps.rs +268 -0
- package/temp/rtk/src/diff_cmd.rs +367 -0
- package/temp/rtk/src/discover/mod.rs +274 -0
- package/temp/rtk/src/discover/provider.rs +388 -0
- package/temp/rtk/src/discover/registry.rs +2022 -0
- package/temp/rtk/src/discover/report.rs +202 -0
- package/temp/rtk/src/discover/rules.rs +667 -0
- package/temp/rtk/src/display_helpers.rs +402 -0
- package/temp/rtk/src/dotnet_cmd.rs +1771 -0
- package/temp/rtk/src/dotnet_format_report.rs +133 -0
- package/temp/rtk/src/dotnet_trx.rs +593 -0
- package/temp/rtk/src/env_cmd.rs +204 -0
- package/temp/rtk/src/filter.rs +462 -0
- package/temp/rtk/src/filters/README.md +52 -0
- package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
- package/temp/rtk/src/filters/basedpyright.toml +47 -0
- package/temp/rtk/src/filters/biome.toml +45 -0
- package/temp/rtk/src/filters/brew-install.toml +37 -0
- package/temp/rtk/src/filters/composer-install.toml +40 -0
- package/temp/rtk/src/filters/df.toml +16 -0
- package/temp/rtk/src/filters/dotnet-build.toml +64 -0
- package/temp/rtk/src/filters/du.toml +16 -0
- package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
- package/temp/rtk/src/filters/gcc.toml +49 -0
- package/temp/rtk/src/filters/gcloud.toml +22 -0
- package/temp/rtk/src/filters/hadolint.toml +24 -0
- package/temp/rtk/src/filters/helm.toml +29 -0
- package/temp/rtk/src/filters/iptables.toml +27 -0
- package/temp/rtk/src/filters/jj.toml +28 -0
- package/temp/rtk/src/filters/jq.toml +24 -0
- package/temp/rtk/src/filters/make.toml +41 -0
- package/temp/rtk/src/filters/markdownlint.toml +24 -0
- package/temp/rtk/src/filters/mix-compile.toml +27 -0
- package/temp/rtk/src/filters/mix-format.toml +15 -0
- package/temp/rtk/src/filters/mvn-build.toml +44 -0
- package/temp/rtk/src/filters/oxlint.toml +43 -0
- package/temp/rtk/src/filters/ping.toml +63 -0
- package/temp/rtk/src/filters/pio-run.toml +40 -0
- package/temp/rtk/src/filters/poetry-install.toml +50 -0
- package/temp/rtk/src/filters/pre-commit.toml +35 -0
- package/temp/rtk/src/filters/ps.toml +16 -0
- package/temp/rtk/src/filters/quarto-render.toml +41 -0
- package/temp/rtk/src/filters/rsync.toml +48 -0
- package/temp/rtk/src/filters/shellcheck.toml +27 -0
- package/temp/rtk/src/filters/shopify-theme.toml +29 -0
- package/temp/rtk/src/filters/skopeo.toml +45 -0
- package/temp/rtk/src/filters/sops.toml +16 -0
- package/temp/rtk/src/filters/ssh.toml +44 -0
- package/temp/rtk/src/filters/stat.toml +34 -0
- package/temp/rtk/src/filters/swift-build.toml +41 -0
- package/temp/rtk/src/filters/systemctl-status.toml +33 -0
- package/temp/rtk/src/filters/terraform-plan.toml +35 -0
- package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
- package/temp/rtk/src/filters/tofu-init.toml +38 -0
- package/temp/rtk/src/filters/tofu-plan.toml +35 -0
- package/temp/rtk/src/filters/tofu-validate.toml +17 -0
- package/temp/rtk/src/filters/trunk-build.toml +39 -0
- package/temp/rtk/src/filters/ty.toml +50 -0
- package/temp/rtk/src/filters/uv-sync.toml +37 -0
- package/temp/rtk/src/filters/xcodebuild.toml +99 -0
- package/temp/rtk/src/filters/yamllint.toml +25 -0
- package/temp/rtk/src/find_cmd.rs +598 -0
- package/temp/rtk/src/format_cmd.rs +386 -0
- package/temp/rtk/src/gain.rs +723 -0
- package/temp/rtk/src/gh_cmd.rs +1651 -0
- package/temp/rtk/src/git.rs +2012 -0
- package/temp/rtk/src/go_cmd.rs +592 -0
- package/temp/rtk/src/golangci_cmd.rs +254 -0
- package/temp/rtk/src/grep_cmd.rs +288 -0
- package/temp/rtk/src/gt_cmd.rs +810 -0
- package/temp/rtk/src/hook_audit_cmd.rs +283 -0
- package/temp/rtk/src/hook_check.rs +171 -0
- package/temp/rtk/src/init.rs +1859 -0
- package/temp/rtk/src/integrity.rs +537 -0
- package/temp/rtk/src/json_cmd.rs +231 -0
- package/temp/rtk/src/learn/detector.rs +628 -0
- package/temp/rtk/src/learn/mod.rs +119 -0
- package/temp/rtk/src/learn/report.rs +184 -0
- package/temp/rtk/src/lint_cmd.rs +694 -0
- package/temp/rtk/src/local_llm.rs +316 -0
- package/temp/rtk/src/log_cmd.rs +248 -0
- package/temp/rtk/src/ls.rs +324 -0
- package/temp/rtk/src/main.rs +2482 -0
- package/temp/rtk/src/mypy_cmd.rs +389 -0
- package/temp/rtk/src/next_cmd.rs +241 -0
- package/temp/rtk/src/npm_cmd.rs +236 -0
- package/temp/rtk/src/parser/README.md +267 -0
- package/temp/rtk/src/parser/error.rs +46 -0
- package/temp/rtk/src/parser/formatter.rs +336 -0
- package/temp/rtk/src/parser/mod.rs +311 -0
- package/temp/rtk/src/parser/types.rs +119 -0
- package/temp/rtk/src/pip_cmd.rs +302 -0
- package/temp/rtk/src/playwright_cmd.rs +479 -0
- package/temp/rtk/src/pnpm_cmd.rs +573 -0
- package/temp/rtk/src/prettier_cmd.rs +221 -0
- package/temp/rtk/src/prisma_cmd.rs +482 -0
- package/temp/rtk/src/psql_cmd.rs +382 -0
- package/temp/rtk/src/pytest_cmd.rs +384 -0
- package/temp/rtk/src/read.rs +217 -0
- package/temp/rtk/src/rewrite_cmd.rs +50 -0
- package/temp/rtk/src/ruff_cmd.rs +402 -0
- package/temp/rtk/src/runner.rs +271 -0
- package/temp/rtk/src/summary.rs +297 -0
- package/temp/rtk/src/tee.rs +405 -0
- package/temp/rtk/src/telemetry.rs +248 -0
- package/temp/rtk/src/toml_filter.rs +1655 -0
- package/temp/rtk/src/tracking.rs +1416 -0
- package/temp/rtk/src/tree.rs +209 -0
- package/temp/rtk/src/tsc_cmd.rs +259 -0
- package/temp/rtk/src/utils.rs +432 -0
- package/temp/rtk/src/verify_cmd.rs +47 -0
- package/temp/rtk/src/vitest_cmd.rs +385 -0
- package/temp/rtk/src/wc_cmd.rs +401 -0
- package/temp/rtk/src/wget_cmd.rs +260 -0
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
- 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
|
+
}
|