# 📄 PHASE_2_TEMPLATES_PLUGINS.md
```markdown
# Phase 2: Templates & Plugin System
## Objectives
Implement secure multi-language project templates and a safe plugin architecture using IPC. Enable `kandil create <lang>` and `kandil plugin install <url>`.
## Prerequisites
- Phase 1 complete (CLI, AI, workspace detection)
- GitHub CLI installed (for plugin management)
- Basic Git knowledge
## Detailed Sub-Tasks
### Day 1-2: Template Structure Creation
1. **Create Template Directory Structure**
```bash
mkdir -p templates/flutter/clean_arch/{lib/{core/{domain,usecases},presentation,infra}}
mkdir -p templates/python/fastapi/{app,tests}
mkdir -p templates/js/nextjs/{pages,components,lib}
mkdir -p templates/rust/cli/{src,tests}
```
2. **Populate Flutter Clean Architecture Template**
```yaml
# templates/flutter/clean_arch/pubspec.yaml
name: {{project_name}}
description: Clean Architecture Flutter App
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.4
equatable: ^2.0.5
dartz: ^0.10.1
get_it: ^7.6.7
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
```
```dart
// templates/flutter/clean_arch/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'presentation/screens/home_screen.dart';
import 'core/usecases/get_example.dart';
import 'core/domain/repositories/example_repository.dart';
import 'infra/repositories/example_repository_impl.dart';
void main() {
setupDependencies();
runApp(const MyApp());
}
void setupDependencies() {
GetIt.I.registerLazySingleton<ExampleRepository>(
() => ExampleRepositoryImpl(),
);
GetIt.I.registerLazySingleton(() => GetExample(GetIt.I()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '{{project_name}}',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
```
3. **Python FastAPI Template**
```python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="{{project_name}}", version="1.0.0")
class HealthResponse(BaseModel):
status: str
@app.get("/health", response_model=HealthResponse)
async def health_check():
return {"status": "healthy"}
@app.get("/")
async def root():
return {"message": "Welcome to {{project_name}}"}
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3
pytest==7.4.4
```
4. **JS Next.js Template**
```json
{
"name": "{{project_name}}",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.0.4",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"eslint": "^8",
"eslint-config-next": "14.0.4"
}
}
```
```tsx
// templates/js/nextjs/pages/index.tsx
import Head from 'next/head'
export default function Home() {
return (
<>
<Head>
<title>{{project_name}}</title>
<meta name="description" content="Generated by Kandil Code" />
</Head>
<main>
<h1>Welcome to {{project_name}}</h1>
</main>
</>
)
}
```
### Day 3-4: Create Command Implementation
1. **Add Dependencies**
```bash
cargo add walkdir --no-default-features
cargo add fs_extra
cargo add handlebars ```
2. **Template Engine**
```rust
use handlebars::Handlebars;
use serde_json::json;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Result;
pub struct TemplateEngine {
registry: Handlebars<'static>,
}
impl TemplateEngine {
pub fn new() -> Self {
let mut registry = Handlebars::new();
registry.set_strict_mode(true);
Self { registry }
}
pub fn create_project(
&self,
lang: &str,
name: &str,
dest: &Path,
) -> Result<PathBuf> {
let template_path = format!("templates/{}", lang);
if !Path::new(&template_path).exists() {
return Err(anyhow::anyhow!(
"Template '{}' not found. Available: {}",
lang,
self.list_templates()?.join(", ")
));
}
fs::create_dir_all(dest)?;
self.copy_template(&template_path, dest)?;
self.apply_substitutions(dest, name)?;
Ok(dest.to_path_buf())
}
fn copy_template(&self, src: &str, dest: &Path) -> Result<()> {
use fs_extra::dir::{copy, CopyOptions};
let mut options = CopyOptions::new();
options.overwrite = true;
options.copy_inside = true;
copy(src, dest, &options)?;
Ok(())
}
fn apply_substitutions(&self, dest: &Path, project_name: &str) -> Result<()> {
let context = json!({
"project_name": project_name,
"project_name_snake": project_name.replace("-", "_"),
"project_name_camel": to_camel_case(project_name),
});
for entry in walkdir::WalkDir::new(dest) {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let content = fs::read_to_string(path)?;
let rendered = self.registry.render_template(&content, &context)?;
fs::write(path, rendered)?;
}
}
Ok(())
}
fn list_templates(&self) -> Result<Vec<String>> {
let mut templates = Vec::new();
for entry in fs::read_dir("templates")? {
let entry = entry?;
if entry.file_type()?.is_dir() {
templates.push(entry.file_name().to_string_lossy().to_string());
}
}
Ok(templates)
}
}
fn to_camel_case(s: &str) -> String {
s.split('-')
.enumerate()
.map(|(i, word)| {
if i == 0 {
word.to_lowercase()
} else {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
}
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_camel_case() {
assert_eq!(to_camel_case("my-app"), "myApp");
assert_eq!(to_camel_case("flutter-project"), "flutterProject");
}
}
```
3. **CLI Integration**
```rust
use crate::templates::engine::TemplateEngine;
use std::path::Path;
use anyhow::Result;
pub async fn create_project(lang: &str, name: Option<&str>) -> Result<()> {
let project_name = name.ok_or_else(|| anyhow::anyhow!("Project name required"))?;
if !project_name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
return Err(anyhow::anyhow!(
"Project name must be alphanumeric with dashes or underscores"
));
}
let dest = Path::new(project_name);
if dest.exists() {
return Err(anyhow::anyhow!("Directory {} already exists", project_name));
}
let engine = TemplateEngine::new();
let created_path = engine.create_project(lang, project_name, dest)?;
println!("✅ Created {} project at {}", lang, created_path.display());
println!("Next steps:");
println!(" cd {}", project_name);
match lang {
"flutter" => println!(" flutter pub get && flutter run"),
"python" => println!(" pip install -r requirements.txt && uvicorn app.main:app --reload"),
"js" => println!(" npm install && npm run dev"),
_ => println!(" Check README.md for setup instructions"),
}
Ok(())
}
```
### Day 5-7: Plugin System with IPC
1. **Plugin Protocol Definition**
```rust
use serde::{Deserialize, Serialize};
use anyhow::Result;
#[derive(Debug, Serialize, Deserialize)]
pub enum PluginMessage {
Request { method: String, params: serde_json::Value },
Response { result: Result<serde_json::Value, String> },
Error { code: i32, message: String },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PluginManifest {
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub commands: Vec<String>,
pub min_kandil_version: String,
}
```
2. **Plugin Registry & Installer**
```rust
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Result;
use serde_json;
pub struct PluginRegistry {
plugins_dir: PathBuf,
}
impl PluginRegistry {
pub fn new() -> Self {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".to_string());
Self {
plugins_dir: Path::new(&home).join(".kandil").join("plugins"),
}
}
pub fn ensure_plugins_dir(&self) -> Result<()> {
std::fs::create_dir_all(&self.plugins_dir)?;
Ok(())
}
pub async fn install(&self, url: &str) -> Result<()> {
self.ensure_plugins_dir()?;
let repo_name = url
.split('/')
.last()
.ok_or_else(|| anyhow::anyhow!("Invalid repository URL"))?
.trim_end_matches(".git");
let plugin_dir = self.plugins_dir.join(repo_name);
if plugin_dir.exists() {
return Err(anyhow::anyhow!("Plugin already installed: {}", repo_name));
}
println!("Cloning plugin from {}...", url);
let status = Command::new("git")
.args(&["clone", url, plugin_dir.to_str().unwrap()])
.status()?;
if !status.success() {
return Err(anyhow::anyhow!("Failed to clone plugin repository"));
}
let manifest_path = plugin_dir.join("plugin.toml");
if !manifest_path.exists() {
std::fs::remove_dir_all(&plugin_dir)?;
return Err(anyhow::anyhow!("Plugin missing plugin.toml manifest"));
}
let manifest = self.load_manifest(&manifest_path)?;
println!("✅ Installed plugin: {} v{}", manifest.name, manifest.version);
println!(" Commands: {}", manifest.commands.join(", "));
Ok(())
}
pub fn list(&self) -> Result<Vec<PluginManifest>> {
self.ensure_plugins_dir()?;
let mut manifests = Vec::new();
for entry in std::fs::read_dir(&self.plugins_dir)? {
let entry = entry?;
let manifest_path = entry.path().join("plugin.toml");
if manifest_path.exists() {
if let Ok(manifest) = self.load_manifest(&manifest_path) {
manifests.push(manifest);
}
}
}
Ok(manifests)
}
pub fn uninstall(&self, name: &str) -> Result<()> {
let plugin_dir = self.plugins_dir.join(name);
if !plugin_dir.exists() {
return Err(anyhow::anyhow!("Plugin not found: {}", name));
}
std::fs::remove_dir_all(&plugin_dir)?;
println!("✅ Uninstalled plugin: {}", name);
Ok(())
}
fn load_manifest(&self, path: &Path) -> Result<PluginManifest> {
let content = std::fs::read_to_string(path)?;
toml::from_str(&content)
.map_err(|e| anyhow::anyhow!("Invalid plugin.toml: {}", e))
}
}
```
3. **Plugin Command Execution**
```rust
use super::protocol::PluginMessage;
use anyhow::Result;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
pub struct PluginExecutor {
registry: super::registry::PluginRegistry,
}
impl PluginExecutor {
pub fn new() -> Self {
Self {
registry: super::PluginRegistry::new(),
}
}
pub async fn execute(
&self,
plugin_name: &str,
command: &str,
args: &[String],
) -> Result<serde_json::Value> {
let plugin_dir = self.registry.plugins_dir.join(plugin_name);
if !plugin_dir.exists() {
return Err(anyhow::anyhow!("Plugin not installed: {}", plugin_name));
}
let binary_name = format!("kandil-plugin-{}", plugin_name);
let binary_path = plugin_dir.join(&binary_name);
if !binary_path.exists() {
self.build_plugin(&plugin_dir, &binary_name)?;
}
let message = PluginMessage::Request {
method: command.to_string(),
params: serde_json::json!({ "args": args }),
};
let mut child = Command::new(&binary_path)
.current_dir(&plugin_dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let stdin = child.stdin.as_mut().unwrap();
let message_json = serde_json::to_string(&message)?;
stdin.write_all(message_json.as_bytes())?;
stdin.write_all(b"\n")?;
let output = child.wait_with_output()?;
if !output.status.success() {
return Err(anywhere::anyhow!(
"Plugin {} failed: {}",
plugin_name,
String::from_utf8_lossy(&output.stderr)
));
}
let response_str = String::from_utf8(output.stdout)?;
let response: PluginMessage = serde_json::from_str(&response_str)?;
match response {
PluginMessage::Response { result } => Ok(result),
PluginMessage::Error { code, message } => {
Err(anyhow::anyhow!("Plugin error {}: {}", code, message))
}
_ => Err(anyhow::anyhow!("Invalid plugin response format")),
}
}
fn build_plugin(&self, plugin_dir: &Path, binary_name: &str) -> Result<()> {
println!("Building plugin...");
let status = Command::new("cargo")
.args(&["build", "--release", "--bin", binary_name.strip_prefix("kandil-plugin-").unwrap()])
.current_dir(plugin_dir)
.status()?;
if !status.success() {
return Err(anyhow::anyhow!("Failed to build plugin"));
}
let src = plugin_dir.join("target/release").join(binary_name);
let dst = plugin_dir.join(binary_name);
std::fs::rename(src, dst)?;
Ok(())
}
}
```
4. **Example Plugin Structure**
```rust
use kandil_plugin_sdk::{Plugin, PluginManifest};
struct FlutterLintsPlugin;
impl Plugin for FlutterLintsPlugin {
fn manifest() -> PluginManifest {
PluginManifest {
name: "flutter-lints".to_string(),
version: "1.0.0".to_string(),
description: "Enhanced Flutter linting rules".to_string(),
author: "Kandil Team".to_string(),
commands: vec!["apply-lints".to_string()],
min_kandil_version: "0.1.0".to_string(),
}
}
fn execute(&self, command: &str, args: &[String]) -> Result<serde_json::Value> {
match command {
"apply-lints" => {
Ok(serde_json::json!({
"status": "success",
"files_updated": 5
}))
}
_ => Err(anyhow::anyhow!("Unknown command: {}", command)),
}
}
}
fn main() {
FlutterLintsPlugin::run();
}
```
### Day 8-10: Plugin CLI Integration
1. **Add Plugin Commands**
```rust
use crate::plugins::{registry::PluginRegistry, executor::PluginExecutor};
use anyhow::Result;
pub async fn handle_plugin(sub: PluginSub) -> Result<()> {
match sub {
PluginSub::Install { url } => install_plugin(&url).await,
PluginSub::List => list_plugins().await,
PluginSub::Uninstall { name } => uninstall_plugin(&name).await,
PluginSub::Run { name, command, args } => run_plugin(&name, &command, &args).await,
}
}
async fn install_plugin(url: &str) -> Result<()> {
let registry = PluginRegistry::new();
registry.install(url).await
}
async fn list_plugins() -> Result<()> {
let registry = PluginRegistry::new();
let manifests = registry.list()?;
if manifests.is_empty() {
println!("No plugins installed");
return Ok(());
}
for manifest in manifests {
println!("📦 {} v{}", manifest.name, manifest.version);
println!(" {}", manifest.description);
println!(" Commands: {}", manifest.commands.join(", "));
println!();
}
Ok(())
}
async fn uninstall_plugin(name: &str) -> Result<()> {
let registry = PluginRegistry::new();
registry.uninstall(name)
}
async fn run_plugin(name: &str, command: &str, args: &[String]) -> Result<()> {
let executor = PluginExecutor::new();
let result = executor.execute(name, command, args).await?;
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
```
2. **Update Main CLI**
```rust
#[derive(Subcommand)]
pub enum Commands {
Create {
lang: String,
name: Option<String>,
},
Plugin {
#[command(subcommand)]
sub: PluginSub,
},
}
#[derive(Subcommand)]
pub enum PluginSub {
Install { url: String },
List,
Uninstall { name: String },
Run {
name: String,
command: String,
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
}
```
### Day 11-14: Integration Testing
1. **Template Tests**
```rust
use kandil_code::templates::engine::TemplateEngine;
use tempfile::TempDir;
use std::path::Path;
#[tokio::test]
async fn test_create_flutter_project() {
let engine = TemplateEngine::new();
let temp = TempDir::new().unwrap();
let result = engine.create_project(
"flutter",
"test-app",
temp.path().join("test-app").as_path(),
).await;
assert!(result.is_ok());
assert!(temp.path().join("test-app/pubspec.yaml").exists());
}
#[tokio::test]
async fn test_template_substitution() {
let engine = TemplateEngine::new();
let temp = TempDir::new().unwrap();
engine.create_project("python", "my-api", temp.path().join("my-api").as_path()).await.unwrap();
let content = std::fs::read_to_string(
temp.path().join("my-api/requirements.txt")
).unwrap();
assert!(content.contains("my-api"));
}
```
2. **Plugin Tests**
```rust
use kandil_code::plugins::{registry::PluginRegistry, executor::PluginExecutor};
use tempfile::TempDir;
#[tokio::test]
async fn test_plugin_install_list() {
let registry = PluginRegistry::new();
let temp = TempDir::new().unwrap();
let plugin_path = temp.path().join("test-plugin");
std::fs::create_dir_all(&plugin_path).unwrap();
std::fs::write(
plugin_path.join("plugin.toml"),
r#"
name = "test-plugin"
version = "1.0.0"
description = "Test plugin"
author = "Test"
commands = ["test-cmd"]
min_kandil_version = "0.1.0"
"#,
).unwrap();
let list = registry.list().unwrap();
assert!(list.is_empty());
}
#[test]
fn test_plugin_manifest_parse() {
let registry = PluginRegistry::new();
let manifest = registry.load_manifest(
Path::new("example_plugins/flutter_lints/plugin.toml")
).unwrap();
assert_eq!(manifest.name, "flutter-lints");
assert!(!manifest.commands.is_empty());
}
```
## Tools & Dependencies
| walkdir | 2.4 | Directory traversal |
| fs_extra | 1.3 | Recursive file copy |
| handlebars | 5.1 | Template substitution |
| serde_json | 1.0 | Plugin IPC format |
| tempfile | 3.8 | Test isolation |
## Testing Strategy
- **Unit**: Template substitution logic (90% coverage)
- **Integration**: Full project generation + plugin lifecycle
- **Manual**: Create projects for each language and verify they compile
## Deliverables
- ✅ 4 language templates (Flutter, Python, JS, Rust)
- ✅ `kandil create <lang> <name>` command
- ✅ Plugin registry with install/uninstall/list
- ✅ Plugin executor with JSON IPC protocol
- ✅ Example plugin structure
- ✅ 85% test coverage on templates/plugins
## Timeline Breakdown
- **Days 1-2**: Template files creation
- **Days 3-4**: Template engine + CLI integration
- **Days 5-7**: Plugin architecture (IPC)
- **Days 8-9**: Plugin CLI commands
- **Days 10-14**: Testing & polish
## Success Criteria
- `kandil create flutter myapp` generates valid Flutter project
- `kandil plugin install https://github.com/user/plugin` clones and validates
- `kandil plugin list` shows installed plugins
- `kandil plugin run my-plugin my-cmd` executes correctly
- Plugin sandbox: Cannot access parent process memory
- CI passes on all platforms
## Potential Risks & Mitigations
| Template vars not substituted | Add validation step in engine |
| Plugin binary missing | Auto-build on first run |
| Plugin crashes parent process | Use process isolation + panic::catch_unwind |
| Git clone fails | Validate URL format, add SSH key check |
| Large templates slow copy | Use shallow clone or tarball distribution |
---
**Next**: Proceed to PHASE_3_TUI_CODE.md after Phase 2 CI passes and manual verification of `kandil create` commands.