1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Output capture and fix suggestions.
//!
//! This module handles:
//! - Saving command output for the end-of-run summary
//! - Generating helpful "to fix, run:" suggestions when checks fail
use crate::step_context::StepContext;
use crate::step_job::StepJob;
use crate::tera;
use crate::ui::style;
use std::sync::Arc;
use super::types::{OutputSummary, RunType, Step};
impl Step {
/// Save command output for the end-of-run summary.
///
/// Based on the step's `output_summary` setting, captures the appropriate
/// output (stderr, stdout, combined, or none) to display after all steps complete.
///
/// Skips saving output for check_first checks that failed (since they'll be
/// followed by a fix that will have its own output).
///
/// # Arguments
///
/// * `ctx` - The step context
/// * `job` - The current job
/// * `stdout` - Command stdout
/// * `stderr` - Command stderr
/// * `combined` - Interleaved stdout/stderr
/// * `is_failure` - Whether the command failed
pub(crate) fn save_output_summary(
&self,
ctx: &StepContext,
job: &StepJob,
stdout: &str,
stderr: &str,
combined: &str,
is_failure: bool,
) {
// Only skip if this is a check_first check that FAILED (will be followed by a fix)
// If the check passed, we want to show its output since no fix will run
let is_check_first_check_that_failed =
job.check_first && matches!(job.run_type, RunType::Check) && is_failure;
if is_check_first_check_that_failed {
return;
}
if is_failure {
ctx.hook_ctx.mark_step_failed(&self.name);
}
// On failure, use combined output so diagnostic messages are never
// lost regardless of which stream the tool writes to — but keep
// the configured label so tests/users see the expected header.
// If the step explicitly opted out with `output_summary = "hide"`,
// respect that even on failure.
if is_failure && self.output_summary != OutputSummary::Hide {
ctx.hook_ctx
.append_step_output(&self.name, self.output_summary.clone(), combined)
} else {
match self.output_summary {
OutputSummary::Stderr => {
ctx.hook_ctx
.append_step_output(&self.name, OutputSummary::Stderr, stderr)
}
OutputSummary::Stdout => {
ctx.hook_ctx
.append_step_output(&self.name, OutputSummary::Stdout, stdout)
}
OutputSummary::Combined => {
ctx.hook_ctx
.append_step_output(&self.name, OutputSummary::Combined, combined)
}
OutputSummary::Hide => {}
}
}
}
/// Collect a helpful fix suggestion when a check fails.
///
/// When running in check mode and a step fails, this generates a message
/// like "To fix, run: eslint --fix src/file.ts" to help the user.
///
/// For multi-line commands, suggests `hk fix -S <step>` instead of the
/// full command.
///
/// # Arguments
///
/// * `ctx` - The step context
/// * `job` - The current job
/// * `cmd_result` - Optional command result (for filtering files from check_list_files output)
pub(crate) fn collect_fix_suggestion(
&self,
ctx: &StepContext,
job: &StepJob,
cmd_result: Option<&ensembler::CmdResult>,
) {
// Only suggest fixes when the entire hook run is in check mode,
// not when an individual job temporarily runs a check (e.g., check_first during a fix run)
if !matches!(ctx.hook_ctx.run_type, RunType::Check) || self.fix.is_none() {
return;
}
// Prefer filtering files if check_list_files output is available
let mut suggest_files = job.files.clone();
if let Some(result) = cmd_result
&& self.check_list_files.is_some()
{
let (files, _extras) = self.filter_files_from_check_list(&job.files, &result.stdout);
if !files.is_empty() {
suggest_files = files;
}
}
// Build a minimal context based on the suggested files, honoring dir/workspace
let temp_job = StepJob::new(Arc::new(self.clone()), suggest_files, RunType::Fix);
let suggest_ctx = temp_job.tctx(&ctx.hook_ctx.tctx);
if let Some(mut fix_cmd) = self
.run_cmd(RunType::Fix)
.map(|s| s.to_string())
.filter(|s| !s.trim().is_empty())
{
if let Some(prefix) = &self.prefix {
fix_cmd = format!("{prefix} {fix_cmd}");
}
if let Ok(rendered) = tera::render(&fix_cmd, &suggest_ctx) {
let is_multi_line = rendered.contains('\n');
if is_multi_line {
// Too long to inline; suggest hk fix with step filter
let step_flag = format!("-S {}", &self.name);
let cmd = format!(
"To fix, run: {}",
style::edim(format!("hk fix {}", step_flag))
);
ctx.hook_ctx.add_fix_suggestion(cmd);
} else {
let cmd = format!("To fix, run: {}", style::edim(rendered));
ctx.hook_ctx.add_fix_suggestion(cmd);
}
}
}
}
}