use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use super::GrammarOptions;
const DEFAULT_GRAMMARS: &[&str] = &[
"rust",
"go",
"python",
"javascript",
"typescript",
"c",
"cpp",
"solidity",
"cairo",
];
pub fn install(options: GrammarOptions) -> Result<()> {
let grammars: Vec<&str> = if let Some(ref only) = options.only {
for name in only {
if !DEFAULT_GRAMMARS.contains(&name.as_str()) {
bail!(
"Unknown grammar '{}'. Available: {}",
name,
DEFAULT_GRAMMARS.join(", ")
);
}
}
only.iter().map(|s| s.as_str()).collect()
} else {
DEFAULT_GRAMMARS.to_vec()
};
let pipeline_dir = pipeline_dir()?;
let source_root = find_source_root()?;
if options.list {
print_list(&grammars, &pipeline_dir, &source_root);
return Ok(());
}
println!(
"Installing grammar plugins to {}...",
pipeline_dir.display()
);
std::fs::create_dir_all(&pipeline_dir)
.with_context(|| format!("Failed to create {}", pipeline_dir.display()))?;
let mut installed = 0;
let mut skipped = 0;
let mut failed = 0;
for name in &grammars {
let target_dir = pipeline_dir.join(format!("grammar-{}", name));
let target_wasm = target_dir.join("plugin.wasm");
let target_toml = target_dir.join("plugin.toml");
if target_wasm.exists() && target_toml.exists() && !options.force {
println!(" grammar-{:<13} already installed", name);
skipped += 1;
continue;
}
let grammar_dir = source_root.join(format!("grammar-{}", name));
let source_toml = grammar_dir.join("plugin.toml");
let wasm_name = format!("grammar_{}.wasm", name);
let source_wasm = grammar_dir
.join("target")
.join("wasm32-wasip2")
.join("release")
.join(&wasm_name);
if !source_toml.exists() || !source_wasm.exists() {
eprintln!(
" grammar-{:<13} MISSING (no build artifacts at {})",
name,
grammar_dir.display()
);
failed += 1;
continue;
}
std::fs::create_dir_all(&target_dir)
.with_context(|| format!("Failed to create {}", target_dir.display()))?;
std::fs::copy(&source_wasm, &target_wasm).with_context(|| {
format!(
"Failed to copy {} -> {}",
source_wasm.display(),
target_wasm.display()
)
})?;
std::fs::copy(&source_toml, &target_toml).with_context(|| {
format!(
"Failed to copy {} -> {}",
source_toml.display(),
target_toml.display()
)
})?;
let size = std::fs::metadata(&target_wasm)
.map(|m| format_size(m.len()))
.unwrap_or_else(|_| "?".to_string());
println!(" grammar-{:<13} installed ({})", name, size);
installed += 1;
}
println!();
let total = installed + skipped;
println!(
" {}/{} grammars ready{}",
total,
grammars.len(),
if failed > 0 {
format!(" ({} missing)", failed)
} else {
String::new()
}
);
Ok(())
}
fn print_list(grammars: &[&str], pipeline_dir: &Path, source_root: &Path) {
println!("Default grammar plugins:\n");
println!(
" {:<20} {:<12} {:<10} SOURCE",
"GRAMMAR", "STATUS", "SIZE"
);
println!(" {}", "-".repeat(65));
for name in grammars {
let target_dir = pipeline_dir.join(format!("grammar-{}", name));
let installed =
target_dir.join("plugin.wasm").exists() && target_dir.join("plugin.toml").exists();
let grammar_dir = source_root.join(format!("grammar-{}", name));
let wasm_name = format!("grammar_{}.wasm", name);
let source_wasm = grammar_dir
.join("target")
.join("wasm32-wasip2")
.join("release")
.join(&wasm_name);
let (status, size, source) = if installed {
let sz = target_dir
.join("plugin.wasm")
.metadata()
.map(|m| format_size(m.len()))
.unwrap_or_else(|_| "?".into());
("installed", sz, "~/.patina/pipeline/".to_string())
} else if source_wasm.exists() {
let sz = source_wasm
.metadata()
.map(|m| format_size(m.len()))
.unwrap_or_else(|_| "?".into());
("available", sz, format!("grammar-{}/", name))
} else {
("missing", "-".to_string(), "-".to_string())
};
println!(
" grammar-{:<10} {:<12} {:<10} {}",
name, status, size, source
);
}
println!();
println!(" Target: {}", pipeline_dir.display());
}
fn find_source_root() -> Result<PathBuf> {
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
let candidates = [
parent.to_path_buf(), parent.join("..").join(".."), parent.join("..").join("..").join(".."), ];
for candidate in &candidates {
let resolved = candidate.canonicalize().unwrap_or(candidate.clone());
if resolved.join("grammar-rust").join("plugin.toml").exists() {
return Ok(resolved);
}
}
}
}
let cwd = std::env::current_dir()?;
if cwd.join("grammar-rust").join("plugin.toml").exists() {
return Ok(cwd);
}
let mut dir = cwd.as_path();
while let Some(parent) = dir.parent() {
if parent.join("grammar-rust").join("plugin.toml").exists() {
return Ok(parent.to_path_buf());
}
dir = parent;
}
bail!(
"Cannot find grammar build artifacts (grammar-*/plugin.toml).\n\
Run this command from the patina repo root, or ensure grammar-*/\n\
directories exist alongside the patina binary."
)
}
fn pipeline_dir() -> Result<PathBuf> {
let home = dirs::home_dir().context("Cannot determine home directory")?;
Ok(home.join(".patina").join("pipeline"))
}
fn format_size(bytes: u64) -> String {
if bytes >= 1_048_576 {
format!("{:.1}MB", bytes as f64 / 1_048_576.0)
} else {
format!("{}KB", bytes / 1024)
}
}