1use std::ffi::OsStr;
5use std::path::{Path, PathBuf};
6use std::process::ExitCode;
7use std::sync::Arc;
8
9use crate::cli::{AuditCommand, Cli, Command, EvalCommand};
10use crate::logging::{self, LogFormat, LogTarget};
11use talon_core::config::TalonConfig;
12use talon_core::env::EnvFile;
13
14fn exit_code(code: i32) -> ExitCode {
15 ExitCode::from(u8::try_from(code).unwrap_or(1))
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum InvokedAs {
21 Current,
23 Legacy,
25}
26
27impl InvokedAs {
28 pub fn from_arg0(arg0: Option<&OsStr>) -> Self {
31 let stem = arg0
32 .map(Path::new)
33 .and_then(Path::file_stem)
34 .and_then(OsStr::to_str);
35 if stem == Some(LEGACY_BINARY_NAME) {
36 InvokedAs::Legacy
37 } else {
38 InvokedAs::Current
39 }
40 }
41}
42
43pub const BINARY_NAME: &str = "talahon";
45pub const LEGACY_BINARY_NAME: &str = "talon";
47
48pub const LEGACY_NAME_NOTICE: &str = "talon: this command is now called `talahon`; the `talon` name is \
50 deprecated and will be removed in the next major version";
51
52fn legacy_name_notice(invoked: InvokedAs, format: LogFormat) {
61 if invoked != InvokedAs::Legacy {
62 return;
63 }
64 match format {
65 LogFormat::Json => tracing::warn!(
66 deprecated = LEGACY_BINARY_NAME,
67 replacement = BINARY_NAME,
68 "{LEGACY_NAME_NOTICE}"
69 ),
70 LogFormat::Pretty => eprintln!("{LEGACY_NAME_NOTICE}"),
71 }
72}
73
74fn bootstrap(
77 root: &Path,
78 target: LogTarget,
79 default_filter: &str,
80 invoked: InvokedAs,
81) -> (Arc<TalonConfig>, Arc<talon_security::redact::Redactor>) {
82 let env = EnvFile::load(root);
83 let format = LogFormat::parse(env.get("TALAHON_LOG_FORMAT").as_deref());
84 let redactor = crate::secrets::redactor_for(&env);
85 logging::init(format, target, default_filter, redactor.clone());
86 legacy_name_notice(invoked, format);
87 let config = Arc::new(TalonConfig::from_env(root, env));
88 talon_core::env::flush_legacy_key_warnings();
91 (config, redactor)
92}
93
94async fn dispatch(cli: Cli, root: PathBuf, invoked: InvokedAs) -> i32 {
95 match cli.command() {
96 Command::Version => {
97 println!("{BINARY_NAME} {}", crate::VERSION);
98 0
99 }
100 Command::Run => {
101 let (config, redactor) = bootstrap(&root, LogTarget::Stdout, "info", invoked);
102 match crate::app::run(config, redactor).await {
103 Ok(()) => 0,
104 Err(e) => {
105 tracing::error!(error = %format!("{e:#}"), "Failed to start Talahon");
106 1
107 }
108 }
109 }
110 Command::Setup(args) => {
111 let _ = bootstrap(&root, LogTarget::Stderr, "info", invoked);
112 let ctx = talon_setup::SetupContext::detect(&root);
113 match talon_setup::run_step_by_name(&args.step, &args.args, &ctx).await {
114 Ok(outcome) => {
115 println!("{}", outcome.render());
116 outcome.exit_code
117 }
118 Err(unknown) => {
119 eprintln!("{unknown}");
120 1
121 }
122 }
123 }
124 Command::Doctor => {
125 let (config, _) = bootstrap(&root, LogTarget::Stderr, "warn", invoked);
126 crate::doctor::run(&root, &config).await
127 }
128 Command::Eval { command } => {
129 let (config, _) = bootstrap(&root, LogTarget::Stderr, "warn", invoked);
130 match command {
131 EvalCommand::Score(args) => crate::eval_cmd::score(&root, args),
132 EvalCommand::Run(args) => crate::eval_cmd::run(&root, &config, args).await,
133 }
134 }
135 Command::Audit { command } => {
136 let (config, _) = bootstrap(&root, LogTarget::Stderr, "warn", invoked);
137 let result = match command {
138 AuditCommand::Verify => crate::audit_cmd::verify(&config),
139 AuditCommand::Head => crate::audit_cmd::head(&config),
140 AuditCommand::Tail { lines, kind } => {
141 crate::audit_cmd::tail(&config, *lines, kind.as_deref())
142 }
143 };
144 result.unwrap_or_else(|e| {
145 eprintln!("{BINARY_NAME} audit: {e:#}");
146 1
147 })
148 }
149 }
150}
151
152pub fn main(alias: InvokedAs) -> ExitCode {
156 let invoked = match alias {
157 InvokedAs::Legacy => InvokedAs::Legacy,
158 InvokedAs::Current => InvokedAs::from_arg0(std::env::args_os().next().as_deref()),
159 };
160 let cli = Cli::parse_with_legacy_env();
161 let root = match cli.resolve_project_root() {
162 Ok(root) => root,
163 Err(e) => {
164 eprintln!("{BINARY_NAME}: cannot determine the project directory: {e}");
165 return ExitCode::from(1);
166 }
167 };
168 let runtime = match tokio::runtime::Builder::new_multi_thread()
169 .enable_all()
170 .thread_name("talahon")
171 .build()
172 {
173 Ok(rt) => rt,
174 Err(e) => {
175 eprintln!("{BINARY_NAME}: cannot start the async runtime: {e}");
176 return ExitCode::from(1);
177 }
178 };
179 exit_code(runtime.block_on(dispatch(cli, root, invoked)))
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn the_old_binary_name_is_recognised_from_arg0() {
188 for legacy in ["talon", "/usr/local/bin/talon", "./talon", "talon.exe"] {
189 assert_eq!(
190 InvokedAs::from_arg0(Some(OsStr::new(legacy))),
191 InvokedAs::Legacy,
192 "{legacy}"
193 );
194 }
195 for current in [
196 "talahon",
197 "/opt/talahon/bin/talahon",
198 "/opt/talon/bin/talahon",
199 "talon-agent",
200 "",
201 ] {
202 assert_eq!(
203 InvokedAs::from_arg0(Some(OsStr::new(current))),
204 InvokedAs::Current,
205 "{current}"
206 );
207 }
208 assert_eq!(InvokedAs::from_arg0(None), InvokedAs::Current);
209 }
210
211 #[test]
212 fn the_notice_is_one_line_and_names_both_commands() {
213 assert_eq!(LEGACY_NAME_NOTICE.lines().count(), 1);
214 assert!(LEGACY_NAME_NOTICE.contains("`talahon`"));
215 assert!(LEGACY_NAME_NOTICE.contains("`talon`"));
216 assert!(LEGACY_NAME_NOTICE.contains("next major version"));
217 }
218}