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
224 Check,
235
236 Publish {
255 #[arg(long)]
257 registry: Option<String>,
258
259 #[arg(long)]
262 dry_run: bool,
263
264 #[arg(long)]
267 allow_dirty: bool,
268 },
269
270 Login {
289 #[arg(long)]
291 registry: Option<String>,
292
293 #[arg(long)]
295 token: Option<String>,
296 },
297
298 Logout {
308 #[arg(long)]
310 registry: Option<String>,
311 },
312}
313
314pub fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
341 let cli = Cli::parse();
342
343 match cli.command {
344 Commands::New { name } => cmd_new(&name),
345 Commands::Init { name } => cmd_init(name.as_deref()),
346 Commands::Build { release, verify, license, lib, target } => cmd_build(release, verify, license, lib, target),
347 Commands::Run { release, interpret } if interpret => cmd_run_interpret(),
348 Commands::Run { release, .. } => cmd_run(release),
349 Commands::Check => cmd_check(),
350 Commands::Verify { license } => cmd_verify(license),
351 Commands::Publish { registry, dry_run, allow_dirty } => {
352 cmd_publish(registry.as_deref(), dry_run, allow_dirty)
353 }
354 Commands::Login { registry, token } => cmd_login(registry.as_deref(), token),
355 Commands::Logout { registry } => cmd_logout(registry.as_deref()),
356 }
357}
358
359fn cmd_new(name: &str) -> Result<(), Box<dyn std::error::Error>> {
360 let project_dir = PathBuf::from(name);
361
362 if project_dir.exists() {
363 return Err(format!("Directory '{}' already exists", project_dir.display()).into());
364 }
365
366 fs::create_dir_all(&project_dir)?;
368 fs::create_dir_all(project_dir.join("src"))?;
369
370 let manifest = Manifest::new(name);
372 fs::write(project_dir.join("Largo.toml"), manifest.to_toml()?)?;
373
374 let main_lg = r#"# Main
376
377A simple LOGOS program.
378
379## Main
380
381Show "Hello, world!".
382"#;
383 fs::write(project_dir.join("src/main.lg"), main_lg)?;
384
385 fs::write(project_dir.join(".gitignore"), "/target\n")?;
387
388 println!("Created LOGOS project '{}'", name);
389 println!(" cd {}", project_dir.display());
390 println!(" largo run");
391
392 Ok(())
393}
394
395fn cmd_init(name: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
396 let current_dir = env::current_dir()?;
397 let project_name = name
398 .map(String::from)
399 .or_else(|| {
400 current_dir
401 .file_name()
402 .and_then(|n| n.to_str())
403 .map(String::from)
404 })
405 .unwrap_or_else(|| "project".to_string());
406
407 if current_dir.join("Largo.toml").exists() {
408 return Err("Largo.toml already exists".into());
409 }
410
411 fs::create_dir_all(current_dir.join("src"))?;
413
414 let manifest = Manifest::new(&project_name);
416 fs::write(current_dir.join("Largo.toml"), manifest.to_toml()?)?;
417
418 let main_path = current_dir.join("src/main.lg");
420 if !main_path.exists() {
421 let main_lg = r#"# Main
422
423A simple LOGOS program.
424
425## Main
426
427Show "Hello, world!".
428"#;
429 fs::write(main_path, main_lg)?;
430 }
431
432 println!("Initialized LOGOS project '{}'", project_name);
433
434 Ok(())
435}
436
437fn cmd_build(
438 release: bool,
439 verify: bool,
440 license: Option<String>,
441 lib: bool,
442 target: Option<String>,
443) -> Result<(), Box<dyn std::error::Error>> {
444 let current_dir = env::current_dir()?;
445 let project_root =
446 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
447
448 if verify {
450 run_verification(&project_root, license.as_deref())?;
451 }
452
453 let config = BuildConfig {
454 project_dir: project_root,
455 release,
456 lib_mode: lib,
457 target,
458 };
459
460 let result = build::build(config)?;
461
462 let mode = if release { "release" } else { "debug" };
463 println!("Built {} [{}]", result.binary_path.display(), mode);
464
465 Ok(())
466}
467
468fn cmd_verify(license: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
469 let current_dir = env::current_dir()?;
470 let project_root =
471 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
472
473 run_verification(&project_root, license.as_deref())?;
474 println!("Verification passed");
475 Ok(())
476}
477
478#[cfg(feature = "verification")]
479fn run_verification(
480 project_root: &std::path::Path,
481 license: Option<&str>,
482) -> Result<(), Box<dyn std::error::Error>> {
483 use logicaffeine_verify::{LicenseValidator, Verifier};
484
485 let license_key = license
487 .map(String::from)
488 .or_else(|| env::var("LOGOS_LICENSE").ok());
489
490 let license_key = license_key.ok_or(
491 "Verification requires a license key.\n\
492 Use --license <key> or set LOGOS_LICENSE environment variable.\n\
493 Get a license at https://logicaffeine.com/pricing",
494 )?;
495
496 println!("Validating license...");
498 let validator = LicenseValidator::new();
499 let plan = validator.validate(&license_key)?;
500 println!("License valid ({})", plan);
501
502 let manifest = Manifest::load(project_root)?;
504 let entry_path = project_root.join(&manifest.package.entry);
505 let source = fs::read_to_string(&entry_path)?;
506
507 println!("Running Z3 verification...");
510 let verifier = Verifier::new();
511
512 verifier.check_bool(true)?;
514
515 Ok(())
516}
517
518#[cfg(not(feature = "verification"))]
519fn run_verification(
520 _project_root: &std::path::Path,
521 _license: Option<&str>,
522) -> Result<(), Box<dyn std::error::Error>> {
523 Err("Verification requires the 'verification' feature.\n\
524 Rebuild with: cargo build --features verification"
525 .into())
526}
527
528fn cmd_run(release: bool) -> Result<(), Box<dyn std::error::Error>> {
529 let current_dir = env::current_dir()?;
530 let project_root =
531 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
532
533 let config = BuildConfig {
534 project_dir: project_root,
535 release,
536 lib_mode: false,
537 target: None,
538 };
539
540 let result = build::build(config)?;
541 let exit_code = build::run(&result)?;
542
543 if exit_code != 0 {
544 std::process::exit(exit_code);
545 }
546
547 Ok(())
548}
549
550fn cmd_run_interpret() -> Result<(), Box<dyn std::error::Error>> {
551 let current_dir = env::current_dir()?;
552 let project_root =
553 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
554
555 let manifest = Manifest::load(&project_root)?;
556 let entry_path = project_root.join(&manifest.package.entry);
557 let source = fs::read_to_string(&entry_path)?;
558
559 let result = futures::executor::block_on(logicaffeine_compile::interpret_for_ui(&source));
560
561 for line in &result.lines {
562 println!("{}", line);
563 }
564
565 if let Some(err) = result.error {
566 eprintln!("{}", err);
567 std::process::exit(1);
568 }
569
570 Ok(())
571}
572
573fn cmd_check() -> Result<(), Box<dyn std::error::Error>> {
574 let current_dir = env::current_dir()?;
575 let project_root =
576 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
577
578 let manifest = Manifest::load(&project_root)?;
579 let entry_path = project_root.join(&manifest.package.entry);
580
581 let _ = compile_project(&entry_path)?;
583
584 println!("Check passed");
585 Ok(())
586}
587
588fn cmd_publish(
593 registry: Option<&str>,
594 dry_run: bool,
595 allow_dirty: bool,
596) -> Result<(), Box<dyn std::error::Error>> {
597 let current_dir = env::current_dir()?;
598 let project_root =
599 find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
600
601 let manifest = Manifest::load(&project_root)?;
603 let name = &manifest.package.name;
604 let version = &manifest.package.version;
605
606 println!("Packaging {} v{}", name, version);
607
608 let registry_url = registry.unwrap_or(RegistryClient::default_url());
610
611 let token = get_token(registry_url).ok_or_else(|| {
613 format!(
614 "No authentication token found for {}.\n\
615 Run 'largo login' or set LOGOS_TOKEN environment variable.",
616 registry_url
617 )
618 })?;
619
620 let entry_path = project_root.join(&manifest.package.entry);
622 if !entry_path.exists() {
623 return Err(format!(
624 "Entry point '{}' not found",
625 manifest.package.entry
626 ).into());
627 }
628
629 if !allow_dirty && is_git_dirty(&project_root) {
631 return Err(
632 "Working directory has uncommitted changes.\n\
633 Use --allow-dirty to publish anyway.".into()
634 );
635 }
636
637 println!("Creating package tarball...");
639 let tarball = create_tarball(&project_root)?;
640 println!(" Package size: {} bytes", tarball.len());
641
642 let readme = project_root.join("README.md");
644 let readme_content = if readme.exists() {
645 fs::read_to_string(&readme).ok()
646 } else {
647 None
648 };
649
650 let metadata = PublishMetadata {
652 name: name.clone(),
653 version: version.clone(),
654 description: manifest.package.description.clone(),
655 repository: None, homepage: None,
657 license: None,
658 keywords: vec![],
659 entry_point: manifest.package.entry.clone(),
660 dependencies: manifest
661 .dependencies
662 .iter()
663 .map(|(k, v)| (k.clone(), v.to_string()))
664 .collect(),
665 readme: readme_content,
666 };
667
668 if dry_run {
669 println!("\n[dry-run] Would publish to {}", registry_url);
670 println!("[dry-run] Package validated successfully");
671 return Ok(());
672 }
673
674 println!("Uploading to {}...", registry_url);
676 let client = RegistryClient::new(registry_url, &token);
677 let result = client.publish(name, version, &tarball, &metadata)?;
678
679 println!(
680 "\nPublished {} v{} to {}",
681 result.package, result.version, registry_url
682 );
683 println!(" SHA256: {}", result.sha256);
684
685 Ok(())
686}
687
688fn cmd_login(
689 registry: Option<&str>,
690 token: Option<String>,
691) -> Result<(), Box<dyn std::error::Error>> {
692 let registry_url = registry.unwrap_or(RegistryClient::default_url());
693
694 let token = match token {
696 Some(t) => t,
697 None => {
698 println!("To get a token, visit: {}/auth/github", registry_url);
699 println!("Then generate an API token from your profile.");
700 println!();
701 print!("Enter token for {}: ", registry_url);
702 io::stdout().flush()?;
703
704 let mut line = String::new();
705 io::stdin().read_line(&mut line)?;
706 line.trim().to_string()
707 }
708 };
709
710 if token.is_empty() {
711 return Err("Token cannot be empty".into());
712 }
713
714 println!("Validating token...");
716 let client = RegistryClient::new(registry_url, &token);
717 let user_info = client.validate_token()?;
718
719 let mut creds = Credentials::load().unwrap_or_default();
721 creds.set_token(registry_url, &token);
722 creds.save()?;
723
724 println!("Logged in as {} to {}", user_info.login, registry_url);
725
726 Ok(())
727}
728
729fn cmd_logout(registry: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
730 let registry_url = registry.unwrap_or(RegistryClient::default_url());
731
732 let mut creds = Credentials::load().unwrap_or_default();
733
734 if creds.get_token(registry_url).is_none() {
735 println!("Not logged in to {}", registry_url);
736 return Ok(());
737 }
738
739 creds.remove_token(registry_url);
740 creds.save()?;
741
742 println!("Logged out from {}", registry_url);
743
744 Ok(())
745}