use std::path::PathBuf;
use std::process::{Command, ExitCode};
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "tatara",
version,
about = "tatara — umbrella CLI: fmt, lint, run, test, deploy"
)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Fmt {
#[arg(value_name = "PATH")]
paths: Vec<PathBuf>,
#[arg(long)]
check: bool,
},
Lint {
#[arg(value_name = "PATH")]
paths: Vec<PathBuf>,
#[arg(long)]
fix: bool,
#[arg(long)]
fix_unsafe: bool,
#[arg(long)]
errors_only: bool,
#[arg(long)]
types: bool,
},
Run {
path_or_url: String,
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
Test { path_or_url: String },
Repl,
Typecheck {
path_or_url: String,
},
Deploy {
url: String,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
skip_checks: bool,
#[arg(long)]
name: Option<String>,
#[arg(long, default_value = "default")]
namespace: String,
},
Checks {
#[arg(long)]
verbose: bool,
},
ForgeDomain {
#[arg(long)]
input: PathBuf,
#[arg(long)]
name: String,
#[arg(long)]
output: PathBuf,
#[arg(long)]
dry_run: bool,
#[arg(long)]
workspace_member: bool,
},
}
fn main() -> ExitCode {
let cli = Cli::parse();
match dispatch(cli) {
Ok(code) => code,
Err(e) => {
eprintln!("tatara: error: {e:#}");
ExitCode::from(1)
}
}
}
fn dispatch(cli: Cli) -> Result<ExitCode> {
match cli.cmd {
Cmd::Fmt { paths, check } => run_feira_fmt(&paths, check),
Cmd::Lint {
paths,
fix,
fix_unsafe,
errors_only,
types,
} => {
let lint = run_feira_lint(&paths, fix, fix_unsafe, errors_only)?;
if types {
let tc = typecheck_paths(&paths)?;
Ok(merge_exit(lint, tc))
} else {
Ok(lint)
}
}
Cmd::Run { path_or_url, args } => run_script(&path_or_url, &args, false, false),
Cmd::Test { path_or_url } => run_script(&path_or_url, &[], true, false),
Cmd::Repl => run_script("", &[], false, true),
Cmd::Typecheck { path_or_url } => typecheck(&path_or_url),
Cmd::Deploy {
url,
output,
skip_checks,
name,
namespace,
} => deploy(
&url,
output.as_deref(),
skip_checks,
name.as_deref(),
&namespace,
),
Cmd::ForgeDomain {
input,
name,
output,
dry_run,
workspace_member,
} => forge_domain(&input, &name, &output, dry_run, workspace_member),
Cmd::Checks { verbose } => run_platform_checks(verbose),
}
}
fn run_platform_checks(verbose: bool) -> Result<ExitCode> {
tatara_gateway_api::register()?;
tatara_cilium::register()?;
tatara_prometheus_operator::register()?;
tatara_ebpf::register()?;
let invariants = tatara_platform_checks::default_invariants();
let run = tatara_platform_checks::run_all(&invariants);
let fails = run.fail_count();
if verbose || fails > 0 {
eprintln!("{}", run.report());
}
let kw_count = tatara_lisp::domain::registered_keywords().len();
eprintln!(
"tatara checks: {} invariant(s) over {} keyword(s) — {} failure(s)",
invariants.len(),
kw_count,
fails,
);
if fails == 0 {
Ok(ExitCode::SUCCESS)
} else {
for (inv, kw, msg) in run.failures() {
eprintln!(" FAIL [{inv}] {kw}: {msg}");
}
Ok(ExitCode::from(1))
}
}
fn forge_domain(
input: &std::path::Path,
name: &str,
output: &std::path::Path,
dry_run: bool,
workspace_member: bool,
) -> Result<ExitCode> {
let domain = tatara_domain_forge::from_crd_yaml(input, name)
.with_context(|| format!("parsing CRD input {}", input.display()))?;
let opts = if workspace_member {
tatara_domain_forge::EmitOptions::workspace_member()
} else {
tatara_domain_forge::EmitOptions::default()
};
let cargo = tatara_domain_forge::emit_cargo_toml(&domain, &opts);
let lib = tatara_domain_forge::emit_lib_rs(&domain);
let readme = tatara_domain_forge::emit_readme(&domain);
if dry_run {
println!("───── Cargo.toml ─────\n{cargo}");
println!("───── src/lib.rs ─────\n{lib}");
println!("───── README.md ─────\n{readme}");
return Ok(ExitCode::SUCCESS);
}
let src_dir = output.join("src");
std::fs::create_dir_all(&src_dir).with_context(|| format!("mkdir {}", src_dir.display()))?;
std::fs::write(output.join("Cargo.toml"), &cargo)
.with_context(|| format!("write {}/Cargo.toml", output.display()))?;
std::fs::write(src_dir.join("lib.rs"), &lib)
.with_context(|| format!("write {}/src/lib.rs", output.display()))?;
std::fs::write(output.join("README.md"), &readme)
.with_context(|| format!("write {}/README.md", output.display()))?;
eprintln!(
"tatara: forge-domain → wrote {} ({} resource{})",
output.display(),
domain.resources.len(),
if domain.resources.len() == 1 { "" } else { "s" }
);
Ok(ExitCode::SUCCESS)
}
fn find_feira() -> Result<PathBuf> {
if let Ok(p) = which("feira") {
return Ok(p);
}
bail!(
"feira not found on $PATH — install it via `cargo install --path \
caixa/caixa-feira` or build the caixa workspace and put \
caixa/target/release on $PATH"
)
}
fn run_feira_fmt(paths: &[PathBuf], check: bool) -> Result<ExitCode> {
let feira = find_feira()?;
let mut cmd = Command::new(feira);
cmd.arg("fmt");
if check {
cmd.arg("--check");
}
for p in paths {
cmd.arg(p);
}
let status = cmd.status().context("running feira fmt")?;
Ok(ExitCode::from(status.code().unwrap_or(1) as u8))
}
fn run_feira_lint(
paths: &[PathBuf],
fix: bool,
fix_unsafe: bool,
errors_only: bool,
) -> Result<ExitCode> {
let feira = find_feira()?;
let mut cmd = Command::new(feira);
cmd.arg("lint");
if fix {
cmd.arg("--fix");
}
if fix_unsafe {
cmd.arg("--fix-unsafe");
}
if errors_only {
cmd.arg("--errors-only");
}
for p in paths {
cmd.arg(p);
}
let status = cmd.status().context("running feira lint")?;
Ok(ExitCode::from(status.code().unwrap_or(1) as u8))
}
fn find_script() -> Result<PathBuf> {
if let Ok(self_path) = std::env::current_exe() {
if let Some(parent) = self_path.parent() {
let sibling = parent.join("tatara-script");
if sibling.exists() {
return Ok(sibling);
}
}
}
if let Ok(p) = which("tatara-script") {
return Ok(p);
}
bail!("tatara-script not found — build it with `cargo build --release` and ensure it's adjacent to `tatara` or on $PATH")
}
fn run_script(path: &str, args: &[String], test: bool, repl: bool) -> Result<ExitCode> {
let script = find_script()?;
let mut cmd = Command::new(script);
if test {
cmd.arg("--test");
}
if repl {
cmd.arg("--repl");
}
if !path.is_empty() {
cmd.arg(path);
}
for a in args {
cmd.arg(a);
}
let status = cmd.status().context("running tatara-script")?;
Ok(ExitCode::from(status.code().unwrap_or(1) as u8))
}
fn typecheck_paths(paths: &[PathBuf]) -> Result<ExitCode> {
let targets: Vec<PathBuf> = if paths.is_empty() {
find_tlisp_files(std::path::Path::new("."))
} else {
paths.to_vec()
};
let mut total_errors = 0usize;
for path in &targets {
let src =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let forms = match tatara_lisp::read_spanned(&src) {
Ok(f) => f,
Err(e) => {
eprintln!("{}: parse error: {e:?}", path.display());
total_errors += 1;
continue;
}
};
let diags = tatara_lisp_eval::build_check::check_program(&forms);
for d in &diags {
eprintln!("{}: {}", path.display(), d.render(&src));
}
total_errors += diags.len();
}
eprintln!(
"tatara typecheck: {} file(s) checked, {} type error(s)",
targets.len(),
total_errors
);
if total_errors > 0 {
Ok(ExitCode::from(1))
} else {
Ok(ExitCode::SUCCESS)
}
}
fn find_tlisp_files(root: &std::path::Path) -> Vec<PathBuf> {
let mut out = Vec::new();
walk_collect(root, &mut out);
out
}
fn walk_collect(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let p = entry.path();
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if name.starts_with('.') || name == "target" || name == "node_modules" {
continue;
}
if p.is_dir() {
walk_collect(&p, out);
} else if p.extension().is_some_and(|e| e == "tlisp" || e == "lisp") {
out.push(p);
}
}
}
fn merge_exit(a: ExitCode, b: ExitCode) -> ExitCode {
if is_success(&a) && is_success(&b) {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
fn typecheck(path_or_url: &str) -> Result<ExitCode> {
let resolved =
tatara_lisp_source::resolve_once(path_or_url).context("resolving source for typecheck")?;
let src = String::from_utf8(resolved.bytes).context("source is not UTF-8")?;
let forms =
tatara_lisp::read_spanned(&src).map_err(|e| anyhow::anyhow!("parse error: {e:?}"))?;
let diags = tatara_lisp_eval::build_check::check_program(&forms);
if diags.is_empty() {
eprintln!("tatara typecheck: 0 errors");
return Ok(ExitCode::SUCCESS);
}
for d in &diags {
eprintln!("{}: {}", path_or_url, d.render(&src));
}
eprintln!("tatara typecheck: {} type error(s)", diags.len());
Ok(ExitCode::from(1))
}
fn deploy(
url: &str,
output: Option<&std::path::Path>,
skip_checks: bool,
name: Option<&str>,
namespace: &str,
) -> Result<ExitCode> {
eprintln!("tatara deploy: resolving {url}");
let resolved = tatara_lisp_source::resolve_once(url).context("resolving source")?;
let bytes_len = resolved.bytes.len();
let blake3 = resolved.blake3.clone();
eprintln!(
"tatara deploy: fetched {bytes_len} bytes, blake3={}",
&blake3[..16]
);
if !skip_checks {
let tmp = tempfile_path(".tlisp")?;
std::fs::write(&tmp, &resolved.bytes).context("writing temp source")?;
eprintln!("tatara deploy: pre-flight fmt --check");
let fmt_ok = run_feira_fmt(&[tmp.clone()], true)?;
if !is_success(&fmt_ok) {
eprintln!("tatara deploy: WARN format drift detected; continuing");
}
eprintln!("tatara deploy: pre-flight lint");
let lint_ok = run_feira_lint(&[tmp.clone()], false, false, true)?;
if !is_success(&lint_ok) {
eprintln!("tatara deploy: lint errors; continuing (use --skip-checks to silence)");
}
let _ = std::fs::remove_file(&tmp);
}
let unit_name = name
.map(str::to_string)
.unwrap_or_else(|| derive_unit_name(url));
let manifest = compute_unit_manifest(&unit_name, namespace, url, &blake3);
let yaml = serde_yaml::to_string(&manifest).context("rendering YAML")?;
if let Some(path) = output {
std::fs::write(path, &yaml).with_context(|| format!("writing {}", path.display()))?;
eprintln!("tatara deploy: wrote manifest to {}", path.display());
} else {
print!("{yaml}");
}
eprintln!("tatara deploy: ready. apply with:\n kubectl -n {namespace} apply -f -");
Ok(ExitCode::SUCCESS)
}
fn is_success(code: &ExitCode) -> bool {
let s = format!("{code:?}");
s.contains('0')
}
fn derive_unit_name(url: &str) -> String {
let stem = url
.split(['?', '#'])
.next()
.unwrap_or(url)
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or("tatara-program");
let stem = stem.split('.').next().unwrap_or(stem);
let mut out = String::with_capacity(stem.len());
for c in stem.chars() {
if c.is_ascii_alphanumeric() || c == '-' {
out.push(c.to_ascii_lowercase());
} else {
out.push('-');
}
}
if out.is_empty() {
"tatara-program".into()
} else {
out
}
}
#[derive(Debug, serde::Serialize)]
struct ComputeUnit {
api_version: &'static str,
kind: &'static str,
metadata: ComputeUnitMeta,
spec: ComputeUnitSpec,
}
#[derive(Debug, serde::Serialize)]
struct ComputeUnitMeta {
name: String,
namespace: String,
annotations: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, serde::Serialize)]
struct ComputeUnitSpec {
source: ComputeUnitSource,
shape: &'static str,
}
#[derive(Debug, serde::Serialize)]
struct ComputeUnitSource {
url: String,
blake3: String,
}
fn compute_unit_manifest(name: &str, namespace: &str, url: &str, blake3: &str) -> ComputeUnit {
let mut annotations = std::collections::BTreeMap::new();
annotations.insert("tatara.pleme.io/source-url".to_string(), url.to_string());
annotations.insert(
"tatara.pleme.io/source-blake3".to_string(),
blake3.to_string(),
);
ComputeUnit {
api_version: "compute.pleme.io/v1alpha1",
kind: "ComputeUnit",
metadata: ComputeUnitMeta {
name: name.to_string(),
namespace: namespace.to_string(),
annotations,
},
spec: ComputeUnitSpec {
source: ComputeUnitSource {
url: url.to_string(),
blake3: blake3.to_string(),
},
shape: "program",
},
}
}
fn which(name: &str) -> Result<PathBuf> {
let path_var = std::env::var_os("PATH").context("PATH not set")?;
for entry in std::env::split_paths(&path_var) {
let candidate = entry.join(name);
if candidate.is_file() {
return Ok(candidate);
}
}
bail!("{name} not found on PATH")
}
fn tempfile_path(extension: &str) -> Result<PathBuf> {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let mut tmp = std::env::temp_dir();
tmp.push(format!("tatara-{nanos}{extension}"));
Ok(tmp)
}