#![cfg_attr(coverage_nightly, coverage(off))]
pub mod agent;
pub mod ci; pub mod config;
pub mod errors;
pub mod hooks; pub mod template;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod property_tests;
pub use agent::{
scaffold_agent, AgentContext, AgentContextBuilder, AgentFeature, AgentTemplate, QualityLevel,
TemplateRegistry as AgentTemplateRegistry,
};
pub use config::{
AgentFramework, Feature, QualityGateConfig, ScaffoldConfig, TemplateType, WasmFramework,
};
pub use errors::{Result, ScaffoldError};
pub use template::{Template, TemplateRegistry};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct ScaffoldEngine {}
impl ScaffoldEngine {
pub fn new() -> Result<Self> {
Ok(Self {})
}
pub fn validate_config(&self, config: &ScaffoldConfig) -> Result<()> {
validate_project_name(&config.project_name)?;
Ok(())
}
pub fn create_directory(&self, name: &str) -> Result<PathBuf> {
let path = PathBuf::from(name);
if path.exists() {
return Err(ScaffoldError::DirectoryExists(path));
}
fs::create_dir_all(&path).map_err(ScaffoldError::IoError)?;
Ok(path)
}
pub fn init_git(&self, project_dir: &Path) -> Result<()> {
let output = Command::new("git")
.args(["init"])
.current_dir(project_dir)
.output()
.map_err(|e| ScaffoldError::GitError(format!("Failed to run git: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ScaffoldError::GitError(format!(
"git init failed: {}",
stderr
)));
}
Ok(())
}
pub fn scaffold(&self, config: ScaffoldConfig) -> Result<PathBuf> {
self.validate_config(&config)?;
let project_dir = self.create_directory(&config.project_name)?;
self.init_git(&project_dir)?;
let registry = self.get_template_registry(&config.template_type);
self.create_project_structure(&project_dir, &config.template_type)?;
self.generate_files(&project_dir, ®istry, &config)?;
self.install_hooks(&project_dir, &config)?;
Ok(project_dir)
}
fn get_template_registry(&self, template_type: &TemplateType) -> TemplateRegistry {
match template_type {
TemplateType::Agent { .. } => TemplateRegistry::with_pforge_templates(),
TemplateType::Wasm { .. } => TemplateRegistry::with_wasm_templates(),
_ => TemplateRegistry::new(),
}
}
fn create_project_structure(
&self,
project_dir: &Path,
template_type: &TemplateType,
) -> Result<()> {
match template_type {
TemplateType::Agent { .. } => {
fs::create_dir_all(project_dir.join("src/handlers"))?;
fs::create_dir_all(project_dir.join("tests"))?;
fs::create_dir_all(project_dir.join("docs"))?;
}
TemplateType::Wasm { .. } => {
fs::create_dir_all(project_dir.join("src"))?;
fs::create_dir_all(project_dir.join("tests"))?;
fs::create_dir_all(project_dir.join("benches"))?;
}
_ => {}
}
Ok(())
}
fn generate_files(
&self,
project_dir: &Path,
registry: &TemplateRegistry,
config: &ScaffoldConfig,
) -> Result<()> {
let vars = self.prepare_template_vars(config);
for template_name in registry.list() {
let template = registry.get(&template_name)?;
let rendered = template.render(&vars)?;
let file_path = self.get_file_path(project_dir, &template_name, &config.template_type);
self.write_file(&file_path, &rendered)?;
}
Ok(())
}
fn prepare_template_vars(
&self,
config: &ScaffoldConfig,
) -> std::collections::HashMap<String, String> {
use std::collections::HashMap;
let mut vars = HashMap::new();
vars.insert("project_name".into(), config.project_name.clone());
vars.insert("author".into(), "Developer".into());
vars.insert(
"description".into(),
format!("{} project", config.project_name),
);
vars.insert("handler_name".into(), "Example".into());
vars.insert("handler_description".into(), "Example handler".into());
vars
}
fn get_file_path(
&self,
project_dir: &Path,
template_name: &str,
_template_type: &TemplateType,
) -> PathBuf {
match template_name {
"pforge.yaml" => project_dir.join("pforge.yaml"),
"Cargo.toml" => project_dir.join("Cargo.toml"),
"Makefile" => project_dir.join("Makefile"),
"README.md" => project_dir.join("README.md"),
"handler.rs" => project_dir.join("src/handlers/example.rs"),
"lib.rs" => project_dir.join("src/lib.rs"),
"vfs.rs" => project_dir.join("src/vfs.rs"),
_ => project_dir.join(template_name),
}
}
fn write_file(&self, path: &Path, content: &str) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, content).map_err(ScaffoldError::IoError)?;
Ok(())
}
fn install_hooks(&self, project_dir: &Path, config: &ScaffoldConfig) -> Result<()> {
use crate::scaffold::hooks::{
generate_pre_commit_hook, install_pre_commit_hook, HookConfig,
};
let hook_config = HookConfig {
project_type: config.template_type.clone(),
quality_gates: config.quality_gates.clone(),
};
let script = generate_pre_commit_hook(&hook_config)?;
install_pre_commit_hook(project_dir, &script)?;
Ok(())
}
}
impl Default for ScaffoldEngine {
fn default() -> Self {
Self::new().expect("ScaffoldEngine::new should not fail")
}
}
fn validate_project_name(name: &str) -> Result<()> {
if is_valid_name(name) {
Ok(())
} else {
Err(ScaffoldError::InvalidProjectName(name.into()))
}
}
fn is_valid_name(name: &str) -> bool {
!name.is_empty() && name.len() < 256 && !name.contains(['/', '\\', '\0'])
}