UNPKG

decocms

Version:

CLI for managing deco.chat apps & projects

238 lines 9.48 kB
import inquirer from "inquirer"; import { promises as fs } from "fs"; import { spawn } from "child_process"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { copy, ensureDir } from "../../lib/fs.js"; import { readWranglerConfig, writeWranglerConfig, } from "../../lib/config.js"; import { slugify } from "../../lib/slugify.js"; import { promptWorkspace } from "../../lib/prompt-workspace.js"; import { genEnv } from "../gen/gen.js"; import { promptIDESetup, writeIDEConfig } from "../../lib/prompt-ide-setup.js"; import process from "node:process"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const DEFAULT_TEMPLATE = { name: "Deco MCP app", description: "A Deco MCP app", repo: "deco-cx/deco-create", branch: "main", wranglerRoot: "server", pathsToIgnore: [".cursorindexingignore", ".specstory", "plans"], }; function runCommand(command, args, cwd) { return new Promise((resolve) => { const process = spawn(command, args, { cwd, stdio: "pipe", }); process.on("close", (code) => { resolve(code === 0); }); process.on("error", () => { resolve(false); }); }); } const PATHS_TO_IGNORE_ALWAYS = [".git"]; async function downloadTemplate(template, targetDir) { // For the base template, use the local copy if (template.name === "base") { const templatePath = join(__dirname, "../../../template/base"); await ensureDir(targetDir); await copy(templatePath, targetDir, { overwrite: true }); console.log(`✅ Template '${template.name}' copied successfully!`); return; } // For remote templates, use git clone const tempDir = join(process.cwd(), `.temp-${Date.now()}`); try { const success = await runCommand("git", [ "clone", "--depth", "1", "--branch", template.branch || "main", `https://github.com/${template.repo}.git`, tempDir, ]); if (!success) { throw new Error(`Failed to clone template repository: ${template.repo}`); } const pathsToIgnore = [ ...(template.pathsToIgnore || []), ...PATHS_TO_IGNORE_ALWAYS, ]; for (const path of pathsToIgnore) { const pathToRemove = join(tempDir, path); const isDirectory = await fs .stat(pathToRemove) .then((stat) => stat.isDirectory()); await fs .rm(pathToRemove, isDirectory ? { recursive: true, force: true } : { force: true }) .catch(() => { console.warn(`Failed to remove ${path} from the original template: ${pathToRemove}`); }); } const templatePath = join(tempDir, template.path || ""); try { await fs.access(templatePath); } catch { throw new Error(`Template '${template.name}' not found in repository`); } await ensureDir(targetDir); await copy(templatePath, targetDir, { overwrite: true }); console.log(`✅ Template '${template.name}' downloaded successfully!`); } finally { await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { }); } } async function customizeTemplate({ targetDir, projectName, workspace, wranglerRoot, }) { const packageJsonPath = join(targetDir, "package.json"); try { const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonContent); packageJson.name = projectName; await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); } catch (error) { console.warn("⚠️ Could not customize package.json:", error instanceof Error ? error.message : String(error)); } // Write config file with project name and workspace if (workspace) { try { // Read current config from target directory const currentConfig = await readWranglerConfig(wranglerRoot || targetDir); // For now, use empty bindings - we can enhance this later with prompt integrations const bindings = []; // Merge with new project name and workspace - preserve all existing config const newConfig = { ...currentConfig, name: projectName, scope: workspace, deco: { ...currentConfig.deco, workspace, bindings, }, }; // Write the new config file await writeWranglerConfig(newConfig, wranglerRoot || targetDir); // Generate environment variables file const envContent = await genEnv({ workspace: workspace, local: false, bindings: newConfig.deco.bindings || [], }); const outputPath = join(wranglerRoot || targetDir, "deco.gen.ts"); await fs.writeFile(outputPath, envContent); console.log(`✅ Environment types written to: ${outputPath}`); } catch (error) { console.warn("⚠️ Could not update config file:", error instanceof Error ? error.message : String(error)); } } } export async function createCommand(projectName, config = {}) { try { const selectedTemplate = DEFAULT_TEMPLATE; const finalProjectName = slugify(projectName || (await inquirer.prompt([ { type: "input", name: "projectName", message: "Enter project name:", validate: (value) => { if (!value.trim()) { return "Project name cannot be empty"; } if (!/^[a-z0-9-]+$/.test(value)) { return "Project name can only contain lowercase letters, numbers, and hyphens"; } return true; }, }, ])).projectName); // Prompt user to select workspace let workspace = config?.workspace; try { workspace = await promptWorkspace(config?.local, workspace); console.log(`📁 Selected workspace: ${workspace}`); } catch (error) { console.error(error); console.warn("⚠️ Could not select workspace. Please run 'deco login' to authenticate for a better experience."); // Continue without workspace } const targetDir = join(process.cwd(), finalProjectName); try { await fs.access(targetDir); const { overwrite } = await inquirer.prompt([ { type: "list", name: "overwrite", message: `Directory '${finalProjectName}' already exists. Overwrite?`, choices: ["No", "Yes"], }, ]); if (overwrite === "No") { console.log("❌ Project creation cancelled."); return; } await fs.rm(targetDir, { recursive: true }); } catch { // Directory doesn't exist, that's fine } const wranglerRoot = join(targetDir, selectedTemplate.wranglerRoot || ""); const { initGit } = await inquirer.prompt([ { type: "list", name: "initGit", message: "Initialize a git repository?", choices: ["No", "Yes"], }, ]); // Prompt user to install MCP configuration for IDE const mcpResult = workspace ? await promptIDESetup({ workspace, app: finalProjectName }, targetDir) : null; console.log(`📦 Downloading template '${selectedTemplate.name}'...`); await downloadTemplate(selectedTemplate, targetDir); if (mcpResult) { await writeIDEConfig(mcpResult); } await customizeTemplate({ targetDir, projectName: finalProjectName, workspace, wranglerRoot, }); if (initGit === "Yes") { try { const success = await runCommand("git", ["init"], targetDir); if (success) { console.log(`✅ Git repository initialized in '${finalProjectName}'`); } else { console.warn("⚠️ Failed to initialize git repository"); } } catch (error) { console.warn("⚠️ Could not initialize git repository:", error instanceof Error ? error.message : String(error)); } } console.log(`\n🎉 Project '${finalProjectName}' created successfully!`); console.log(`\nNext steps:`); console.log(` cd ${finalProjectName}`); console.log(` npm install`); console.log(` npm run dev`); } catch (error) { console.error("❌ Failed to create project:", error instanceof Error ? error.message : String(error)); process.exit(1); } } //# sourceMappingURL=create.js.map