use clap::{Parser, Subcommand};
use std::path::PathBuf;
use crate::serve;
use crate::skill;
use crate::templates;
use zorto_core::site;
#[derive(Parser)]
#[command(
name = "zorto",
version,
about = "The AI-native static site generator (SSG) with executable code blocks"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(short, long, default_value = ".")]
root: PathBuf,
#[arg(short = 'N', long)]
no_exec: bool,
#[arg(long)]
sandbox: Option<PathBuf>,
#[cfg(feature = "webapp")]
#[arg(long)]
webapp: bool,
#[cfg(feature = "app")]
#[arg(long)]
app: bool,
}
#[derive(Subcommand)]
enum Commands {
Build {
#[arg(short, long, default_value = "public")]
output: PathBuf,
#[arg(long)]
drafts: bool,
#[arg(long)]
base_url: Option<String>,
},
Preview {
#[arg(short, long, default_value = "public")]
output: PathBuf,
#[arg(short, long, default_value = "1111")]
port: u16,
#[arg(long)]
drafts: bool,
#[arg(short = 'O', long)]
open: bool,
#[arg(long, default_value = "127.0.0.1")]
interface: String,
},
Clean {
#[arg(short, long, default_value = "public")]
output: PathBuf,
#[arg(long)]
cache: bool,
},
Init {
name: Option<String>,
#[arg(short, long, default_value = "default")]
template: String,
},
Check {
#[arg(long)]
drafts: bool,
#[arg(long)]
deny_warnings: bool,
},
Skill {
#[command(subcommand)]
command: Option<skill::SkillCommands>,
},
}
pub fn run<I, T>(args: I) -> anyhow::Result<()>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let cli = Cli::parse_from(args);
if matches!(&cli.command, Some(Commands::Skill { .. })) {
let Some(Commands::Skill { command }) = cli.command else {
unreachable!();
};
return skill::handle_skill(command);
}
let root = std::fs::canonicalize(&cli.root)?;
let sandbox = resolve_sandbox(&cli.sandbox)?;
#[cfg(feature = "webapp")]
if cli.webapp {
let output = resolve_output(&root, std::path::PathBuf::from("public"));
return zorto_webapp::run_webapp(&root, &output, sandbox.as_deref());
}
#[cfg(feature = "app")]
if cli.app {
return zorto_app::run_app(&root);
}
let Some(command) = cli.command else {
Cli::parse_from(["zorto", "--help"]);
unreachable!();
};
match command {
Commands::Build {
output,
drafts,
base_url,
} => {
let output = resolve_output(&root, output);
let mut site = site::Site::load(&root, &output, drafts)?;
site.no_exec = cli.no_exec;
site.sandbox = sandbox;
if let Some(url) = base_url {
site.set_base_url(url);
}
site.build()?;
println!("Site built to {}", output.display());
}
Commands::Preview {
output,
port,
drafts,
open,
interface,
} => {
let output = resolve_output(&root, output);
let cfg = serve::ServeConfig {
root: &root,
output_dir: &output,
drafts,
no_exec: cli.no_exec,
sandbox: sandbox.as_deref(),
interface: &interface,
port,
open_browser: open,
};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(serve::serve(&cfg))?;
}
Commands::Clean { output, cache } => {
let output = resolve_output(&root, output);
if output.exists() {
std::fs::remove_dir_all(&output)?;
println!("Removed {}", output.display());
}
if cache {
zorto_core::cache::clear_cache(&root)?;
println!("Cleared code block cache");
}
}
Commands::Init { name, template } => {
let target = match name {
Some(n) => root.join(n),
None => root.clone(),
};
init_site(&target, &template)?;
}
Commands::Check {
drafts,
deny_warnings,
} => {
let output = root.join("public");
let mut site = site::Site::load(&root, &output, drafts)?;
site.no_exec = cli.no_exec;
site.sandbox = sandbox;
site.check(deny_warnings)?;
println!("Site check passed.");
}
Commands::Skill { .. } => unreachable!("handled above"),
}
Ok(())
}
fn resolve_output(root: &std::path::Path, output: PathBuf) -> PathBuf {
if output.is_relative() {
root.join(output)
} else {
output
}
}
fn resolve_sandbox(sandbox: &Option<PathBuf>) -> anyhow::Result<Option<PathBuf>> {
match sandbox {
Some(p) => {
let canonical = std::fs::canonicalize(p)
.map_err(|e| anyhow::anyhow!("cannot resolve sandbox path {}: {e}", p.display()))?;
Ok(Some(canonical))
}
None => Ok(None),
}
}
fn init_site(target: &std::path::Path, template: &str) -> anyhow::Result<()> {
if target.join("config.toml").exists() {
anyhow::bail!("config.toml already exists in {}", target.display());
}
templates::write_template(target, template)?;
println!(
"Initialized new site at {} (template: {template})",
target.display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn parse_skill_install() {
let cli = Cli::parse_from(["zorto", "skill", "install", "--target", "/tmp/skills"]);
assert!(matches!(cli.command, Some(Commands::Skill { .. })));
}
#[test]
fn parse_skill_install_all() {
let cli = Cli::parse_from([
"zorto",
"skill",
"install",
"--target",
"/tmp/skills",
"--all",
]);
assert!(matches!(cli.command, Some(Commands::Skill { .. })));
}
}