@hasna/terminal 2.0.5 → 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 +52 -21
- package/package.json +1 -1
- package/src/ai.ts +77 -130
- package/src/cli.tsx +51 -21
- package/src/command-validator.ts +11 -0
- package/src/context-hints.ts +291 -0
- package/src/discover.ts +238 -0
- package/src/economy.ts +53 -0
- package/src/output-processor.ts +7 -18
- package/src/output-store.ts +65 -0
- package/src/providers/base.ts +3 -1
- package/src/providers/groq.ts +108 -0
- package/src/providers/index.ts +26 -2
- package/src/providers/providers.test.ts +4 -2
- package/src/providers/xai.ts +108 -0
- 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
- package/dist/App.js +0 -404
- package/dist/Browse.js +0 -79
- package/dist/FuzzyPicker.js +0 -47
- package/dist/Onboarding.js +0 -51
- package/dist/Spinner.js +0 -12
- package/dist/StatusBar.js +0 -49
- package/dist/ai.js +0 -368
- package/dist/cache.js +0 -41
- package/dist/command-rewriter.js +0 -64
- package/dist/command-validator.js +0 -77
- package/dist/compression.js +0 -107
- package/dist/diff-cache.js +0 -107
- package/dist/economy.js +0 -79
- package/dist/expand-store.js +0 -38
- package/dist/file-cache.js +0 -72
- package/dist/file-index.js +0 -62
- package/dist/history.js +0 -62
- package/dist/lazy-executor.js +0 -54
- package/dist/line-dedup.js +0 -59
- package/dist/loop-detector.js +0 -75
- package/dist/mcp/install.js +0 -98
- package/dist/mcp/server.js +0 -569
- package/dist/noise-filter.js +0 -86
- package/dist/output-processor.js +0 -136
- package/dist/output-router.js +0 -41
- package/dist/parsers/base.js +0 -2
- package/dist/parsers/build.js +0 -64
- package/dist/parsers/errors.js +0 -101
- package/dist/parsers/files.js +0 -78
- package/dist/parsers/git.js +0 -99
- package/dist/parsers/index.js +0 -48
- package/dist/parsers/tests.js +0 -89
- package/dist/providers/anthropic.js +0 -39
- package/dist/providers/base.js +0 -4
- package/dist/providers/cerebras.js +0 -95
- package/dist/providers/index.js +0 -49
- package/dist/recipes/model.js +0 -20
- package/dist/recipes/storage.js +0 -136
- package/dist/search/content-search.js +0 -68
- package/dist/search/file-search.js +0 -61
- package/dist/search/filters.js +0 -34
- package/dist/search/index.js +0 -5
- package/dist/search/semantic.js +0 -320
- package/dist/session-boot.js +0 -59
- package/dist/session-context.js +0 -55
- package/dist/sessions-db.js +0 -120
- package/dist/smart-display.js +0 -286
- package/dist/snapshots.js +0 -51
- package/dist/supervisor.js +0 -112
- package/dist/test-watchlist.js +0 -131
- package/dist/tree.js +0 -94
- package/dist/usage-cache.js +0 -65
|
@@ -0,0 +1,297 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
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
|
+
}
|