mod agents;
mod build;
mod completions;
mod discover;
mod file;
mod git;
mod hook_log;
mod hooks;
mod infra;
mod init;
mod integrity;
mod learn;
mod lint;
mod log;
mod pkg;
mod rewrite;
mod session;
mod stats;
mod test;
use std::borrow::Cow;
use std::io::{self, IsTerminal, Read, Write};
use std::process::ExitCode;
use crate::output::ParseResult;
use crate::runner::{CommandOutput, CommandRunner};
pub(crate) const KNOWN_SUBCOMMANDS: &[&str] = &[
"agents",
"build",
"completions",
"discover",
"file",
"git",
"infra",
"init",
"learn",
"lint",
"log",
"pkg",
"rewrite",
"stats",
"test",
];
pub(crate) fn is_known_subcommand(name: &str) -> bool {
KNOWN_SUBCOMMANDS.contains(&name)
}
pub(crate) fn user_has_flag(args: &[String], flags: &[&str]) -> bool {
args.iter().any(|a| {
flags.iter().any(|flag| {
a == flag || (a.starts_with(flag) && a.as_bytes().get(flag.len()) == Some(&b'='))
})
})
}
pub(crate) fn extract_show_stats(args: &[String]) -> (Vec<String>, bool) {
let show_stats = args.iter().any(|a| a == "--show-stats");
let filtered: Vec<String> = args
.iter()
.filter(|a| a.as_str() != "--show-stats")
.cloned()
.collect();
(filtered, show_stats)
}
pub(crate) fn extract_json_flag(args: &[String]) -> (Vec<String>, bool) {
let is_json = args.iter().any(|a| a == "--json");
let filtered: Vec<String> = args
.iter()
.filter(|a| a.as_str() != "--json")
.cloned()
.collect();
(filtered, is_json)
}
pub(crate) fn extract_output_format(args: &[String]) -> (Vec<String>, OutputFormat) {
let (filtered, is_json) = extract_json_flag(args);
let fmt = if is_json {
OutputFormat::Json
} else {
OutputFormat::Text
};
(filtered, fmt)
}
pub(crate) fn combine_output(output: &CommandOutput) -> Cow<'_, str> {
if output.stderr.is_empty() {
Cow::Borrowed(&output.stdout)
} else {
Cow::Owned(format!("{}\n{}", output.stdout, output.stderr))
}
}
pub(crate) fn inject_flag_before_separator(args: &mut Vec<String>, flag: &str) {
if let Some(pos) = args.iter().position(|a| a == "--") {
args.insert(pos, flag.to_string());
} else {
args.push(flag.to_string());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum OutputFormat {
#[default]
Text,
Json,
}
pub(crate) struct ParsedCommandConfig<'a> {
pub program: &'a str,
pub args: &'a [String],
pub env_overrides: &'a [(&'a str, &'a str)],
pub install_hint: &'a str,
pub use_stdin: bool,
pub show_stats: bool,
pub command_type: crate::analytics::CommandType,
pub output_format: OutputFormat,
}
#[allow(dead_code)]
pub(crate) fn run_parsed_command<T>(
program: &str,
args: &[String],
env_overrides: &[(&str, &str)],
install_hint: &str,
show_stats: bool,
command_type: crate::analytics::CommandType,
parse: impl FnOnce(&CommandOutput, &[String]) -> ParseResult<T>,
) -> anyhow::Result<ExitCode>
where
T: AsRef<str> + serde::Serialize,
{
let use_stdin = !io::stdin().is_terminal();
let config = ParsedCommandConfig {
program,
args,
env_overrides,
install_hint,
use_stdin,
show_stats,
command_type,
output_format: OutputFormat::default(),
};
run_parsed_command_with_mode(config, parse)
}
pub(crate) fn run_parsed_command_with_mode<T>(
config: ParsedCommandConfig<'_>,
parse: impl FnOnce(&CommandOutput, &[String]) -> ParseResult<T>,
) -> anyhow::Result<ExitCode>
where
T: AsRef<str> + serde::Serialize,
{
const MAX_STDIN_BYTES: u64 = 64 * 1024 * 1024;
let ParsedCommandConfig {
program,
args,
env_overrides,
install_hint,
use_stdin,
show_stats,
command_type,
output_format,
} = config;
let output = if use_stdin {
let mut stdin_buf = String::new();
let bytes_read = io::stdin()
.take(MAX_STDIN_BYTES)
.read_to_string(&mut stdin_buf)?;
if bytes_read as u64 >= MAX_STDIN_BYTES {
anyhow::bail!("stdin input exceeded 64 MiB limit");
}
CommandOutput {
stdout: stdin_buf,
stderr: String::new(),
exit_code: Some(0),
duration: std::time::Duration::ZERO,
}
} else {
let runner = CommandRunner::new(Some(std::time::Duration::from_secs(300)));
let args_str: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
match runner.run_with_env(program, &args_str, env_overrides) {
Ok(out) => out,
Err(e) => {
let msg = e.to_string();
if msg.contains("failed to execute") {
eprintln!("error: '{program}' not found");
eprintln!("hint: {install_hint}");
return Ok(ExitCode::FAILURE);
}
return Err(e);
}
}
};
let output = CommandOutput {
stdout: crate::output::strip_ansi(&output.stdout),
stderr: crate::output::strip_ansi(&output.stderr),
..output
};
let result = parse(&output, args);
let _ = result.emit_markers(&mut io::stderr().lock());
let code = output.exit_code.unwrap_or(1);
let compressed: String = match output_format {
OutputFormat::Json => {
let json_str = result.to_json_envelope()?;
let mut handle = io::stdout().lock();
writeln!(handle, "{json_str}")?;
handle.flush()?;
json_str
}
OutputFormat::Text => {
let content = result.content();
let mut handle = io::stdout().lock();
write!(handle, "{content}")?;
if !content.is_empty() && !content.ends_with('\n') {
writeln!(handle)?;
}
handle.flush()?;
content.to_string()
}
};
if show_stats {
let (orig, comp) = crate::process::count_token_pair(&output.stdout, &compressed);
crate::process::report_token_stats(orig, comp, "");
}
if crate::analytics::is_analytics_enabled() {
crate::analytics::try_record_command(
output.stdout,
compressed,
format!("skim {program} {}", args.join(" ")),
command_type,
output.duration,
Some(result.tier_name()),
);
}
Ok(ExitCode::from(code.clamp(0, 255) as u8))
}
pub(crate) fn dispatch(subcommand: &str, args: &[String]) -> anyhow::Result<ExitCode> {
if !is_known_subcommand(subcommand) {
anyhow::bail!(
"Unknown subcommand: '{subcommand}'\n\
Available subcommands: {}\n\
Run 'skim --help' for usage information",
KNOWN_SUBCOMMANDS.join(", ")
);
}
match subcommand {
"agents" => agents::run(args),
"build" => build::run(args),
"completions" => completions::run(args),
"discover" => discover::run(args),
"file" => file::run(args),
"git" => git::run(args),
"infra" => infra::run(args),
"init" => init::run(args),
"learn" => learn::run(args),
"lint" => lint::run(args),
"log" => log::run(args),
"pkg" => pkg::run(args),
"rewrite" => rewrite::run(args),
"stats" => stats::run(args),
"test" => test::run(args),
_ => unreachable!("unknown subcommand '{subcommand}' passed is_known_subcommand guard"),
}
}
pub(crate) fn sanitize_for_display(input: &str) -> String {
input
.chars()
.take(64)
.map(|c| {
if c.is_ascii_graphic() || c == ' ' {
c
} else {
'?'
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_json_flag_present() {
let args: Vec<String> = vec!["--json".into(), "--cached".into()];
let (filtered, is_json) = extract_json_flag(&args);
assert!(is_json);
assert_eq!(filtered, vec!["--cached"]);
}
#[test]
fn test_extract_json_flag_absent() {
let args: Vec<String> = vec!["--cached".into()];
let (filtered, is_json) = extract_json_flag(&args);
assert!(!is_json);
assert_eq!(filtered, vec!["--cached"]);
}
#[test]
fn test_sanitize_for_display_clean_input() {
assert_eq!(sanitize_for_display("hello-world"), "hello-world");
}
#[test]
fn test_sanitize_for_display_rejects_non_ascii() {
let input = "tool\x1b[31mred\x1b[0m";
let sanitized = sanitize_for_display(input);
assert!(!sanitized.contains('\x1b'));
}
#[test]
fn test_sanitize_for_display_truncates_at_64() {
let long_input = "a".repeat(100);
let sanitized = sanitize_for_display(&long_input);
assert_eq!(sanitized.len(), 64);
}
}