import { existsSync } from "node:fs"; import { readFile, readdir } from "node:fs/promises"; import { dirname, join } from "node:path"; import { unzipSync } from "fflate"; import { getControlApiUrl } from "../lib/control-plane.ts"; import { parseJsonc } from "../lib/jsonc.ts"; import type { TemplateMetadata as TemplateOrigin } from "../lib/project-link.ts"; import type { Template } from "./types"; // Resolve templates directory relative to this file (src/templates -> templates) const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates"); export const BUILTIN_TEMPLATES = [ "hello", "miniapp", "api", "cron", "resend", "nextjs", "saas", "ai-chat", "chat", "semantic-search", "nextjs-shadcn", "nextjs-clerk", "nextjs-auth", "telegram-bot", "mpp-api", ]; /** * Resolved template with origin tracking for lineage */ export interface ResolvedTemplate { template: Template; origin: TemplateOrigin; } /** * Read all files in a directory recursively */ async function readTemplateFiles(dir: string, base = ""): Promise> { const files: Record = {}; const entries = await readdir(dir, { withFileTypes: true }); // Skip these directories/files (but keep bun.lock for faster installs) const SKIP = [ ".jack.json", ".jack", // Skip .jack directory (template.json is for origin tracking, not project files) "node_modules", ".git", "package-lock.json", "CLAUDE.md", ".wrangler", "dist", ]; for (const entry of entries) { const relativePath = base ? `${base}/${entry.name}` : entry.name; const fullPath = join(dir, entry.name); if (SKIP.includes(entry.name)) continue; if (entry.isDirectory()) { Object.assign(files, await readTemplateFiles(fullPath, relativePath)); } else { const content = await readFile(fullPath, "utf-8"); files[relativePath] = content; } } return files; } /** * Load a template from the templates directory */ async function loadTemplate(name: string): Promise