use std::ffi::OsString;
use std::path::PathBuf;
use std::process::{Command, ExitCode};
const EXPLAIN_MD: &str = include_str!("../../docs/explain/ct.md");
const EXPLAIN_JSON: &str = include_str!("../../docs/explain/ct.json");
const SUBCOMMANDS: &[(&str, &str)] = &[
(
"search",
"Recursively find files by name, type, size, and content (ct-search)",
),
(
"view",
"Show a file's lines by range, or regions around a pattern (ct-view)",
),
(
"tree",
"Report a file tree with per-file line/word/char counts and filters (ct-tree)",
),
(
"edit",
"Find/replace across files, gated by an --expect verdict and --dry-run (ct-edit)",
),
(
"patch",
"Set/delete nodes by path in JSON/JSONC/JSONL, preserving formatting (ct-patch)",
),
(
"test",
"Run a command as a framed experiment with a templated verdict (ct-test)",
),
];
fn usage() -> String {
let mut commands = String::new();
for (name, blurb) in SUBCOMMANDS {
commands.push_str(&format!(" {name:<9} {blurb}\n"));
}
format!(
"ct — umbrella launcher for the coding_tools suite\n\
\n\
Usage:\n \
ct <command> [args...] run the matching ct-<command> tool\n \
ct help [<command>] show this help, or a command's own --help\n \
ct <command> --explain print one tool's definition (md or json)\n \
ct --explain [md|json] describe the whole suite (json = a manifest of every tool)\n \
ct --version\n\
\n\
Commands:\n\
{commands}\n\
ct <command> runs ct-<command> — found beside ct or on PATH — the same way\n\
git runs git-<command>, so any ct-* tool you install is reachable through ct.\n"
)
}
fn resolve(child: &str) -> OsString {
if let Ok(exe) = std::env::current_exe()
&& let Some(dir) = exe.parent()
{
let candidate: PathBuf = dir.join(child);
if candidate.is_file() {
return candidate.into_os_string();
}
}
OsString::from(child)
}
fn dispatch(sub: &str, rest: &[String]) -> ExitCode {
let program = resolve(&format!("ct-{sub}"));
let mut command = Command::new(&program);
command.args(rest);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
launch_error(sub, command.exec())
}
#[cfg(not(unix))]
{
match command.status() {
Ok(status) => ExitCode::from(u8::try_from(status.code().unwrap_or(2)).unwrap_or(2)),
Err(e) => launch_error(sub, e),
}
}
}
fn launch_error(sub: &str, err: std::io::Error) -> ExitCode {
if err.kind() == std::io::ErrorKind::NotFound {
eprintln!("ct: unknown command '{sub}' — no 'ct-{sub}' found beside ct or on PATH");
eprint!("\n{}", usage());
} else {
eprintln!("ct: could not run 'ct-{sub}': {err}");
}
ExitCode::from(2)
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let Some(first) = args.first() else {
eprint!("{}", usage());
return ExitCode::from(2);
};
match first.as_str() {
"-h" | "--help" => {
print!("{}", usage());
ExitCode::SUCCESS
}
"-V" | "--version" => {
println!("ct {}", env!("CARGO_PKG_VERSION"));
ExitCode::SUCCESS
}
"help" => match args.get(1) {
None => {
print!("{}", usage());
ExitCode::SUCCESS
}
Some(sub) => dispatch(sub, &["--help".to_string()]),
},
explain if explain == "--explain" || explain.starts_with("--explain=") => {
let fmt = explain
.strip_prefix("--explain=")
.or(args.get(1).map(String::as_str))
.unwrap_or("md");
match fmt {
"md" => {
print!("{EXPLAIN_MD}");
ExitCode::SUCCESS
}
"json" => {
print!("{EXPLAIN_JSON}");
ExitCode::SUCCESS
}
other => {
eprintln!("ct: unknown --explain format '{other}' (use md or json)");
ExitCode::from(2)
}
}
}
flag if flag.starts_with('-') => {
eprintln!("ct: unknown option '{flag}'");
eprint!("\n{}", usage());
ExitCode::from(2)
}
sub => dispatch(sub, &args[1..]),
}
}