1use clap::{Parser, Subcommand};
31use std::env;
32use std::fs;
33use std::io::{self, Write};
34use std::path::PathBuf;
35
36use crate::compile::compile_project;
37use crate::project::build::{self, find_project_root, BuildConfig};
38use crate::project::manifest::Manifest;
39use crate::project::credentials::{Credentials, get_token};
40use crate::project::registry::{
41 RegistryClient, PublishMetadata, create_tarball, is_git_dirty,
42};
43
44#[derive(Parser)]
63#[command(name = "largo")]
64#[command(about = "The LOGOS build tool", long_about = None)]
65#[command(version)]
66pub struct Cli {
67 #[command(subcommand)]
69 pub command: Commands,
70}
71
72#[derive(Subcommand)]
92pub enum Commands {
93 New {
108 name: String,
110 },
111
112 Init {
124 #[arg(long)]
126 name: Option<String>,
127 },
128
129 Build {
150 #[arg(long, short)]
152 release: bool,
153
154 #[arg(long)]
157 verify: bool,
158
159 #[arg(long)]
162 license: Option<String>,
163
164 #[arg(long)]
167 lib: bool,
168
169 #[arg(long)]
172 target: Option<String>,
173 },
174
175 Verify {
192 #[arg(long)]
195 license: Option<String>,
196 },
197
198 Run {
214 #[arg(long, short)]
216 release: bool,
217
218 #[arg(long, short)]
221 interpret: bool,
222
223 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
225 args: Vec<String>,
226 },
227
228 Check,
239
240 Publish {
259 #[arg(long)]
261 registry: Option<String>,
262
263 #[arg(long)]
266 dry_run: bool,
267
268 #[arg(long)]
271 allow_dirty: bool,
272 },
273
274 Login {
293 #[arg(long)]
295 registry: Option<String>,
296
297 #[arg(long)]
299 token: Option<String>,
300 },
301
302 Logout {
312 #[arg(long)]
314 registry: Option<String>,
315 },
316}
317
318pub fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
345 let cli = Cli::parse();
346
347 match cli.command {
348 Commands::New { name } => cmd_new(&name),
349 Commands::Init { name } => cmd_init(name.as_deref()),
350 Commands::Build { release, verify, license, lib, target } => cmd_build(release, verify, license, lib, target),
351 Commands::Run { interpret, .. } if interpret => cmd_run_interpret(),
352 Commands::Run { release, args, .. } => cmd_run(release, &args),
353 Commands::Check => cmd_check(),
354 Commands::Verify { license } => cmd_verify(license),
355 Commands::Publish { registry, dry_run, allow_dirty } => {
356 cmd_publish(registry.as_deref(), dry_run, allow_dirty)
357 }
358 Commands::Login { registry, token } => cmd_login(registry.as_deref(), token),
359 Commands::Logout { registry } => cmd_logout(registry.as_deref()),
360 }
361}
362
363fn cmd_new(name: &str) -> Result<(), Box<dyn std::error::Error>> {
364 let project_dir = PathBuf::from(name);
365
366 if project_dir.exists() {
367 return Err(format!("Directory '{}' already exists", project_dir.display()).into());
368 }
369
370 fs::create_dir_all(&project_dir)?;
372 fs::create_dir_all(project_dir.join("src"))?;
373
374 let manifest = Manifest::new(name);
376 fs::write(project_dir.join("Largo.toml"), manifest.to_toml()?)?;
377
378 let main_lg = r#"# Main
380
381A simple LOGOS program.
382
383## Main
384
385Show "Hello, world!".
386"#;
387 fs::write(project_dir.join("src/main.lg"), main_lg)?;
388
389 fs::write(project_dir.join(".gitignore"), "/target\n")?;
391
392 println!("Created LOGOS project '{}'", name);
393 println!(" cd {}", project_dir.display());
394 println!(" largo run");
395
396 Ok(())
397}
398
399fn cmd_init(name: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
400 let current_dir = env::current_dir()?;
401 let project_name = name
402 .map(String::from)
403 .or_else(|| {
404 current_dir
405 .file_name()
406 .and_then(|n| n.to_str())
407 .map(String::from)
408 })
409 .unwrap_or_else(|| "project".to_string());
410
411 if current_dir.join("Largo.toml").exists() {
412 return Err("Largo.toml already exists".into());
413 }
414
415 fs::create_dir_all(current_dir.join("src"))?;
417
418 let manifest = Manifest::new(&project_name);
420 fs::write(current_dir.join("Largo.toml"), manifest.to_toml()?)?;
421
422 let main_path = current_dir.join("src/main.lg");
424 if !main_path.exists() {
425 let main_lg = r#"# Main
426
427A simple LOGOS program.
428
429## Main
430
431Show "Hello, world!".
432"#;
433 fs::write(main_path, main_lg)?;
434 }
435
436 println!("Initialized LOGOS project '{}'", project_name);
437
438 Ok(())
439}
440
441fn cmd_build(
442 release: bool,
443 verify: bool,
444 license: Option<String>,
445 lib: bool,
446 target: Option<String>,
447) -> Result<(), Box<dyn std::error::Error>> {
448 let current_dir = env::current_dir()?;
449 let project_root =
450 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
451
452 if verify {
454 run_verification(&project_root, license.as_deref())?;
455 }
456
457 let config = BuildConfig {
458 project_dir: project_root,
459 release,
460 lib_mode: lib,
461 target,
462 };
463
464 let result = build::build(config)?;
465
466 let mode = if release { "release" } else { "debug" };
467 println!("Built {} [{}]", result.binary_path.display(), mode);
468
469 Ok(())
470}
471
472fn cmd_verify(license: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
473 let current_dir = env::current_dir()?;
474 let project_root =
475 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
476
477 run_verification(&project_root, license.as_deref())?;
478 println!("Verification passed");
479 Ok(())
480}
481
482#[cfg(feature = "verification")]
483fn run_verification(
484 project_root: &std::path::Path,
485 license: Option<&str>,
486) -> Result<(), Box<dyn std::error::Error>> {
487 use logicaffeine_verify::{LicenseValidator, Verifier};
488
489 let license_key = license
491 .map(String::from)
492 .or_else(|| env::var("LOGOS_LICENSE").ok());
493
494 let license_key = license_key.ok_or(
495 "Verification requires a license key.\n\
496 Use --license <key> or set LOGOS_LICENSE environment variable.\n\
497 Get a license at https://logicaffeine.com/pricing",
498 )?;
499
500 println!("Validating license...");
502 let validator = LicenseValidator::new();
503 let plan = validator.validate(&license_key)?;
504 println!("License valid ({})", plan);
505
506 let manifest = Manifest::load(project_root)?;
508 let entry_path = project_root.join(&manifest.package.entry);
509 let source = fs::read_to_string(&entry_path)?;
510
511 println!("Running Z3 verification...");
514 let verifier = Verifier::new();
515
516 verifier.check_bool(true)?;
518
519 Ok(())
520}
521
522#[cfg(not(feature = "verification"))]
523fn run_verification(
524 _project_root: &std::path::Path,
525 _license: Option<&str>,
526) -> Result<(), Box<dyn std::error::Error>> {
527 Err("Verification requires the 'verification' feature.\n\
528 Rebuild with: cargo build --features verification"
529 .into())
530}
531
532fn cmd_run(release: bool, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
533 let current_dir = env::current_dir()?;
534 let project_root =
535 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
536
537 let config = BuildConfig {
538 project_dir: project_root,
539 release,
540 lib_mode: false,
541 target: None,
542 };
543
544 let result = build::build(config)?;
545 let exit_code = build::run(&result, args)?;
546
547 if exit_code != 0 {
548 std::process::exit(exit_code);
549 }
550
551 Ok(())
552}
553
554fn cmd_run_interpret() -> Result<(), Box<dyn std::error::Error>> {
555 let current_dir = env::current_dir()?;
556 let project_root =
557 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
558
559 let manifest = Manifest::load(&project_root)?;
560 let entry_path = project_root.join(&manifest.package.entry);
561 let source = fs::read_to_string(&entry_path)?;
562
563 let result = futures::executor::block_on(logicaffeine_compile::interpret_for_ui(&source));
564
565 for line in &result.lines {
566 println!("{}", line);
567 }
568
569 if let Some(err) = result.error {
570 eprintln!("{}", err);
571 std::process::exit(1);
572 }
573
574 Ok(())
575}
576
577fn cmd_check() -> Result<(), Box<dyn std::error::Error>> {
578 let current_dir = env::current_dir()?;
579 let project_root =
580 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
581
582 let manifest = Manifest::load(&project_root)?;
583 let entry_path = project_root.join(&manifest.package.entry);
584
585 let _ = compile_project(&entry_path)?;
587
588 println!("Check passed");
589 Ok(())
590}
591
592fn cmd_publish(
597 registry: Option<&str>,
598 dry_run: bool,
599 allow_dirty: bool,
600) -> Result<(), Box<dyn std::error::Error>> {
601 let current_dir = env::current_dir()?;
602 let project_root =
603 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
604
605 let manifest = Manifest::load(&project_root)?;
607 let name = &manifest.package.name;
608 let version = &manifest.package.version;
609
610 println!("Packaging {} v{}", name, version);
611
612 let registry_url = registry.unwrap_or(RegistryClient::default_url());
614
615 let token = get_token(registry_url).ok_or_else(|| {
617 format!(
618 "No authentication token found for {}.\n\
619 Run 'largo login' or set LOGOS_TOKEN environment variable.",
620 registry_url
621 )
622 })?;
623
624 let entry_path = project_root.join(&manifest.package.entry);
626 if !entry_path.exists() {
627 return Err(format!(
628 "Entry point '{}' not found",
629 manifest.package.entry
630 ).into());
631 }
632
633 if !allow_dirty && is_git_dirty(&project_root) {
635 return Err(
636 "Working directory has uncommitted changes.\n\
637 Use --allow-dirty to publish anyway.".into()
638 );
639 }
640
641 println!("Creating package tarball...");
643 let tarball = create_tarball(&project_root)?;
644 println!(" Package size: {} bytes", tarball.len());
645
646 let readme = project_root.join("README.md");
648 let readme_content = if readme.exists() {
649 fs::read_to_string(&readme).ok()
650 } else {
651 None
652 };
653
654 let metadata = PublishMetadata {
656 name: name.clone(),
657 version: version.clone(),
658 description: manifest.package.description.clone(),
659 repository: None, homepage: None,
661 license: None,
662 keywords: vec![],
663 entry_point: manifest.package.entry.clone(),
664 dependencies: manifest
665 .dependencies
666 .iter()
667 .map(|(k, v)| (k.clone(), v.to_string()))
668 .collect(),
669 readme: readme_content,
670 };
671
672 if dry_run {
673 println!("\n[dry-run] Would publish to {}", registry_url);
674 println!("[dry-run] Package validated successfully");
675 return Ok(());
676 }
677
678 println!("Uploading to {}...", registry_url);
680 let client = RegistryClient::new(registry_url, &token);
681 let result = client.publish(name, version, &tarball, &metadata)?;
682
683 println!(
684 "\nPublished {} v{} to {}",
685 result.package, result.version, registry_url
686 );
687 println!(" SHA256: {}", result.sha256);
688
689 Ok(())
690}
691
692fn cmd_login(
693 registry: Option<&str>,
694 token: Option<String>,
695) -> Result<(), Box<dyn std::error::Error>> {
696 let registry_url = registry.unwrap_or(RegistryClient::default_url());
697
698 let token = match token {
700 Some(t) => t,
701 None => {
702 println!("To get a token, visit: {}/auth/github", registry_url);
703 println!("Then generate an API token from your profile.");
704 println!();
705 print!("Enter token for {}: ", registry_url);
706 io::stdout().flush()?;
707
708 let mut line = String::new();
709 io::stdin().read_line(&mut line)?;
710 line.trim().to_string()
711 }
712 };
713
714 if token.is_empty() {
715 return Err("Token cannot be empty".into());
716 }
717
718 println!("Validating token...");
720 let client = RegistryClient::new(registry_url, &token);
721 let user_info = client.validate_token()?;
722
723 let mut creds = Credentials::load().unwrap_or_default();
725 creds.set_token(registry_url, &token);
726 creds.save()?;
727
728 println!("Logged in as {} to {}", user_info.login, registry_url);
729
730 Ok(())
731}
732
733fn cmd_logout(registry: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
734 let registry_url = registry.unwrap_or(RegistryClient::default_url());
735
736 let mut creds = Credentials::load().unwrap_or_default();
737
738 if creds.get_token(registry_url).is_none() {
739 println!("Not logged in to {}", registry_url);
740 return Ok(());
741 }
742
743 creds.remove_token(registry_url);
744 creds.save()?;
745
746 println!("Logged out from {}", registry_url);
747
748 Ok(())
749}