From be4aadb63264768f3c5002f12bcce8223c770453 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Fri, 12 Dec 2025 17:14:31 -0500 Subject: [PATCH] adding new project from template --- apps/app/src/components/new-project-modal.tsx | 424 ++++++++++++++++++ .../app/src/components/views/welcome-view.tsx | 333 +++++++++----- apps/app/src/lib/http-api-client.ts | 10 + apps/app/src/lib/templates.ts | 62 +++ apps/server/src/index.ts | 2 + apps/server/src/routes/templates.ts | 171 +++++++ 6 files changed, 892 insertions(+), 110 deletions(-) create mode 100644 apps/app/src/components/new-project-modal.tsx create mode 100644 apps/app/src/lib/templates.ts create mode 100644 apps/server/src/routes/templates.ts diff --git a/apps/app/src/components/new-project-modal.tsx b/apps/app/src/components/new-project-modal.tsx new file mode 100644 index 00000000..562363c4 --- /dev/null +++ b/apps/app/src/components/new-project-modal.tsx @@ -0,0 +1,424 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { HotkeyButton } from "@/components/ui/hotkey-button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; +import { + FolderPlus, + Rocket, + ExternalLink, + Check, + Loader2, + Link, + Folder, +} from "lucide-react"; +import { starterTemplates, type StarterTemplate } from "@/lib/templates"; +import { getElectronAPI } from "@/lib/electron"; +import { getHttpApiClient } from "@/lib/http-api-client"; +import { cn } from "@/lib/utils"; + +interface ValidationErrors { + projectName?: boolean; + workspaceDir?: boolean; + templateSelection?: boolean; + customUrl?: boolean; +} + +interface NewProjectModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onCreateBlankProject: (projectName: string, parentDir: string) => Promise; + onCreateFromTemplate: ( + template: StarterTemplate, + projectName: string, + parentDir: string + ) => Promise; + onCreateFromCustomUrl: ( + repoUrl: string, + projectName: string, + parentDir: string + ) => Promise; + isCreating: boolean; +} + +export function NewProjectModal({ + open, + onOpenChange, + onCreateBlankProject, + onCreateFromTemplate, + onCreateFromCustomUrl, + isCreating, +}: NewProjectModalProps) { + const [activeTab, setActiveTab] = useState<"blank" | "template">("blank"); + const [projectName, setProjectName] = useState(""); + const [workspaceDir, setWorkspaceDir] = useState(""); + const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false); + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [useCustomUrl, setUseCustomUrl] = useState(false); + const [customUrl, setCustomUrl] = useState(""); + const [errors, setErrors] = useState({}); + + // Fetch workspace directory when modal opens + useEffect(() => { + if (open) { + setIsLoadingWorkspace(true); + const httpClient = getHttpApiClient(); + httpClient.workspace.getConfig() + .then((result) => { + if (result.success && result.workspaceDir) { + setWorkspaceDir(result.workspaceDir); + } + }) + .catch((error) => { + console.error("Failed to get workspace config:", error); + }) + .finally(() => { + setIsLoadingWorkspace(false); + }); + } + }, [open]); + + // Reset form when modal closes + useEffect(() => { + if (!open) { + setProjectName(""); + setSelectedTemplate(null); + setUseCustomUrl(false); + setCustomUrl(""); + setActiveTab("blank"); + setErrors({}); + } + }, [open]); + + // Clear specific errors when user fixes them + useEffect(() => { + if (projectName && errors.projectName) { + setErrors((prev) => ({ ...prev, projectName: false })); + } + }, [projectName, errors.projectName]); + + useEffect(() => { + if ((selectedTemplate || (useCustomUrl && customUrl)) && errors.templateSelection) { + setErrors((prev) => ({ ...prev, templateSelection: false })); + } + }, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]); + + useEffect(() => { + if (customUrl && errors.customUrl) { + setErrors((prev) => ({ ...prev, customUrl: false })); + } + }, [customUrl, errors.customUrl]); + + const validateAndCreate = async () => { + const newErrors: ValidationErrors = {}; + + // Check project name + if (!projectName.trim()) { + newErrors.projectName = true; + } + + // Check workspace dir + if (!workspaceDir) { + newErrors.workspaceDir = true; + } + + // Check template selection (only for template tab) + if (activeTab === "template") { + if (useCustomUrl) { + if (!customUrl.trim()) { + newErrors.customUrl = true; + } + } else if (!selectedTemplate) { + newErrors.templateSelection = true; + } + } + + // If there are errors, show them and don't proceed + if (Object.values(newErrors).some(Boolean)) { + setErrors(newErrors); + return; + } + + // Clear errors and proceed + setErrors({}); + + if (activeTab === "blank") { + await onCreateBlankProject(projectName, workspaceDir); + } else if (useCustomUrl && customUrl) { + await onCreateFromCustomUrl(customUrl, projectName, workspaceDir); + } else if (selectedTemplate) { + await onCreateFromTemplate(selectedTemplate, projectName, workspaceDir); + } + }; + + const handleOpenRepo = (url: string) => { + const api = getElectronAPI(); + api.openExternalLink(url); + }; + + const handleSelectTemplate = (template: StarterTemplate) => { + setSelectedTemplate(template); + setUseCustomUrl(false); + setCustomUrl(""); + }; + + const handleToggleCustomUrl = () => { + setUseCustomUrl(!useCustomUrl); + if (!useCustomUrl) { + setSelectedTemplate(null); + } + }; + + const projectPath = workspaceDir && projectName ? `${workspaceDir}/${projectName}` : ""; + + return ( + + + + Create New Project + + Start with a blank project or choose from a starter template. + + + + {/* Project Name Input - Always visible at top */} +
+
+ + setProjectName(e.target.value)} + className={cn( + "bg-input text-foreground placeholder:text-muted-foreground", + errors.projectName + ? "border-red-500 focus:border-red-500 focus:ring-red-500/20" + : "border-border" + )} + data-testid="project-name-input" + autoFocus + /> + {errors.projectName && ( +

Project name is required

+ )} +
+ + {/* Workspace Directory Display */} +
+ + + {isLoadingWorkspace ? ( + "Loading workspace..." + ) : workspaceDir ? ( + <>Will be created at: {projectPath || "..."} + ) : ( + No workspace configured - please configure WORKSPACE_DIR + )} + +
+
+ + setActiveTab(v as "blank" | "template")} + className="flex-1 flex flex-col overflow-hidden" + > + + + + Blank Project + + + + Starter Kit + + + +
+ +
+

+ Create an empty project with the standard .automaker directory + structure. Perfect for starting from scratch or importing an + existing codebase. +

+
+
+ + +
+ {/* Error message for template selection */} + {errors.templateSelection && ( +

Please select a template or enter a custom GitHub URL

+ )} + + {/* Preset Templates */} +
+ {starterTemplates.map((template) => ( +
handleSelectTemplate(template)} + data-testid={`template-${template.id}`} + > +
+
+
+

+ {template.name} +

+ {selectedTemplate?.id === template.id && !useCustomUrl && ( + + )} +
+

+ {template.description} +

+ + {/* Tech Stack */} +
+ {template.techStack.slice(0, 6).map((tech) => ( + + {tech} + + ))} + {template.techStack.length > 6 && ( + + +{template.techStack.length - 6} more + + )} +
+ + {/* Key Features */} +
+ Features: + {template.features.slice(0, 3).join(" · ")} + {template.features.length > 3 && + ` · +${template.features.length - 3} more`} +
+
+ + +
+
+ ))} + + {/* Custom URL Option */} +
+
+ +

Custom GitHub URL

+ {useCustomUrl && } +
+

+ Clone any public GitHub repository as a starting point. +

+ + {useCustomUrl && ( +
e.stopPropagation()} className="space-y-1"> + setCustomUrl(e.target.value)} + className={cn( + "bg-input text-foreground placeholder:text-muted-foreground", + errors.customUrl + ? "border-red-500 focus:border-red-500 focus:ring-red-500/20" + : "border-border" + )} + data-testid="custom-url-input" + /> + {errors.customUrl && ( +

GitHub URL is required

+ )} +
+ )} +
+
+
+
+
+
+ + + + + {isCreating ? ( + <> + + {activeTab === "template" ? "Cloning..." : "Creating..."} + + ) : ( + <>Create Project + )} + + +
+
+ ); +} diff --git a/apps/app/src/components/views/welcome-view.tsx b/apps/app/src/components/views/welcome-view.tsx index 9128c179..bd161a9e 100644 --- a/apps/app/src/components/views/welcome-view.tsx +++ b/apps/app/src/components/views/welcome-view.tsx @@ -2,9 +2,6 @@ import { useState, useCallback } from "react"; import { Button } from "@/components/ui/button"; -import { HotkeyButton } from "@/components/ui/hotkey-button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, @@ -13,13 +10,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { useAppStore } from "@/store/app-store"; import { getElectronAPI, type Project } from "@/lib/electron"; import { initializeProject } from "@/lib/project-init"; @@ -41,14 +31,14 @@ import { } from "@/components/ui/dropdown-menu"; import { toast } from "sonner"; import { WorkspacePickerModal } from "@/components/workspace-picker-modal"; +import { NewProjectModal } from "@/components/new-project-modal"; import { getHttpApiClient } from "@/lib/http-api-client"; +import type { StarterTemplate } from "@/lib/templates"; export function WelcomeView() { const { projects, addProject, setCurrentProject, setCurrentView } = useAppStore(); - const [showNewProjectDialog, setShowNewProjectDialog] = useState(false); - const [newProjectName, setNewProjectName] = useState(""); - const [newProjectPath, setNewProjectPath] = useState(""); + const [showNewProjectModal, setShowNewProjectModal] = useState(false); const [isCreating, setIsCreating] = useState(false); const [isOpening, setIsOpening] = useState(false); const [showInitDialog, setShowInitDialog] = useState(false); @@ -231,31 +221,21 @@ export function WelcomeView() { ); const handleNewProject = () => { - setNewProjectName(""); - setNewProjectPath(""); - setShowNewProjectDialog(true); + setShowNewProjectModal(true); }; const handleInteractiveMode = () => { setCurrentView("interview"); }; - const handleSelectDirectory = async () => { - const api = getElectronAPI(); - const result = await api.openDirectory(); - - if (!result.canceled && result.filePaths[0]) { - setNewProjectPath(result.filePaths[0]); - } - }; - - const handleCreateProject = async () => { - if (!newProjectName || !newProjectPath) return; - + /** + * Create a blank project with just .automaker directory structure + */ + const handleCreateBlankProject = async (projectName: string, parentDir: string) => { setIsCreating(true); try { const api = getElectronAPI(); - const projectPath = `${newProjectPath}/${newProjectName}`; + const projectPath = `${parentDir}/${projectName}`; // Create project directory await api.mkdir(projectPath); @@ -274,7 +254,7 @@ export function WelcomeView() { await api.writeFile( `${projectPath}/.automaker/app_spec.txt`, ` - ${newProjectName} + ${projectName} Describe your project here. This file will be analyzed by an AI agent @@ -297,24 +277,24 @@ export function WelcomeView() { const project = { id: `project-${Date.now()}`, - name: newProjectName, + name: projectName, path: projectPath, lastOpened: new Date().toISOString(), }; addProject(project); setCurrentProject(project); - setShowNewProjectDialog(false); + setShowNewProjectModal(false); toast.success("Project created", { - description: `Created ${newProjectName} with .automaker directory`, + description: `Created ${projectName} with .automaker directory`, }); // Set init status to show the dialog setInitStatus({ isNewProject: true, createdFiles: initResult.createdFiles || [], - projectName: newProjectName, + projectName: projectName, projectPath: projectPath, }); setShowInitDialog(true); @@ -328,6 +308,206 @@ export function WelcomeView() { } }; + /** + * Create a project from a GitHub starter template + */ + const handleCreateFromTemplate = async ( + template: StarterTemplate, + projectName: string, + parentDir: string + ) => { + setIsCreating(true); + try { + const httpClient = getHttpApiClient(); + const api = getElectronAPI(); + + // Clone the template repository + const cloneResult = await httpClient.templates.clone( + template.repoUrl, + projectName, + parentDir + ); + + if (!cloneResult.success || !cloneResult.projectPath) { + toast.error("Failed to clone template", { + description: cloneResult.error || "Unknown error occurred", + }); + return; + } + + const projectPath = cloneResult.projectPath; + + // Initialize .automaker directory with all necessary files + const initResult = await initializeProject(projectPath); + + if (!initResult.success) { + toast.error("Failed to initialize project", { + description: initResult.error || "Unknown error occurred", + }); + return; + } + + // Update the app_spec.txt with template-specific info + await api.writeFile( + `${projectPath}/.automaker/app_spec.txt`, + ` + ${projectName} + + + This project was created from the "${template.name}" starter template. + ${template.description} + + + + ${template.techStack.map((tech) => `${tech}`).join("\n ")} + + + + ${template.features.map((feature) => `${feature}`).join("\n ")} + + + + + +` + ); + + const project = { + id: `project-${Date.now()}`, + name: projectName, + path: projectPath, + lastOpened: new Date().toISOString(), + }; + + addProject(project); + setCurrentProject(project); + setShowNewProjectModal(false); + + toast.success("Project created from template", { + description: `Created ${projectName} from ${template.name}`, + }); + + // Set init status to show the dialog + setInitStatus({ + isNewProject: true, + createdFiles: initResult.createdFiles || [], + projectName: projectName, + projectPath: projectPath, + }); + setShowInitDialog(true); + + // Kick off project analysis + analyzeProject(projectPath); + } catch (error) { + console.error("Failed to create project from template:", error); + toast.error("Failed to create project", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + setIsCreating(false); + } + }; + + /** + * Create a project from a custom GitHub URL + */ + const handleCreateFromCustomUrl = async ( + repoUrl: string, + projectName: string, + parentDir: string + ) => { + setIsCreating(true); + try { + const httpClient = getHttpApiClient(); + const api = getElectronAPI(); + + // Clone the repository + const cloneResult = await httpClient.templates.clone( + repoUrl, + projectName, + parentDir + ); + + if (!cloneResult.success || !cloneResult.projectPath) { + toast.error("Failed to clone repository", { + description: cloneResult.error || "Unknown error occurred", + }); + return; + } + + const projectPath = cloneResult.projectPath; + + // Initialize .automaker directory with all necessary files + const initResult = await initializeProject(projectPath); + + if (!initResult.success) { + toast.error("Failed to initialize project", { + description: initResult.error || "Unknown error occurred", + }); + return; + } + + // Update the app_spec.txt with basic info + await api.writeFile( + `${projectPath}/.automaker/app_spec.txt`, + ` + ${projectName} + + + This project was cloned from ${repoUrl}. + The AI agent will analyze the project structure. + + + + + + + + + + + + + +` + ); + + const project = { + id: `project-${Date.now()}`, + name: projectName, + path: projectPath, + lastOpened: new Date().toISOString(), + }; + + addProject(project); + setCurrentProject(project); + setShowNewProjectModal(false); + + toast.success("Project created from repository", { + description: `Created ${projectName} from ${repoUrl}`, + }); + + // Set init status to show the dialog + setInitStatus({ + isNewProject: true, + createdFiles: initResult.createdFiles || [], + projectName: projectName, + projectPath: projectPath, + }); + setShowInitDialog(true); + + // Kick off project analysis + analyzeProject(projectPath); + } catch (error) { + console.error("Failed to create project from custom URL:", error); + toast.error("Failed to create project", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + setIsCreating(false); + } + }; + const recentProjects = [...projects] .sort((a, b) => { const dateA = a.lastOpened ? new Date(a.lastOpened).getTime() : 0; @@ -508,82 +688,15 @@ export function WelcomeView() { - {/* New Project Dialog */} - - - - - Create New Project - - - Set up a new project directory with initial configuration files. - - -
-
- - setNewProjectName(e.target.value)} - className="bg-input border-border text-foreground placeholder:text-muted-foreground" - data-testid="project-name-input" - /> -
-
- -
- setNewProjectPath(e.target.value)} - className="flex-1 bg-input border-border text-foreground placeholder:text-muted-foreground" - data-testid="project-path-input" - /> - -
-
-
- - - - {isCreating ? "Creating..." : "Create Project"} - - -
-
+ {/* New Project Modal */} + {/* Project Initialization Dialog */} diff --git a/apps/app/src/lib/http-api-client.ts b/apps/app/src/lib/http-api-client.ts index d11d6044..0cb6b56b 100644 --- a/apps/app/src/lib/http-api-client.ts +++ b/apps/app/src/lib/http-api-client.ts @@ -702,6 +702,16 @@ export class HttpApiClient implements ElectronAPI { }, }; + // Templates API + templates = { + clone: (repoUrl: string, projectName: string, parentDir: string): Promise<{ + success: boolean; + projectPath?: string; + projectName?: string; + error?: string; + }> => this.post("/api/templates/clone", { repoUrl, projectName, parentDir }), + }; + // Sessions API sessions = { list: (includeArchived?: boolean): Promise<{ diff --git a/apps/app/src/lib/templates.ts b/apps/app/src/lib/templates.ts new file mode 100644 index 00000000..b445895a --- /dev/null +++ b/apps/app/src/lib/templates.ts @@ -0,0 +1,62 @@ +/** + * Starter Kit Templates + * + * Define GitHub templates that users can clone when creating new projects. + */ + +export interface StarterTemplate { + id: string; + name: string; + description: string; + repoUrl: string; + techStack: string[]; + features: string[]; + category: "fullstack" | "frontend" | "backend" | "ai" | "other"; + author: string; +} + +export const starterTemplates: StarterTemplate[] = [ + { + id: "agentic-jumpstart", + name: "Agentic Jumpstart", + description: "A starter template for building agentic AI applications with a pre-configured development environment including database setup, Docker support, and TypeScript configuration.", + repoUrl: "https://github.com/webdevcody/agentic-jumpstart-starter-kit", + techStack: ["TypeScript", "Vite", "Drizzle ORM", "Docker", "PostCSS"], + features: [ + "Pre-configured VS Code settings", + "Docker Compose setup", + "Database migrations with Drizzle", + "Type-safe development", + "Environment setup with .env.example" + ], + category: "ai", + author: "webdevcody" + }, + { + id: "full-stack-campus", + name: "Full Stack Campus", + description: "A feature-driven development template for building community platforms. Includes authentication, Stripe payments, file uploads, and real-time features using TanStack Start.", + repoUrl: "https://github.com/webdevcody/full-stack-campus", + techStack: ["TanStack Start", "PostgreSQL", "Drizzle ORM", "Better Auth", "Tailwind CSS", "Radix UI", "Stripe", "AWS S3/R2"], + features: [ + "Community posts with comments and reactions", + "User profiles and portfolios", + "Calendar event management", + "Direct messaging", + "Member discovery directory", + "Real-time notifications", + "Tiered subscriptions (free/basic/pro)", + "File uploads with presigned URLs" + ], + category: "fullstack", + author: "webdevcody" + } +]; + +export function getTemplateById(id: string): StarterTemplate | undefined { + return starterTemplates.find(t => t.id === id); +} + +export function getTemplatesByCategory(category: StarterTemplate["category"]): StarterTemplate[] { + return starterTemplates.filter(t => t.category === category); +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 2c4821b2..4801c5ce 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -29,6 +29,7 @@ import { createModelsRoutes } from "./routes/models.js"; import { createSpecRegenerationRoutes } from "./routes/spec-regeneration.js"; import { createRunningAgentsRoutes } from "./routes/running-agents.js"; import { createWorkspaceRoutes } from "./routes/workspace.js"; +import { createTemplatesRoutes } from "./routes/templates.js"; import { AgentService } from "./services/agent-service.js"; import { FeatureLoader } from "./services/feature-loader.js"; @@ -112,6 +113,7 @@ app.use("/api/models", createModelsRoutes()); app.use("/api/spec-regeneration", createSpecRegenerationRoutes(events)); app.use("/api/running-agents", createRunningAgentsRoutes()); app.use("/api/workspace", createWorkspaceRoutes()); +app.use("/api/templates", createTemplatesRoutes()); // Create HTTP server const server = createServer(app); diff --git a/apps/server/src/routes/templates.ts b/apps/server/src/routes/templates.ts new file mode 100644 index 00000000..3dd27bd2 --- /dev/null +++ b/apps/server/src/routes/templates.ts @@ -0,0 +1,171 @@ +/** + * Templates routes + * Provides API for cloning GitHub starter templates + */ + +import { Router, type Request, type Response } from "express"; +import { spawn } from "child_process"; +import path from "path"; +import fs from "fs/promises"; +import { addAllowedPath } from "../lib/security.js"; + +export function createTemplatesRoutes(): Router { + const router = Router(); + + /** + * Clone a GitHub template to a new project directory + * POST /api/templates/clone + * Body: { repoUrl: string, projectName: string, parentDir: string } + */ + router.post("/clone", async (req: Request, res: Response) => { + try { + const { repoUrl, projectName, parentDir } = req.body as { + repoUrl: string; + projectName: string; + parentDir: string; + }; + + // Validate inputs + if (!repoUrl || !projectName || !parentDir) { + res.status(400).json({ + success: false, + error: "repoUrl, projectName, and parentDir are required", + }); + return; + } + + // Validate repo URL is a valid GitHub URL + const githubUrlPattern = /^https:\/\/github\.com\/[\w-]+\/[\w.-]+$/; + if (!githubUrlPattern.test(repoUrl)) { + res.status(400).json({ + success: false, + error: "Invalid GitHub repository URL", + }); + return; + } + + // Sanitize project name (allow alphanumeric, dash, underscore) + const sanitizedName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-"); + if (sanitizedName !== projectName) { + console.log( + `[Templates] Sanitized project name: ${projectName} -> ${sanitizedName}` + ); + } + + // Build full project path + const projectPath = path.join(parentDir, sanitizedName); + + // Check if directory already exists + try { + await fs.access(projectPath); + res.status(400).json({ + success: false, + error: `Directory "${sanitizedName}" already exists in ${parentDir}`, + }); + return; + } catch { + // Directory doesn't exist, which is what we want + } + + // Ensure parent directory exists + try { + await fs.mkdir(parentDir, { recursive: true }); + } catch (error) { + console.error("[Templates] Failed to create parent directory:", error); + res.status(500).json({ + success: false, + error: "Failed to create parent directory", + }); + return; + } + + console.log(`[Templates] Cloning ${repoUrl} to ${projectPath}`); + + // Clone the repository + const cloneResult = await new Promise<{ + success: boolean; + error?: string; + }>((resolve) => { + const gitProcess = spawn("git", ["clone", repoUrl, projectPath], { + cwd: parentDir, + }); + + let stderr = ""; + + gitProcess.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + gitProcess.on("close", (code) => { + if (code === 0) { + resolve({ success: true }); + } else { + resolve({ + success: false, + error: stderr || `Git clone failed with code ${code}`, + }); + } + }); + + gitProcess.on("error", (error) => { + resolve({ + success: false, + error: `Failed to spawn git: ${error.message}`, + }); + }); + }); + + if (!cloneResult.success) { + res.status(500).json({ + success: false, + error: cloneResult.error || "Failed to clone repository", + }); + return; + } + + // Remove .git directory to start fresh + try { + const gitDir = path.join(projectPath, ".git"); + await fs.rm(gitDir, { recursive: true, force: true }); + console.log("[Templates] Removed .git directory"); + } catch (error) { + console.warn("[Templates] Could not remove .git directory:", error); + // Continue anyway - not critical + } + + // Initialize a fresh git repository + await new Promise((resolve) => { + const gitInit = spawn("git", ["init"], { + cwd: projectPath, + }); + + gitInit.on("close", () => { + console.log("[Templates] Initialized fresh git repository"); + resolve(); + }); + + gitInit.on("error", () => { + console.warn("[Templates] Could not initialize git"); + resolve(); + }); + }); + + // Add to allowed paths + addAllowedPath(projectPath); + + console.log(`[Templates] Successfully cloned template to ${projectPath}`); + + res.json({ + success: true, + projectPath, + projectName: sanitizedName, + }); + } catch (error) { + console.error("[Templates] Clone error:", error); + const message = error instanceof Error ? error.message : "Unknown error"; + res.status(500).json({ success: false, error: message }); + } + }); + + return router; +}