1use std::io::{self, Write};
2use std::path::Path;
3use std::time::SystemTime;
4
5use anyhow::{Context, Result};
6
7use crate::cli::{Command, ExecutionSpec};
8use crate::engine::{
9 ExecutionPayload, LanguageRegistry, build_install_command, default_language,
10 detect_language_for_source, ensure_known_language,
11};
12use crate::language::LanguageSpec;
13use crate::output;
14use crate::repl;
15use crate::version;
16
17pub fn run(command: Command) -> Result<i32> {
18 let registry = LanguageRegistry::bootstrap();
19
20 match command {
21 Command::Execute(spec) => execute_once(spec, ®istry),
22 Command::Repl {
23 initial_language,
24 detect_language,
25 } => {
26 let language = resolve_language(initial_language, detect_language, None, ®istry)?;
27 repl::run_repl(language, registry, detect_language)
28 }
29 Command::ShowVersion => {
30 println!("{}", version::describe());
31 Ok(0)
32 }
33 Command::CheckToolchains => check_toolchains(®istry),
34 Command::ShowVersions { language } => show_versions(®istry, language),
35 Command::Install { language, package } => {
36 let lang = language.unwrap_or_else(|| LanguageSpec::new(default_language()));
37 install_package(&lang, &package)
38 }
39 Command::Bench { spec, iterations } => bench_run(spec, ®istry, iterations),
40 Command::Watch { spec } => watch_run(spec, ®istry),
41 }
42}
43
44fn check_toolchains(registry: &LanguageRegistry) -> Result<i32> {
45 println!("Checking language toolchains...\n");
46
47 let mut available = 0u32;
48 let mut missing = 0u32;
49
50 let mut languages: Vec<_> = registry.known_languages();
51 languages.sort();
52
53 for lang_id in &languages {
54 let spec = LanguageSpec::new(lang_id.to_string());
55 if let Some(engine) = registry.resolve(&spec) {
56 let status = match engine.validate() {
57 Ok(()) => {
58 available += 1;
59 "\x1b[32m OK \x1b[0m"
60 }
61 Err(_) => {
62 missing += 1;
63 "\x1b[31mMISS\x1b[0m"
64 }
65 };
66 println!(" [{status}] {:<14} {}", engine.display_name(), lang_id);
67 }
68 }
69
70 println!();
71 println!(
72 " {} available, {} missing, {} total",
73 available,
74 missing,
75 available + missing
76 );
77
78 if missing > 0 {
79 println!("\n Tip: Install missing toolchains to enable those languages.");
80 }
81
82 Ok(0)
83}
84
85fn show_versions(registry: &LanguageRegistry, language: Option<LanguageSpec>) -> Result<i32> {
86 println!("Language toolchain versions...\n");
87
88 let mut available = 0u32;
89 let mut missing = 0u32;
90
91 let mut languages: Vec<String> = if let Some(lang) = language {
92 vec![lang.canonical_id().to_string()]
93 } else {
94 registry
95 .known_languages()
96 .into_iter()
97 .map(|value| value.to_string())
98 .collect()
99 };
100 languages.sort();
101
102 for lang_id in &languages {
103 let spec = LanguageSpec::new(lang_id.to_string());
104 if let Some(engine) = registry.resolve(&spec) {
105 match engine.toolchain_version() {
106 Ok(Some(version)) => {
107 available += 1;
108 println!(
109 " [\x1b[32m OK \x1b[0m] {:<14} {} - {}",
110 engine.display_name(),
111 lang_id,
112 version
113 );
114 }
115 Ok(None) => {
116 available += 1;
117 println!(
118 " [\x1b[33m ?? \x1b[0m] {:<14} {} - unknown",
119 engine.display_name(),
120 lang_id
121 );
122 }
123 Err(_) => {
124 missing += 1;
125 println!(
126 " [\x1b[31mMISS\x1b[0m] {:<14} {}",
127 engine.display_name(),
128 lang_id
129 );
130 }
131 }
132 }
133 }
134
135 println!();
136 println!(
137 " {} available, {} missing, {} total",
138 available,
139 missing,
140 available + missing
141 );
142
143 if missing > 0 {
144 println!("\n Tip: Install missing toolchains to enable those languages.");
145 }
146
147 Ok(0)
148}
149
150fn execute_once(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
151 let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
152 .context("failed to materialize execution payload")?;
153 let language = resolve_language(
154 spec.language,
155 spec.detect_language,
156 Some(&payload),
157 registry,
158 )?;
159
160 let engine = registry
161 .resolve(&language)
162 .context("failed to resolve language engine")?;
163
164 if let Err(e) = engine.validate() {
165 let display = engine.display_name();
166 let id = engine.id();
167 eprintln!(
168 "Warning: {display} ({id}) toolchain not found: {e:#}\n\
169 Install the required toolchain and ensure it is on your PATH."
170 );
171 return Err(e.context(format!("{display} is not available")));
172 }
173
174 let outcome = engine.execute(&payload)?;
175
176 if !outcome.stdout.is_empty() {
177 print!("{}", outcome.stdout);
178 io::stdout().flush().ok();
179 }
180 if !outcome.stderr.is_empty() {
181 let formatted =
182 output::format_stderr(engine.display_name(), &outcome.stderr, outcome.success());
183 eprint!("{formatted}");
184 io::stderr().flush().ok();
185 }
186
187 let show_timing = std::env::var("RUN_TIMING").is_ok_and(|v| v == "1" || v == "true");
189 if show_timing || outcome.duration.as_millis() > 1000 {
190 eprintln!(
191 "\x1b[2m[{} {}ms]\x1b[0m",
192 engine.display_name(),
193 outcome.duration.as_millis()
194 );
195 }
196
197 Ok(outcome
198 .exit_code
199 .unwrap_or(if outcome.success() { 0 } else { 1 }))
200}
201
202fn install_package(language: &LanguageSpec, package: &str) -> Result<i32> {
203 let lang_id = language.canonical_id();
204 let override_key = format!("RUN_INSTALL_COMMAND_{}", lang_id.to_ascii_uppercase());
205 let override_value = std::env::var(&override_key).ok();
206
207 let Some(mut cmd) = build_install_command(lang_id, package) else {
208 if override_value.is_some() {
209 eprintln!(
210 "\x1b[31mError:\x1b[0m {override_key} is set but could not be parsed.\n\
211 Provide a valid command, e.g. {override_key}=\"uv pip install {{package}}\""
212 );
213 return Ok(1);
214 }
215 eprintln!(
216 "\x1b[31mError:\x1b[0m No package manager available for '{lang_id}'.\n\
217 This language doesn't have a standard CLI package manager.\n\
218 Tip: You can override with {override_key}=\"<cmd> {{package}}\"",
219 );
220 return Ok(1);
221 };
222
223 eprintln!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
224
225 let result = cmd
226 .stdin(std::process::Stdio::inherit())
227 .stdout(std::process::Stdio::inherit())
228 .stderr(std::process::Stdio::inherit())
229 .status();
230
231 match result {
232 Ok(status) if status.success() => {
233 eprintln!("\x1b[32m[run]\x1b[0m Successfully installed '{package}' for {lang_id}");
234 Ok(0)
235 }
236 Ok(status) => {
237 eprintln!("\x1b[31m[run]\x1b[0m Failed to install '{package}' for {lang_id}");
238 Ok(status.code().unwrap_or(1))
239 }
240 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
241 let program = cmd.get_program().to_string_lossy();
242 eprintln!("\x1b[31m[run]\x1b[0m Package manager not found: {program}");
243 eprintln!("Tip: install it or set {override_key}=\"<cmd> {{package}}\"");
244 Ok(1)
245 }
246 Err(err) => {
247 Err(err).with_context(|| format!("failed to run package manager for {lang_id}"))
248 }
249 }
250}
251
252fn bench_run(spec: ExecutionSpec, registry: &LanguageRegistry, iterations: u32) -> Result<i32> {
253 let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
254 .context("failed to materialize execution payload")?;
255 let language = resolve_language(
256 spec.language,
257 spec.detect_language,
258 Some(&payload),
259 registry,
260 )?;
261
262 let engine = registry
263 .resolve(&language)
264 .context("failed to resolve language engine")?;
265
266 engine
267 .validate()
268 .with_context(|| format!("{} is not available", engine.display_name()))?;
269
270 eprintln!(
271 "\x1b[1mBenchmark:\x1b[0m {} — {} iteration{}",
272 engine.display_name(),
273 iterations,
274 if iterations == 1 { "" } else { "s" }
275 );
276
277 let warmup = engine.execute(&payload)?;
279 if !warmup.success() {
280 eprintln!("\x1b[31mError:\x1b[0m Code failed during warmup run");
281 if !warmup.stderr.is_empty() {
282 eprint!("{}", warmup.stderr);
283 }
284 return Ok(1);
285 }
286 eprintln!("\x1b[2m warmup: {}ms\x1b[0m", warmup.duration.as_millis());
287
288 let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
289
290 for i in 0..iterations {
291 let outcome = engine.execute(&payload)?;
292 let ms = outcome.duration.as_secs_f64() * 1000.0;
293 times.push(ms);
294
295 if i < 3 || i == iterations - 1 || (i + 1) % 10 == 0 {
296 eprintln!("\x1b[2m run {}: {:.2}ms\x1b[0m", i + 1, ms);
297 }
298 }
299
300 times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
301 let total: f64 = times.iter().sum();
302 let avg = total / times.len() as f64;
303 let min = times.first().copied().unwrap_or(0.0);
304 let max = times.last().copied().unwrap_or(0.0);
305 let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
306 (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
307 } else {
308 times[times.len() / 2]
309 };
310
311 let variance: f64 = times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
313 let stddev = variance.sqrt();
314
315 eprintln!();
316 eprintln!("\x1b[1mResults ({} runs):\x1b[0m", iterations);
317 eprintln!(" min: \x1b[32m{:.2}ms\x1b[0m", min);
318 eprintln!(" max: \x1b[33m{:.2}ms\x1b[0m", max);
319 eprintln!(" avg: \x1b[36m{:.2}ms\x1b[0m", avg);
320 eprintln!(" median: \x1b[36m{:.2}ms\x1b[0m", median);
321 eprintln!(" stddev: {:.2}ms", stddev);
322
323 if !warmup.stdout.is_empty() {
324 print!("{}", warmup.stdout);
325 io::stdout().flush().ok();
326 }
327
328 Ok(0)
329}
330
331fn watch_run(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
332 use crate::cli::InputSource;
333
334 let file_path = match &spec.source {
335 InputSource::File(p) => p.clone(),
336 _ => anyhow::bail!("--watch requires a file path (use -f or pass a file as argument)"),
337 };
338
339 if !file_path.exists() {
340 anyhow::bail!("File not found: {}", file_path.display());
341 }
342
343 let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
344 .context("failed to materialize execution payload")?;
345 let language = resolve_language(
346 spec.language.clone(),
347 spec.detect_language,
348 Some(&payload),
349 registry,
350 )?;
351
352 let engine = registry
353 .resolve(&language)
354 .context("failed to resolve language engine")?;
355
356 engine
357 .validate()
358 .with_context(|| format!("{} is not available", engine.display_name()))?;
359
360 eprintln!(
361 "\x1b[1m[watch]\x1b[0m Watching \x1b[36m{}\x1b[0m ({}). Press Ctrl+C to stop.",
362 file_path.display(),
363 engine.display_name()
364 );
365
366 fn get_mtime(path: &Path) -> Option<SystemTime> {
367 std::fs::metadata(path).ok()?.modified().ok()
368 }
369
370 let mut last_mtime = get_mtime(&file_path);
371 let mut run_count = 0u32;
372
373 run_count += 1;
375 eprintln!("\n\x1b[2m--- run #{run_count} ---\x1b[0m");
376 run_file_once(&file_path, engine, &spec.args);
377
378 loop {
379 std::thread::sleep(std::time::Duration::from_millis(300));
380
381 let current_mtime = get_mtime(&file_path);
382 if current_mtime != last_mtime {
383 last_mtime = current_mtime;
384 run_count += 1;
385
386 eprintln!("\n\x1b[2m--- run #{run_count} ---\x1b[0m");
387
388 run_file_once(&file_path, engine, &spec.args);
389 }
390 }
391}
392
393fn run_file_once(file_path: &Path, engine: &dyn crate::engine::LanguageEngine, args: &[String]) {
394 let payload = ExecutionPayload::File {
395 path: file_path.to_path_buf(),
396 args: args.to_vec(),
397 };
398 match engine.execute(&payload) {
399 Ok(outcome) => {
400 if !outcome.stdout.is_empty() {
401 print!("{}", outcome.stdout);
402 io::stdout().flush().ok();
403 }
404 if !outcome.stderr.is_empty() {
405 eprint!("\x1b[31m{}\x1b[0m", outcome.stderr);
406 io::stderr().flush().ok();
407 }
408 let ms = outcome.duration.as_millis();
409 let status = if outcome.success() {
410 "\x1b[32mOK\x1b[0m"
411 } else {
412 "\x1b[31mFAIL\x1b[0m"
413 };
414 eprintln!("\x1b[2m[{status} {ms}ms]\x1b[0m");
415 }
416 Err(e) => {
417 eprintln!("\x1b[31mError:\x1b[0m {e:#}");
418 }
419 }
420}
421
422fn resolve_language(
423 explicit: Option<LanguageSpec>,
424 allow_detect: bool,
425 payload: Option<&ExecutionPayload>,
426 registry: &LanguageRegistry,
427) -> Result<LanguageSpec> {
428 if let Some(spec) = explicit {
429 ensure_known_language(&spec, registry)?;
430 return Ok(spec);
431 }
432
433 if allow_detect
434 && let Some(payload) = payload
435 && let Some(detected) = detect_language_for_source(payload, registry)
436 {
437 return Ok(detected);
438 }
439
440 let default = LanguageSpec::new(default_language());
441 ensure_known_language(&default, registry)?;
442 Ok(default)
443}