use clap::{Parser, Subcommand};
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use crate::compile::compile_project;
use crate::project::build::{self, find_project_root, BuildConfig};
use crate::project::manifest::Manifest;
use crate::project::credentials::{Credentials, get_token};
use crate::project::registry::{
RegistryClient, PublishMetadata, create_tarball, is_git_dirty,
};
#[derive(Parser)]
#[command(name = "largo")]
#[command(about = "The LOGOS build tool", long_about = None)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
New {
name: String,
},
Init {
#[arg(long)]
name: Option<String>,
},
Build {
#[arg(long, short)]
release: bool,
#[arg(long)]
verify: bool,
#[arg(long)]
license: Option<String>,
#[arg(long)]
lib: bool,
#[arg(long)]
target: Option<String>,
},
Verify {
#[arg(long)]
license: Option<String>,
},
Run {
#[arg(long, short)]
release: bool,
#[arg(long, short)]
interpret: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Check,
Publish {
#[arg(long)]
registry: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
allow_dirty: bool,
},
Login {
#[arg(long)]
registry: Option<String>,
#[arg(long)]
token: Option<String>,
},
Logout {
#[arg(long)]
registry: Option<String>,
},
}
pub fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
match cli.command {
Commands::New { name } => cmd_new(&name),
Commands::Init { name } => cmd_init(name.as_deref()),
Commands::Build { release, verify, license, lib, target } => cmd_build(release, verify, license, lib, target),
Commands::Run { interpret, .. } if interpret => cmd_run_interpret(),
Commands::Run { release, args, .. } => cmd_run(release, &args),
Commands::Check => cmd_check(),
Commands::Verify { license } => cmd_verify(license),
Commands::Publish { registry, dry_run, allow_dirty } => {
cmd_publish(registry.as_deref(), dry_run, allow_dirty)
}
Commands::Login { registry, token } => cmd_login(registry.as_deref(), token),
Commands::Logout { registry } => cmd_logout(registry.as_deref()),
}
}
fn cmd_new(name: &str) -> Result<(), Box<dyn std::error::Error>> {
let project_dir = PathBuf::from(name);
if project_dir.exists() {
return Err(format!("Directory '{}' already exists", project_dir.display()).into());
}
fs::create_dir_all(&project_dir)?;
fs::create_dir_all(project_dir.join("src"))?;
let manifest = Manifest::new(name);
fs::write(project_dir.join("Largo.toml"), manifest.to_toml()?)?;
let main_lg = r#"# Main
A simple LOGOS program.
## Main
Show "Hello, world!".
"#;
fs::write(project_dir.join("src/main.lg"), main_lg)?;
fs::write(project_dir.join(".gitignore"), "/target\n")?;
println!("Created LOGOS project '{}'", name);
println!(" cd {}", project_dir.display());
println!(" largo run");
Ok(())
}
fn cmd_init(name: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let project_name = name
.map(String::from)
.or_else(|| {
current_dir
.file_name()
.and_then(|n| n.to_str())
.map(String::from)
})
.unwrap_or_else(|| "project".to_string());
if current_dir.join("Largo.toml").exists() {
return Err("Largo.toml already exists".into());
}
fs::create_dir_all(current_dir.join("src"))?;
let manifest = Manifest::new(&project_name);
fs::write(current_dir.join("Largo.toml"), manifest.to_toml()?)?;
let main_path = current_dir.join("src/main.lg");
if !main_path.exists() {
let main_lg = r#"# Main
A simple LOGOS program.
## Main
Show "Hello, world!".
"#;
fs::write(main_path, main_lg)?;
}
println!("Initialized LOGOS project '{}'", project_name);
Ok(())
}
fn cmd_build(
release: bool,
verify: bool,
license: Option<String>,
lib: bool,
target: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let project_root =
find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
if verify {
run_verification(&project_root, license.as_deref())?;
}
let config = BuildConfig {
project_dir: project_root,
release,
lib_mode: lib,
target,
};
let result = build::build(config)?;
let mode = if release { "release" } else { "debug" };
println!("Built {} [{}]", result.binary_path.display(), mode);
Ok(())
}
fn cmd_verify(license: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let project_root =
find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
run_verification(&project_root, license.as_deref())?;
println!("Verification passed");
Ok(())
}
#[cfg(feature = "verification")]
fn run_verification(
project_root: &std::path::Path,
license: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
use logicaffeine_verify::{LicenseValidator, Verifier};
let license_key = license
.map(String::from)
.or_else(|| env::var("LOGOS_LICENSE").ok());
let license_key = license_key.ok_or(
"Verification requires a license key.\n\
Use --license <key> or set LOGOS_LICENSE environment variable.\n\
Get a license at https://logicaffeine.com/pricing",
)?;
println!("Validating license...");
let validator = LicenseValidator::new();
let plan = validator.validate(&license_key)?;
println!("License valid ({})", plan);
let manifest = Manifest::load(project_root)?;
let entry_path = project_root.join(&manifest.package.entry);
let source = fs::read_to_string(&entry_path)?;
println!("Running Z3 verification...");
let verifier = Verifier::new();
verifier.check_bool(true)?;
Ok(())
}
#[cfg(not(feature = "verification"))]
fn run_verification(
_project_root: &std::path::Path,
_license: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
Err("Verification requires the 'verification' feature.\n\
Rebuild with: cargo build --features verification"
.into())
}
fn cmd_run(release: bool, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let project_root =
find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
let config = BuildConfig {
project_dir: project_root,
release,
lib_mode: false,
target: None,
};
let result = build::build(config)?;
let exit_code = build::run(&result, args)?;
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
fn cmd_run_interpret() -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let project_root =
find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
let manifest = Manifest::load(&project_root)?;
let entry_path = project_root.join(&manifest.package.entry);
let source = fs::read_to_string(&entry_path)?;
let result = futures::executor::block_on(logicaffeine_compile::interpret_for_ui(&source));
for line in &result.lines {
println!("{}", line);
}
if let Some(err) = result.error {
eprintln!("{}", err);
std::process::exit(1);
}
Ok(())
}
fn cmd_check() -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let project_root =
find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
let manifest = Manifest::load(&project_root)?;
let entry_path = project_root.join(&manifest.package.entry);
let _ = compile_project(&entry_path)?;
println!("Check passed");
Ok(())
}
fn cmd_publish(
registry: Option<&str>,
dry_run: bool,
allow_dirty: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let project_root =
find_project_root(¤t_dir).ok_or("Not in a LOGOS project (Largo.toml not found)")?;
let manifest = Manifest::load(&project_root)?;
let name = &manifest.package.name;
let version = &manifest.package.version;
println!("Packaging {} v{}", name, version);
let registry_url = registry.unwrap_or(RegistryClient::default_url());
let token = get_token(registry_url).ok_or_else(|| {
format!(
"No authentication token found for {}.\n\
Run 'largo login' or set LOGOS_TOKEN environment variable.",
registry_url
)
})?;
let entry_path = project_root.join(&manifest.package.entry);
if !entry_path.exists() {
return Err(format!(
"Entry point '{}' not found",
manifest.package.entry
).into());
}
if !allow_dirty && is_git_dirty(&project_root) {
return Err(
"Working directory has uncommitted changes.\n\
Use --allow-dirty to publish anyway.".into()
);
}
println!("Creating package tarball...");
let tarball = create_tarball(&project_root)?;
println!(" Package size: {} bytes", tarball.len());
let readme = project_root.join("README.md");
let readme_content = if readme.exists() {
fs::read_to_string(&readme).ok()
} else {
None
};
let metadata = PublishMetadata {
name: name.clone(),
version: version.clone(),
description: manifest.package.description.clone(),
repository: None, homepage: None,
license: None,
keywords: vec![],
entry_point: manifest.package.entry.clone(),
dependencies: manifest
.dependencies
.iter()
.map(|(k, v)| (k.clone(), v.to_string()))
.collect(),
readme: readme_content,
};
if dry_run {
println!("\n[dry-run] Would publish to {}", registry_url);
println!("[dry-run] Package validated successfully");
return Ok(());
}
println!("Uploading to {}...", registry_url);
let client = RegistryClient::new(registry_url, &token);
let result = client.publish(name, version, &tarball, &metadata)?;
println!(
"\nPublished {} v{} to {}",
result.package, result.version, registry_url
);
println!(" SHA256: {}", result.sha256);
Ok(())
}
fn cmd_login(
registry: Option<&str>,
token: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let registry_url = registry.unwrap_or(RegistryClient::default_url());
let token = match token {
Some(t) => t,
None => {
println!("To get a token, visit: {}/auth/github", registry_url);
println!("Then generate an API token from your profile.");
println!();
print!("Enter token for {}: ", registry_url);
io::stdout().flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
line.trim().to_string()
}
};
if token.is_empty() {
return Err("Token cannot be empty".into());
}
println!("Validating token...");
let client = RegistryClient::new(registry_url, &token);
let user_info = client.validate_token()?;
let mut creds = Credentials::load().unwrap_or_default();
creds.set_token(registry_url, &token);
creds.save()?;
println!("Logged in as {} to {}", user_info.login, registry_url);
Ok(())
}
fn cmd_logout(registry: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let registry_url = registry.unwrap_or(RegistryClient::default_url());
let mut creds = Credentials::load().unwrap_or_default();
if creds.get_token(registry_url).is_none() {
println!("Not logged in to {}", registry_url);
return Ok(());
}
creds.remove_token(registry_url);
creds.save()?;
println!("Logged out from {}", registry_url);
Ok(())
}