222 lines
7 KiB
Rust
222 lines
7 KiB
Rust
//! CLI command handlers.
|
|
|
|
pub mod apply;
|
|
pub mod check;
|
|
pub mod decrypt;
|
|
pub mod decrypt_entries;
|
|
pub mod decrypt_var;
|
|
pub mod diff;
|
|
pub mod edit;
|
|
pub mod encrypt;
|
|
pub mod encrypt_var;
|
|
pub mod fmt;
|
|
pub mod init;
|
|
pub mod keygen;
|
|
pub mod lsp;
|
|
pub mod package;
|
|
pub mod reencrypt;
|
|
pub mod rollback;
|
|
pub mod snapshot;
|
|
pub mod status;
|
|
pub mod tui;
|
|
|
|
use doot_core::Config;
|
|
use doot_lang::evaluator::{EvalResult, Value};
|
|
use doot_lang::{Lexer, Parser, TypeChecker};
|
|
use indexmap::IndexMap;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
/// Resolves the config file path, checking the given path or default locations.
|
|
/// Always returns an absolute path so source_dir is correct regardless of CWD.
|
|
#[tracing::instrument(skip_all)]
|
|
pub fn find_config_file(base: Option<PathBuf>) -> anyhow::Result<PathBuf> {
|
|
if let Some(path) = base {
|
|
if path.exists() {
|
|
return Ok(std::fs::canonicalize(&path)?);
|
|
}
|
|
anyhow::bail!("config file not found: {}", path.display());
|
|
}
|
|
|
|
let candidates = vec![PathBuf::from("doot.doot"), Config::default_config_file()];
|
|
|
|
for candidate in candidates {
|
|
if candidate.exists() {
|
|
return Ok(std::fs::canonicalize(&candidate)?);
|
|
}
|
|
}
|
|
|
|
anyhow::bail!(
|
|
"no config file found. searched:\n - ./doot.doot\n - {}",
|
|
Config::default_config_file().display()
|
|
)
|
|
}
|
|
|
|
fn byte_offset_to_line(source: &str, offset: usize) -> usize {
|
|
source[..offset.min(source.len())]
|
|
.chars()
|
|
.filter(|&c| c == '\n')
|
|
.count()
|
|
+ 1
|
|
}
|
|
|
|
/// Parses a `.doot` config file into a program AST.
|
|
#[tracing::instrument(skip_all, fields(path = %path.display()))]
|
|
pub fn parse_config(path: &PathBuf) -> anyhow::Result<doot_lang::Program> {
|
|
let source = std::fs::read_to_string(path)?;
|
|
let tokens = Lexer::lex(&source).map_err(|errs| {
|
|
let msg = errs
|
|
.iter()
|
|
.map(|e| {
|
|
let line = byte_offset_to_line(&source, e.span().start);
|
|
format!("{}:{}: {}", path.display(), line, e)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
anyhow::anyhow!("lexer errors:\n{}", msg)
|
|
})?;
|
|
|
|
let program = Parser::parse(tokens).map_err(|errs| {
|
|
let msg = errs
|
|
.iter()
|
|
.map(|e| {
|
|
let line = byte_offset_to_line(&source, e.span().start);
|
|
format!("{}:{}: {}", path.display(), line, e)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
anyhow::anyhow!("parser errors:\n{}", msg)
|
|
})?;
|
|
|
|
Ok(program)
|
|
}
|
|
|
|
/// Runs the type checker on a parsed program.
|
|
#[tracing::instrument(skip_all)]
|
|
pub fn type_check(
|
|
program: &doot_lang::Program,
|
|
source: &str,
|
|
filename: &str,
|
|
) -> anyhow::Result<()> {
|
|
let mut checker = TypeChecker::new();
|
|
if let Err(errors) = checker.check(program) {
|
|
for error in &errors {
|
|
error.report(source, filename);
|
|
}
|
|
anyhow::bail!("{} type error(s) found", errors.len());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Decrypts encrypted vars and files, resolving file paths relative to `source_dir`.
|
|
pub fn decrypt_encrypted_vars_with_source_dir(
|
|
result: &EvalResult,
|
|
config: &Config,
|
|
template_vars: &mut HashMap<String, Value>,
|
|
source_dir: Option<&std::path::Path>,
|
|
) -> anyhow::Result<()> {
|
|
if result.encrypted_vars.is_empty() && result.encrypted_files.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Read identity key (filter out comment lines from age-keygen output)
|
|
let identity_raw = if let Ok(key) = std::env::var("DOOT_AGE_IDENTITY") {
|
|
key
|
|
} else if config.identity_file.exists() {
|
|
std::fs::read_to_string(&config.identity_file)?
|
|
} else {
|
|
anyhow::bail!(
|
|
"encrypted entries found but no identity available. set DOOT_AGE_IDENTITY env var or create {}",
|
|
config.identity_file.display()
|
|
);
|
|
};
|
|
|
|
let identity_key = identity_raw
|
|
.lines()
|
|
.find(|line| line.starts_with("AGE-SECRET-KEY-"))
|
|
.map(|s| s.trim().to_string())
|
|
.unwrap_or_else(|| identity_raw.trim().to_string());
|
|
|
|
let identity = identity_key
|
|
.parse::<age::x25519::Identity>()
|
|
.map_err(|e| anyhow::anyhow!("invalid age identity: {}", e))?;
|
|
|
|
let mut decrypted_map = IndexMap::new();
|
|
|
|
// Decrypt inline vars
|
|
for (name, ciphertext_b64) in &result.encrypted_vars {
|
|
let encrypted_bytes = doot_lang::builtins::crypto::base64_decode(ciphertext_b64)
|
|
.map_err(|e| anyhow::anyhow!("invalid base64 for encrypted var '{}': {}", name, e))?;
|
|
|
|
let decryptor = match age::Decryptor::new(&encrypted_bytes[..])
|
|
.map_err(|e| anyhow::anyhow!("decryption error for '{}': {}", name, e))?
|
|
{
|
|
age::Decryptor::Recipients(d) => d,
|
|
_ => anyhow::bail!("unexpected decryptor type for '{}'", name),
|
|
};
|
|
|
|
let mut decrypted = vec![];
|
|
let mut reader = decryptor
|
|
.decrypt(std::iter::once(&identity as &dyn age::Identity))
|
|
.map_err(|e| anyhow::anyhow!("decryption failed for '{}': {}", name, e))?;
|
|
|
|
use std::io::Read;
|
|
reader
|
|
.read_to_end(&mut decrypted)
|
|
.map_err(|e| anyhow::anyhow!("read error decrypting '{}': {}", name, e))?;
|
|
|
|
let plaintext = String::from_utf8(decrypted)
|
|
.map_err(|e| anyhow::anyhow!("invalid UTF-8 in decrypted '{}': {}", name, e))?;
|
|
|
|
decrypted_map.insert(name.clone(), Value::Str(plaintext));
|
|
}
|
|
|
|
// Decrypt file entries
|
|
for (name, rel_path) in &result.encrypted_files {
|
|
let full_path = if rel_path.is_relative() {
|
|
source_dir
|
|
.map(|d| d.join(rel_path))
|
|
.unwrap_or_else(|| rel_path.clone())
|
|
} else {
|
|
rel_path.clone()
|
|
};
|
|
|
|
let encrypted_bytes = std::fs::read(&full_path).map_err(|e| {
|
|
anyhow::anyhow!(
|
|
"cannot read encrypted file '{}' ({}): {}",
|
|
name,
|
|
full_path.display(),
|
|
e
|
|
)
|
|
})?;
|
|
|
|
let decryptor = match age::Decryptor::new(&encrypted_bytes[..])
|
|
.map_err(|e| anyhow::anyhow!("decryption error for file '{}': {}", name, e))?
|
|
{
|
|
age::Decryptor::Recipients(d) => d,
|
|
_ => anyhow::bail!("unexpected decryptor type for file '{}'", name),
|
|
};
|
|
|
|
let mut decrypted = vec![];
|
|
let mut reader = decryptor
|
|
.decrypt(std::iter::once(&identity as &dyn age::Identity))
|
|
.map_err(|e| anyhow::anyhow!("decryption failed for file '{}': {}", name, e))?;
|
|
|
|
use std::io::Read;
|
|
reader
|
|
.read_to_end(&mut decrypted)
|
|
.map_err(|e| anyhow::anyhow!("read error decrypting file '{}': {}", name, e))?;
|
|
|
|
let plaintext = String::from_utf8(decrypted)
|
|
.map_err(|e| anyhow::anyhow!("invalid UTF-8 in decrypted file '{}': {}", name, e))?;
|
|
|
|
decrypted_map.insert(name.clone(), Value::Str(plaintext));
|
|
}
|
|
|
|
template_vars.insert(
|
|
"encrypted".to_string(),
|
|
Value::Struct("encrypted".to_string(), decrypted_map),
|
|
);
|
|
|
|
Ok(())
|
|
}
|