render-create 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +207 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.js +45 -0
  4. package/dist/commands/check.d.ts +8 -0
  5. package/dist/commands/check.js +96 -0
  6. package/dist/commands/init.d.ts +12 -0
  7. package/dist/commands/init.js +1201 -0
  8. package/dist/commands/sync.d.ts +8 -0
  9. package/dist/commands/sync.js +126 -0
  10. package/dist/types.d.ts +246 -0
  11. package/dist/types.js +4 -0
  12. package/dist/utils.d.ts +53 -0
  13. package/dist/utils.js +142 -0
  14. package/package.json +65 -0
  15. package/templates/LINTING_SETUP.md +205 -0
  16. package/templates/README_TEMPLATE.md +68 -0
  17. package/templates/STYLE_GUIDE.md +241 -0
  18. package/templates/assets/favicon.png +0 -0
  19. package/templates/assets/favicon.svg +17 -0
  20. package/templates/biome.json +43 -0
  21. package/templates/cursor/rules/drizzle.mdc +165 -0
  22. package/templates/cursor/rules/fastify.mdc +132 -0
  23. package/templates/cursor/rules/general.mdc +112 -0
  24. package/templates/cursor/rules/nextjs.mdc +89 -0
  25. package/templates/cursor/rules/python.mdc +89 -0
  26. package/templates/cursor/rules/react.mdc +200 -0
  27. package/templates/cursor/rules/sqlalchemy.mdc +205 -0
  28. package/templates/cursor/rules/tailwind.mdc +139 -0
  29. package/templates/cursor/rules/typescript.mdc +112 -0
  30. package/templates/cursor/rules/vite.mdc +169 -0
  31. package/templates/cursor/rules/workflows.mdc +349 -0
  32. package/templates/docker-compose.example.yml +55 -0
  33. package/templates/drizzle/db-index.ts +15 -0
  34. package/templates/drizzle/drizzle.config.ts +10 -0
  35. package/templates/drizzle/schema.ts +12 -0
  36. package/templates/env.example +15 -0
  37. package/templates/fastapi/app/__init__.py +1 -0
  38. package/templates/fastapi/app/config.py +12 -0
  39. package/templates/fastapi/app/database.py +16 -0
  40. package/templates/fastapi/app/models.py +13 -0
  41. package/templates/fastapi/main.py +22 -0
  42. package/templates/fastify/index.ts +40 -0
  43. package/templates/github/CODEOWNERS +10 -0
  44. package/templates/github/ISSUE_TEMPLATE/bug_report.md +39 -0
  45. package/templates/github/ISSUE_TEMPLATE/feature_request.md +23 -0
  46. package/templates/github/PULL_REQUEST_TEMPLATE.md +25 -0
  47. package/templates/gitignore/node.gitignore +41 -0
  48. package/templates/gitignore/python.gitignore +49 -0
  49. package/templates/multi-api/README.md +60 -0
  50. package/templates/multi-api/gitignore +28 -0
  51. package/templates/multi-api/node-api/drizzle.config.ts +10 -0
  52. package/templates/multi-api/node-api/package-simple.json +13 -0
  53. package/templates/multi-api/node-api/package.json +16 -0
  54. package/templates/multi-api/node-api/src/db/index.ts +13 -0
  55. package/templates/multi-api/node-api/src/db/schema.ts +9 -0
  56. package/templates/multi-api/node-api/src/index-simple.ts +36 -0
  57. package/templates/multi-api/node-api/src/index.ts +50 -0
  58. package/templates/multi-api/node-api/tsconfig.json +20 -0
  59. package/templates/multi-api/python-api/app/__init__.py +1 -0
  60. package/templates/multi-api/python-api/app/config.py +12 -0
  61. package/templates/multi-api/python-api/app/database.py +16 -0
  62. package/templates/multi-api/python-api/app/models.py +13 -0
  63. package/templates/multi-api/python-api/main-simple.py +25 -0
  64. package/templates/multi-api/python-api/main.py +44 -0
  65. package/templates/multi-api/python-api/requirements-simple.txt +3 -0
  66. package/templates/multi-api/python-api/requirements.txt +8 -0
  67. package/templates/next/globals.css +126 -0
  68. package/templates/next/layout.tsx +34 -0
  69. package/templates/next/next.config.static.ts +10 -0
  70. package/templates/next/page-fullstack.tsx +120 -0
  71. package/templates/next/page.tsx +72 -0
  72. package/templates/presets.json +581 -0
  73. package/templates/ruff.toml +30 -0
  74. package/templates/tsconfig.base.json +17 -0
  75. package/templates/vite/index.css +127 -0
  76. package/templates/vite/vite.config.ts +7 -0
  77. package/templates/worker/py/cron.py +53 -0
  78. package/templates/worker/py/worker.py +95 -0
  79. package/templates/worker/py/workflow.py +73 -0
  80. package/templates/worker/ts/cron.ts +49 -0
  81. package/templates/worker/ts/worker.ts +84 -0
  82. package/templates/worker/ts/workflow.ts +67 -0
@@ -0,0 +1,1201 @@
1
+ /**
2
+ * Init command - Scaffold a new project with dependencies, Cursor rules, and configs
3
+ */
4
+ import { execSync } from "node:child_process";
5
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import chalk from "chalk";
8
+ import inquirer from "inquirer";
9
+ import { copyTemplate, copyTemplateWithVars, ensureDir, loadPresets } from "../utils.js";
10
+ /**
11
+ * Validate project name
12
+ */
13
+ function validateProjectName(name) {
14
+ if (!name) {
15
+ return "Project name is required";
16
+ }
17
+ if (!/^[a-z0-9-_]+$/i.test(name)) {
18
+ return "Project name can only contain letters, numbers, hyphens, and underscores";
19
+ }
20
+ if (existsSync(name)) {
21
+ return `Directory "${name}" already exists`;
22
+ }
23
+ return true;
24
+ }
25
+ /**
26
+ * Get the files to copy based on preset selection
27
+ */
28
+ function getFilesForPreset(preset, _extras) {
29
+ const rules = [...preset.rules];
30
+ const configs = [...preset.configs];
31
+ return { rules, configs };
32
+ }
33
+ /**
34
+ * Run the create command for a preset (e.g., create-next-app)
35
+ */
36
+ function runCreateCommand(createCommand, projectName) {
37
+ const command = createCommand.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
38
+ console.log(chalk.blue(`\nRunning: ${chalk.bold(command)}\n`));
39
+ execSync(command, { stdio: "inherit" });
40
+ }
41
+ /**
42
+ * Add dependencies to an existing project
43
+ */
44
+ function addDependencies(projectDir, deps, dev, packageManager) {
45
+ if (deps.length === 0)
46
+ return;
47
+ const flag = dev ? "-D" : "";
48
+ const depsStr = deps.join(" ");
49
+ const cmd = `${packageManager} install ${flag} ${depsStr}`.trim();
50
+ console.log(chalk.gray(` ${cmd}`));
51
+ execSync(cmd, { cwd: projectDir, stdio: "inherit" });
52
+ }
53
+ /**
54
+ * Add scripts to package.json
55
+ */
56
+ function addScriptsToPackageJson(projectDir, scripts) {
57
+ const pkgPath = join(projectDir, "package.json");
58
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
59
+ pkg.scripts = { ...pkg.scripts, ...scripts };
60
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
61
+ console.log(chalk.green(" Updated package.json scripts"));
62
+ }
63
+ /**
64
+ * Delete files after create command
65
+ */
66
+ function deletePostCreateFiles(projectDir, files) {
67
+ for (const filePath of files) {
68
+ const fullPath = join(projectDir, filePath);
69
+ if (existsSync(fullPath)) {
70
+ unlinkSync(fullPath);
71
+ console.log(chalk.yellow(` Deleted ${filePath}`));
72
+ }
73
+ }
74
+ }
75
+ /**
76
+ * Copy post-create files with variable substitution (text files only)
77
+ */
78
+ function copyPostCreateFiles(projectDir, files, projectName) {
79
+ const binaryExtensions = [
80
+ ".png",
81
+ ".ico",
82
+ ".jpg",
83
+ ".jpeg",
84
+ ".gif",
85
+ ".webp",
86
+ ".woff",
87
+ ".woff2",
88
+ ".ttf",
89
+ ".eot",
90
+ ];
91
+ for (const [targetPath, templatePath] of Object.entries(files)) {
92
+ const fullTargetPath = join(projectDir, targetPath);
93
+ ensureDir(dirname(fullTargetPath));
94
+ const isBinary = binaryExtensions.some((ext) => targetPath.endsWith(ext));
95
+ if (isBinary) {
96
+ // Copy binary files directly without substitution
97
+ copyTemplate(templatePath, fullTargetPath);
98
+ }
99
+ else {
100
+ // Copy text files with variable substitution
101
+ copyTemplateWithVars(templatePath, fullTargetPath, {
102
+ PROJECT_NAME: projectName,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * Generate package.json for presets without createCommand
109
+ */
110
+ function generatePackageJson(projectName, preset) {
111
+ return {
112
+ name: projectName,
113
+ version: "0.1.0",
114
+ private: true,
115
+ type: "module",
116
+ scripts: preset.scripts ?? {},
117
+ dependencies: {},
118
+ devDependencies: {},
119
+ };
120
+ }
121
+ /**
122
+ * Generate requirements.txt content
123
+ */
124
+ function generateRequirementsTxt(preset) {
125
+ return `${(preset.pythonDependencies ?? []).join("\n")}\n`;
126
+ }
127
+ /**
128
+ * Install npm dependencies for presets without createCommand
129
+ */
130
+ function installNpmDependencies(projectDir, preset, packageManager) {
131
+ const deps = preset.dependencies ?? [];
132
+ const devDeps = preset.devDependencies ?? [];
133
+ console.log(chalk.blue("\nInstalling dependencies...\n"));
134
+ if (deps.length > 0) {
135
+ const depsStr = deps.join(" ");
136
+ console.log(chalk.gray(` ${packageManager} install ${depsStr}`));
137
+ execSync(`${packageManager} install ${depsStr}`, {
138
+ cwd: projectDir,
139
+ stdio: "inherit",
140
+ });
141
+ }
142
+ if (devDeps.length > 0) {
143
+ const devDepsStr = devDeps.join(" ");
144
+ console.log(chalk.gray(` ${packageManager} install -D ${devDepsStr}`));
145
+ execSync(`${packageManager} install -D ${devDepsStr}`, {
146
+ cwd: projectDir,
147
+ stdio: "inherit",
148
+ });
149
+ }
150
+ }
151
+ /**
152
+ * Install Python dependencies
153
+ */
154
+ function installPythonDependencies(projectDir) {
155
+ console.log(chalk.blue("\nSetting up Python environment...\n"));
156
+ // Create virtual environment
157
+ console.log(chalk.gray(" Creating virtual environment..."));
158
+ execSync("python3 -m venv .venv", { cwd: projectDir, stdio: "inherit" });
159
+ // Install dependencies
160
+ const pipCmd = process.platform === "win32" ? ".venv\\Scripts\\pip" : ".venv/bin/pip";
161
+ console.log(chalk.gray(` ${pipCmd} install -r requirements.txt`));
162
+ execSync(`${pipCmd} install -r requirements.txt`, {
163
+ cwd: projectDir,
164
+ stdio: "inherit",
165
+ });
166
+ }
167
+ /**
168
+ * Generate render.yaml Blueprint content
169
+ */
170
+ function generateRenderYaml(projectName, preset) {
171
+ const blueprint = preset.blueprint;
172
+ if (!blueprint)
173
+ return "";
174
+ const replacePlaceholders = (str) => str.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
175
+ const generateEnvVarYaml = (envVar, indent) => {
176
+ const lines = [];
177
+ if ("fromDatabase" in envVar) {
178
+ lines.push(`${indent}- key: ${envVar.key}`);
179
+ lines.push(`${indent} fromDatabase:`);
180
+ lines.push(`${indent} name: ${replacePlaceholders(envVar.fromDatabase.name)}`);
181
+ lines.push(`${indent} property: ${envVar.fromDatabase.property}`);
182
+ }
183
+ else if ("fromService" in envVar) {
184
+ lines.push(`${indent}- key: ${envVar.key}`);
185
+ lines.push(`${indent} fromService:`);
186
+ lines.push(`${indent} type: ${envVar.fromService.type}`);
187
+ lines.push(`${indent} name: ${replacePlaceholders(envVar.fromService.name)}`);
188
+ if (envVar.fromService.property) {
189
+ lines.push(`${indent} property: ${envVar.fromService.property}`);
190
+ }
191
+ if (envVar.fromService.envVarKey) {
192
+ lines.push(`${indent} envVarKey: ${envVar.fromService.envVarKey}`);
193
+ }
194
+ }
195
+ else {
196
+ lines.push(`${indent}- key: ${envVar.key}`);
197
+ if (envVar.value !== undefined) {
198
+ lines.push(`${indent} value: "${envVar.value}"`);
199
+ }
200
+ if (envVar.generateValue) {
201
+ lines.push(`${indent} generateValue: true`);
202
+ }
203
+ if (envVar.sync !== undefined) {
204
+ lines.push(`${indent} sync: ${envVar.sync}`);
205
+ }
206
+ }
207
+ return lines;
208
+ };
209
+ const generateServiceYaml = (service, indent) => {
210
+ const lines = [];
211
+ const serviceName = service.name ?? projectName;
212
+ lines.push(`${indent}- type: ${service.type}`);
213
+ lines.push(`${indent} name: ${serviceName}`);
214
+ lines.push(`${indent} runtime: ${service.runtime}`);
215
+ if (service.plan) {
216
+ lines.push(`${indent} plan: ${service.plan}`);
217
+ }
218
+ if (service.rootDir) {
219
+ lines.push(`${indent} rootDir: ${service.rootDir}`);
220
+ }
221
+ if (service.buildCommand) {
222
+ lines.push(`${indent} buildCommand: ${service.buildCommand}`);
223
+ }
224
+ if (service.startCommand) {
225
+ lines.push(`${indent} startCommand: ${service.startCommand}`);
226
+ }
227
+ if (service.staticPublishPath) {
228
+ lines.push(`${indent} staticPublishPath: ${service.staticPublishPath}`);
229
+ }
230
+ if (service.healthCheckPath) {
231
+ lines.push(`${indent} healthCheckPath: ${service.healthCheckPath}`);
232
+ }
233
+ if (service.routes && service.routes.length > 0) {
234
+ lines.push(`${indent} routes:`);
235
+ for (const route of service.routes) {
236
+ lines.push(`${indent} - type: ${route.type}`);
237
+ lines.push(`${indent} source: ${route.source}`);
238
+ lines.push(`${indent} destination: ${route.destination}`);
239
+ }
240
+ }
241
+ if (service.envVars && service.envVars.length > 0) {
242
+ lines.push(`${indent} envVars:`);
243
+ for (const envVar of service.envVars) {
244
+ lines.push(...generateEnvVarYaml(envVar, `${indent} `));
245
+ }
246
+ }
247
+ return lines;
248
+ };
249
+ const generateDatabaseYaml = (db, indent) => {
250
+ const lines = [];
251
+ lines.push(`${indent}- name: ${replacePlaceholders(db.name)}`);
252
+ if (db.plan) {
253
+ lines.push(`${indent} plan: ${db.plan}`);
254
+ }
255
+ if (db.postgresMajorVersion) {
256
+ lines.push(`${indent} postgresMajorVersion: "${db.postgresMajorVersion}"`);
257
+ }
258
+ return lines;
259
+ };
260
+ const yaml = [];
261
+ const hasMultipleResources = (blueprint.services?.length ?? 0) + (blueprint.databases?.length ?? 0) > 1;
262
+ if (hasMultipleResources) {
263
+ yaml.push("# Render Blueprint - https://render.com/docs/blueprint-spec");
264
+ yaml.push("# Uses projects/environments for grouped resource management");
265
+ yaml.push("");
266
+ yaml.push("projects:");
267
+ yaml.push(` - name: ${projectName}`);
268
+ yaml.push(" environments:");
269
+ yaml.push(" - name: production");
270
+ if (blueprint.services && blueprint.services.length > 0) {
271
+ yaml.push(" services:");
272
+ for (const service of blueprint.services) {
273
+ yaml.push(...generateServiceYaml(service, " "));
274
+ }
275
+ }
276
+ if (blueprint.databases && blueprint.databases.length > 0) {
277
+ yaml.push(" databases:");
278
+ for (const db of blueprint.databases) {
279
+ yaml.push(...generateDatabaseYaml(db, " "));
280
+ }
281
+ }
282
+ }
283
+ else {
284
+ yaml.push("# Render Blueprint - https://render.com/docs/blueprint-spec");
285
+ yaml.push("");
286
+ if (blueprint.services && blueprint.services.length > 0) {
287
+ yaml.push("services:");
288
+ for (const service of blueprint.services) {
289
+ yaml.push(...generateServiceYaml(service, " "));
290
+ }
291
+ }
292
+ if (blueprint.databases && blueprint.databases.length > 0) {
293
+ yaml.push("");
294
+ yaml.push("databases:");
295
+ for (const db of blueprint.databases) {
296
+ yaml.push(...generateDatabaseYaml(db, " "));
297
+ }
298
+ }
299
+ }
300
+ return `${yaml.join("\n")}\n`;
301
+ }
302
+ /**
303
+ * Copy config files and Cursor rules
304
+ */
305
+ function copyConfigFiles(projectDir, rules, configs, extras, _projectName) {
306
+ console.log(chalk.blue("\nAdding project configs...\n"));
307
+ // Create .cursor/rules directory
308
+ const rulesDir = join(projectDir, ".cursor", "rules");
309
+ ensureDir(rulesDir);
310
+ // Copy rule files
311
+ for (const rule of rules) {
312
+ copyTemplate(`cursor/rules/${rule}.mdc`, join(rulesDir, `${rule}.mdc`));
313
+ }
314
+ // Copy config files
315
+ for (const config of configs) {
316
+ switch (config) {
317
+ case "biome":
318
+ copyTemplate("biome.json", join(projectDir, "biome.json"));
319
+ break;
320
+ case "ruff":
321
+ copyTemplate("ruff.toml", join(projectDir, "ruff.toml"));
322
+ break;
323
+ case "tsconfig":
324
+ copyTemplate("tsconfig.base.json", join(projectDir, "tsconfig.json"));
325
+ break;
326
+ case "gitignore-node":
327
+ // Only copy if .gitignore doesn't exist (create-next-app creates one)
328
+ if (!existsSync(join(projectDir, ".gitignore"))) {
329
+ copyTemplate("gitignore/node.gitignore", join(projectDir, ".gitignore"));
330
+ }
331
+ break;
332
+ case "gitignore-python":
333
+ copyTemplate("gitignore/python.gitignore", join(projectDir, ".gitignore"));
334
+ break;
335
+ case "github":
336
+ copyGitHubTemplates(projectDir);
337
+ break;
338
+ }
339
+ }
340
+ // Copy extras
341
+ if (extras.includes("env")) {
342
+ copyTemplate("env.example", join(projectDir, ".env.example"));
343
+ }
344
+ if (extras.includes("docker")) {
345
+ copyTemplate("docker-compose.example.yml", join(projectDir, "docker-compose.yml"));
346
+ }
347
+ }
348
+ /**
349
+ * Copy GitHub templates
350
+ */
351
+ function copyGitHubTemplates(projectDir) {
352
+ const githubDir = join(projectDir, ".github");
353
+ const issueDir = join(githubDir, "ISSUE_TEMPLATE");
354
+ ensureDir(issueDir);
355
+ copyTemplate("github/PULL_REQUEST_TEMPLATE.md", join(githubDir, "PULL_REQUEST_TEMPLATE.md"));
356
+ copyTemplate("github/ISSUE_TEMPLATE/bug_report.md", join(issueDir, "bug_report.md"));
357
+ copyTemplate("github/ISSUE_TEMPLATE/feature_request.md", join(issueDir, "feature_request.md"));
358
+ copyTemplate("github/CODEOWNERS", join(githubDir, "CODEOWNERS"));
359
+ }
360
+ /**
361
+ * Initialize git repository (if not already initialized)
362
+ */
363
+ function initGit(projectDir) {
364
+ if (!existsSync(join(projectDir, ".git"))) {
365
+ console.log(chalk.blue("\nInitializing git repository...\n"));
366
+ execSync("git init", { cwd: projectDir, stdio: "pipe" });
367
+ console.log(chalk.green(" Initialized git repository"));
368
+ }
369
+ }
370
+ // ============================================================================
371
+ // Composable Project Scaffolding
372
+ // ============================================================================
373
+ /**
374
+ * Scaffold a frontend component
375
+ */
376
+ async function scaffoldFrontend(projectDir, _componentId, component, projectName, skipInstall, deployType) {
377
+ const subdir = join(projectDir, component.subdir);
378
+ const subdirName = `${projectName}-frontend`;
379
+ console.log(chalk.blue(`\nScaffolding frontend: ${component.name} (${deployType})...\n`));
380
+ // Run create command in parent dir, it creates its own folder
381
+ const createCommand = component.createCommand.replace(/\{\{PROJECT_NAME\}\}/g, subdirName);
382
+ console.log(chalk.gray(` ${createCommand}`));
383
+ execSync(createCommand, { cwd: projectDir, stdio: "inherit" });
384
+ // Rename to subdir name
385
+ const createdDir = join(projectDir, subdirName);
386
+ if (existsSync(createdDir) && createdDir !== subdir) {
387
+ execSync(`mv "${subdirName}" "${component.subdir}"`, { cwd: projectDir, stdio: "pipe" });
388
+ }
389
+ if (!skipInstall) {
390
+ // Add post-create dependencies
391
+ const postDeps = component.postCreateDependencies ?? [];
392
+ const postDevDeps = component.postCreateDevDependencies ?? [];
393
+ if (postDeps.length > 0) {
394
+ addDependencies(subdir, postDeps, false, "npm");
395
+ }
396
+ if (postDevDeps.length > 0) {
397
+ addDependencies(subdir, postDevDeps, true, "npm");
398
+ }
399
+ }
400
+ // Add post-create scripts
401
+ if (component.postCreateScripts) {
402
+ addScriptsToPackageJson(subdir, component.postCreateScripts);
403
+ }
404
+ // Delete unwanted files
405
+ if (component.postCreateDelete) {
406
+ deletePostCreateFiles(subdir, component.postCreateDelete);
407
+ }
408
+ // Copy base post-create files (common to both deploy types)
409
+ if (component.postCreateFiles) {
410
+ copyPostCreateFiles(subdir, component.postCreateFiles, projectName);
411
+ }
412
+ // Copy deploy-type specific files
413
+ if (deployType === "static" && component.postCreateFilesStatic) {
414
+ copyPostCreateFiles(subdir, component.postCreateFilesStatic, projectName);
415
+ }
416
+ else if (deployType === "webservice" && component.postCreateFilesWebservice) {
417
+ copyPostCreateFiles(subdir, component.postCreateFilesWebservice, projectName);
418
+ }
419
+ console.log(chalk.green(` ✓ Frontend scaffolded in ${component.subdir}/ (${deployType})`));
420
+ }
421
+ /**
422
+ * Scaffold an API component
423
+ */
424
+ async function scaffoldApi(projectDir, _componentId, component, projectName, skipInstall) {
425
+ const subdir = join(projectDir, component.subdir);
426
+ ensureDir(subdir);
427
+ console.log(chalk.blue(`\nScaffolding API: ${component.name}...\n`));
428
+ if (component.runtime === "python") {
429
+ // Python API
430
+ const pythonDeps = component.pythonDependencies ?? [];
431
+ writeFileSync(join(subdir, "requirements.txt"), `${pythonDeps.join("\n")}\n`);
432
+ console.log(chalk.green(` Created requirements.txt`));
433
+ if (component.scaffoldFiles) {
434
+ copyPostCreateFiles(subdir, component.scaffoldFiles, projectName);
435
+ }
436
+ if (!skipInstall) {
437
+ installPythonDependencies(subdir);
438
+ }
439
+ }
440
+ else {
441
+ // Node API
442
+ const packageJson = {
443
+ name: `${projectName}-${component.subdir}`,
444
+ version: "0.1.0",
445
+ private: true,
446
+ type: "module",
447
+ scripts: component.scripts ?? {},
448
+ dependencies: {},
449
+ devDependencies: {},
450
+ };
451
+ writeFileSync(join(subdir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
452
+ console.log(chalk.green(` Created package.json`));
453
+ if (component.scaffoldFiles) {
454
+ copyPostCreateFiles(subdir, component.scaffoldFiles, projectName);
455
+ }
456
+ if (!skipInstall) {
457
+ const deps = component.dependencies ?? [];
458
+ const devDeps = component.devDependencies ?? [];
459
+ if (deps.length > 0) {
460
+ console.log(chalk.gray(` npm install ${deps.join(" ")}`));
461
+ execSync(`npm install ${deps.join(" ")}`, { cwd: subdir, stdio: "inherit" });
462
+ }
463
+ if (devDeps.length > 0) {
464
+ console.log(chalk.gray(` npm install -D ${devDeps.join(" ")}`));
465
+ execSync(`npm install -D ${devDeps.join(" ")}`, { cwd: subdir, stdio: "inherit" });
466
+ }
467
+ }
468
+ }
469
+ console.log(chalk.green(` ✓ API scaffolded in ${component.subdir}/`));
470
+ }
471
+ /**
472
+ * Scaffold a worker component
473
+ */
474
+ async function scaffoldWorker(projectDir, _componentId, component, projectName, skipInstall) {
475
+ // Use unique subdir name to avoid conflicts
476
+ const subdirName = component.workerType === "workflow"
477
+ ? `${component.subdir}-${component.runtime === "python" ? "py" : "ts"}`
478
+ : `${component.subdir}-${component.runtime === "python" ? "py" : "ts"}`;
479
+ const subdir = join(projectDir, subdirName);
480
+ ensureDir(subdir);
481
+ console.log(chalk.blue(`\nScaffolding ${component.workerType}: ${component.name}...\n`));
482
+ if (component.runtime === "python") {
483
+ // Python worker
484
+ const pythonDeps = component.pythonDependencies ?? [];
485
+ writeFileSync(join(subdir, "requirements.txt"), `${pythonDeps.join("\n")}\n`);
486
+ console.log(chalk.green(` Created requirements.txt`));
487
+ if (component.scaffoldFiles) {
488
+ copyPostCreateFiles(subdir, component.scaffoldFiles, projectName);
489
+ }
490
+ if (!skipInstall) {
491
+ installPythonDependencies(subdir);
492
+ }
493
+ }
494
+ else {
495
+ // Node worker
496
+ const packageJson = {
497
+ name: `${projectName}-${subdirName}`,
498
+ version: "0.1.0",
499
+ private: true,
500
+ type: "module",
501
+ scripts: component.scripts ?? {},
502
+ dependencies: {},
503
+ devDependencies: {},
504
+ };
505
+ writeFileSync(join(subdir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
506
+ console.log(chalk.green(` Created package.json`));
507
+ // Copy tsconfig for TypeScript workers
508
+ copyTemplate("tsconfig.base.json", join(subdir, "tsconfig.json"));
509
+ if (component.scaffoldFiles) {
510
+ copyPostCreateFiles(subdir, component.scaffoldFiles, projectName);
511
+ }
512
+ if (!skipInstall) {
513
+ const deps = component.dependencies ?? [];
514
+ const devDeps = component.devDependencies ?? [];
515
+ if (deps.length > 0) {
516
+ console.log(chalk.gray(` npm install ${deps.join(" ")}`));
517
+ execSync(`npm install ${deps.join(" ")}`, { cwd: subdir, stdio: "inherit" });
518
+ }
519
+ if (devDeps.length > 0) {
520
+ console.log(chalk.gray(` npm install -D ${devDeps.join(" ")}`));
521
+ execSync(`npm install -D ${devDeps.join(" ")}`, { cwd: subdir, stdio: "inherit" });
522
+ }
523
+ }
524
+ }
525
+ console.log(chalk.green(` ✓ ${component.workerType} scaffolded in ${subdirName}/`));
526
+ }
527
+ /**
528
+ * Generate composed render.yaml Blueprint
529
+ */
530
+ function generateComposedBlueprint(projectName, selection, components) {
531
+ const yaml = [];
532
+ const services = [];
533
+ const databases = [];
534
+ const keyValues = [];
535
+ const replacePlaceholders = (str) => str.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
536
+ // Helper to generate service YAML
537
+ const addService = (name, type, runtime, rootDir, buildCommand, startCommand, staticPublishPath, healthCheckPath, envVars, routes, schedule) => {
538
+ services.push(` - type: ${type}`);
539
+ services.push(` name: ${name}`);
540
+ services.push(` runtime: ${runtime}`);
541
+ services.push(` rootDir: ${rootDir}`);
542
+ if (buildCommand)
543
+ services.push(` buildCommand: ${buildCommand}`);
544
+ if (startCommand)
545
+ services.push(` startCommand: ${startCommand}`);
546
+ if (staticPublishPath)
547
+ services.push(` staticPublishPath: ${staticPublishPath}`);
548
+ if (healthCheckPath)
549
+ services.push(` healthCheckPath: ${healthCheckPath}`);
550
+ if (schedule)
551
+ services.push(` schedule: "${schedule}"`);
552
+ if (routes && routes.length > 0) {
553
+ services.push(` routes:`);
554
+ for (const route of routes) {
555
+ services.push(` - type: ${route.type}`);
556
+ services.push(` source: ${route.source}`);
557
+ services.push(` destination: ${route.destination}`);
558
+ }
559
+ }
560
+ if (envVars && envVars.length > 0) {
561
+ services.push(` envVars:`);
562
+ for (const envVar of envVars) {
563
+ if ("fromDatabase" in envVar) {
564
+ services.push(` - key: ${envVar.key}`);
565
+ services.push(` fromDatabase:`);
566
+ services.push(` name: ${replacePlaceholders(envVar.fromDatabase.name)}`);
567
+ services.push(` property: ${envVar.fromDatabase.property}`);
568
+ }
569
+ else if ("value" in envVar && envVar.value !== undefined) {
570
+ services.push(` - key: ${envVar.key}`);
571
+ services.push(` value: "${envVar.value}"`);
572
+ }
573
+ }
574
+ }
575
+ };
576
+ // Add frontend service
577
+ if (selection.frontend && selection.frontendDeployType) {
578
+ const comp = components.frontends[selection.frontend];
579
+ if (comp) {
580
+ // Select blueprint based on deploy type
581
+ const bp = selection.frontendDeployType === "webservice" && comp.blueprintWebservice
582
+ ? comp.blueprintWebservice
583
+ : comp.blueprintStatic;
584
+ addService(`${projectName}-frontend`, bp.type, bp.runtime, comp.subdir, bp.buildCommand, bp.startCommand, bp.staticPublishPath, bp.healthCheckPath, bp.envVars, bp.routes);
585
+ }
586
+ }
587
+ // Add API services
588
+ for (const apiId of selection.apis) {
589
+ const comp = components.apis[apiId];
590
+ if (comp?.blueprint) {
591
+ const bp = comp.blueprint;
592
+ // Add DATABASE_URL env var if database selected
593
+ const envVars = [...(bp.envVars ?? [])];
594
+ if (selection.database) {
595
+ envVars.push({
596
+ key: "DATABASE_URL",
597
+ fromDatabase: { name: `${projectName}-db`, property: "connectionString" },
598
+ });
599
+ }
600
+ addService(`${projectName}-${comp.subdir}`, bp.type, bp.runtime, comp.subdir, bp.buildCommand, bp.startCommand, undefined, bp.healthCheckPath, envVars);
601
+ }
602
+ }
603
+ // Add worker services (excluding workflows which aren't blueprint-supported yet)
604
+ for (const workerId of selection.workers) {
605
+ const comp = components.workers[workerId];
606
+ if (comp?.blueprint && comp.workerType !== "workflow") {
607
+ const bp = comp.blueprint;
608
+ const subdirName = `${comp.subdir}-${comp.runtime === "python" ? "py" : "ts"}`;
609
+ // Add DATABASE_URL env var if database selected
610
+ const envVars = [...(bp.envVars ?? [])];
611
+ if (selection.database) {
612
+ envVars.push({
613
+ key: "DATABASE_URL",
614
+ fromDatabase: { name: `${projectName}-db`, property: "connectionString" },
615
+ });
616
+ }
617
+ addService(`${projectName}-${subdirName}`, bp.type, bp.runtime, subdirName, bp.buildCommand, bp.startCommand, undefined, undefined, envVars, undefined, bp.schedule);
618
+ }
619
+ }
620
+ // Add database
621
+ if (selection.database) {
622
+ const comp = components.databases[selection.database];
623
+ if (comp?.blueprint) {
624
+ databases.push(` - name: ${replacePlaceholders(comp.blueprint.name)}`);
625
+ if (comp.blueprint.postgresMajorVersion) {
626
+ databases.push(` postgresMajorVersion: "${comp.blueprint.postgresMajorVersion}"`);
627
+ }
628
+ }
629
+ }
630
+ // Add cache/keyval
631
+ if (selection.cache) {
632
+ const comp = components.caches[selection.cache];
633
+ if (comp?.blueprint) {
634
+ keyValues.push(` - name: ${replacePlaceholders(comp.blueprint.name)}`);
635
+ }
636
+ }
637
+ // Build the final YAML
638
+ yaml.push("# Render Blueprint - https://render.com/docs/blueprint-spec");
639
+ yaml.push("# Uses projects/environments for grouped resource management");
640
+ yaml.push("");
641
+ yaml.push("projects:");
642
+ yaml.push(` - name: ${projectName}`);
643
+ yaml.push(" environments:");
644
+ yaml.push(" - name: production");
645
+ if (services.length > 0) {
646
+ yaml.push(" services:");
647
+ yaml.push(...services);
648
+ }
649
+ if (databases.length > 0) {
650
+ yaml.push(" databases:");
651
+ yaml.push(...databases);
652
+ }
653
+ if (keyValues.length > 0) {
654
+ yaml.push(" keyValues:");
655
+ yaml.push(...keyValues);
656
+ }
657
+ return `${yaml.join("\n")}\n`;
658
+ }
659
+ /**
660
+ * Collect and merge rules from all selected components
661
+ */
662
+ /**
663
+ * Check if any workflow components are selected
664
+ */
665
+ function hasWorkflowSelected(selection, components) {
666
+ return selection.workers.some((workerId) => {
667
+ const comp = components.workers[workerId];
668
+ return comp?.workerType === "workflow";
669
+ });
670
+ }
671
+ function collectRules(selection, components) {
672
+ const rules = new Set(["general"]);
673
+ const hasWorkflows = hasWorkflowSelected(selection, components);
674
+ if (selection.frontend) {
675
+ const comp = components.frontends[selection.frontend];
676
+ for (const rule of comp?.rules ?? []) {
677
+ rules.add(rule);
678
+ }
679
+ }
680
+ for (const apiId of selection.apis) {
681
+ const comp = components.apis[apiId];
682
+ for (const rule of comp?.rules ?? []) {
683
+ rules.add(rule);
684
+ }
685
+ // Add workflows rule to APIs when workflows are selected
686
+ if (hasWorkflows) {
687
+ rules.add("workflows");
688
+ }
689
+ }
690
+ for (const workerId of selection.workers) {
691
+ const comp = components.workers[workerId];
692
+ for (const rule of comp?.rules ?? []) {
693
+ rules.add(rule);
694
+ }
695
+ }
696
+ return Array.from(rules);
697
+ }
698
+ /**
699
+ * Collect configs from all selected components
700
+ */
701
+ function collectConfigs(selection, components) {
702
+ const configs = new Set();
703
+ if (selection.frontend) {
704
+ const comp = components.frontends[selection.frontend];
705
+ for (const config of comp?.configs ?? []) {
706
+ configs.add(config);
707
+ }
708
+ }
709
+ for (const apiId of selection.apis) {
710
+ const comp = components.apis[apiId];
711
+ for (const config of comp?.configs ?? []) {
712
+ configs.add(config);
713
+ }
714
+ }
715
+ for (const workerId of selection.workers) {
716
+ const comp = components.workers[workerId];
717
+ for (const config of comp?.configs ?? []) {
718
+ configs.add(config);
719
+ }
720
+ }
721
+ return Array.from(configs);
722
+ }
723
+ /**
724
+ * Scaffold a composable project with selected components
725
+ */
726
+ export async function scaffoldComposableProject(selection, components, skipInstall) {
727
+ const projectDir = resolve(process.cwd(), selection.projectName);
728
+ console.log(chalk.blue(`\nCreating composable project: ${chalk.bold(selection.projectName)}\n`));
729
+ ensureDir(projectDir);
730
+ // Create root README
731
+ const structureLines = [];
732
+ if (selection.frontend) {
733
+ structureLines.push("- `frontend/` - Frontend application");
734
+ }
735
+ for (const apiId of selection.apis) {
736
+ const comp = components.apis[apiId];
737
+ structureLines.push(`- \`${comp?.subdir}/\` - ${comp?.name}`);
738
+ }
739
+ for (const workerId of selection.workers) {
740
+ const comp = components.workers[workerId];
741
+ const subdirName = `${comp?.subdir}-${comp?.runtime === "python" ? "py" : "ts"}`;
742
+ structureLines.push(`- \`${subdirName}/\` - ${comp?.name}`);
743
+ }
744
+ const readmeContent = `# ${selection.projectName}
745
+
746
+ A composable demo project scaffolded with render-demo.
747
+
748
+ ## Structure
749
+
750
+ ${structureLines.join("\n")}
751
+
752
+ ## Getting Started
753
+
754
+ 1. Set up environment variables (copy \`.env.example\` to \`.env\` in each service)
755
+ 2. Install dependencies in each service directory
756
+ 3. Run \`render blueprint launch\` to deploy to Render
757
+
758
+ ## Deploy to Render
759
+
760
+ This project includes a \`render.yaml\` Blueprint for easy deployment.
761
+ `;
762
+ writeFileSync(join(projectDir, "README.md"), readmeContent);
763
+ // Create root .gitignore
764
+ const gitignoreContent = `# Dependencies
765
+ node_modules/
766
+ .venv/
767
+ __pycache__/
768
+
769
+ # Build outputs
770
+ dist/
771
+ build/
772
+ out/
773
+ .next/
774
+
775
+ # Environment
776
+ .env
777
+ .env.local
778
+ .env.*.local
779
+
780
+ # IDE
781
+ .idea/
782
+ .vscode/
783
+ *.swp
784
+ *.swo
785
+
786
+ # OS
787
+ .DS_Store
788
+ Thumbs.db
789
+ `;
790
+ writeFileSync(join(projectDir, ".gitignore"), gitignoreContent);
791
+ // Scaffold frontend
792
+ if (selection.frontend && selection.frontendDeployType) {
793
+ const comp = components.frontends[selection.frontend];
794
+ if (comp) {
795
+ await scaffoldFrontend(projectDir, selection.frontend, comp, selection.projectName, skipInstall, selection.frontendDeployType);
796
+ }
797
+ }
798
+ // Scaffold APIs
799
+ const hasWorkflows = hasWorkflowSelected(selection, components);
800
+ for (const apiId of selection.apis) {
801
+ const comp = components.apis[apiId];
802
+ if (comp) {
803
+ // Add Render SDK dependency when workflows are selected
804
+ const apiComp = hasWorkflows
805
+ ? {
806
+ ...comp,
807
+ dependencies: comp.runtime === "node"
808
+ ? [...(comp.dependencies ?? []), "@renderinc/sdk"]
809
+ : comp.dependencies,
810
+ pythonDependencies: comp.runtime === "python"
811
+ ? [...(comp.pythonDependencies ?? []), "render-sdk"]
812
+ : comp.pythonDependencies,
813
+ }
814
+ : comp;
815
+ await scaffoldApi(projectDir, apiId, apiComp, selection.projectName, skipInstall);
816
+ }
817
+ }
818
+ // Scaffold workers
819
+ for (const workerId of selection.workers) {
820
+ const comp = components.workers[workerId];
821
+ if (comp) {
822
+ await scaffoldWorker(projectDir, workerId, comp, selection.projectName, skipInstall);
823
+ }
824
+ }
825
+ // Generate render.yaml Blueprint
826
+ const hasServices = selection.frontend || selection.apis.length > 0 || selection.workers.length > 0;
827
+ if (hasServices || selection.database || selection.cache) {
828
+ const renderYaml = generateComposedBlueprint(selection.projectName, selection, components);
829
+ writeFileSync(join(projectDir, "render.yaml"), renderYaml);
830
+ console.log(chalk.green(`\n Created render.yaml`));
831
+ }
832
+ // Copy Cursor rules to root
833
+ const rules = collectRules(selection, components);
834
+ const rulesDir = join(projectDir, ".cursor", "rules");
835
+ ensureDir(rulesDir);
836
+ for (const rule of rules) {
837
+ try {
838
+ copyTemplate(`cursor/rules/${rule}.mdc`, join(rulesDir, `${rule}.mdc`));
839
+ }
840
+ catch {
841
+ // Rule file might not exist, skip
842
+ }
843
+ }
844
+ console.log(chalk.green(` Added ${rules.length} Cursor rules`));
845
+ // Copy config files to root
846
+ const configs = collectConfigs(selection, components);
847
+ for (const config of configs) {
848
+ switch (config) {
849
+ case "biome":
850
+ copyTemplate("biome.json", join(projectDir, "biome.json"));
851
+ console.log(chalk.green(` Created biome.json`));
852
+ break;
853
+ case "ruff":
854
+ copyTemplate("ruff.toml", join(projectDir, "ruff.toml"));
855
+ console.log(chalk.green(` Created ruff.toml`));
856
+ break;
857
+ case "tsconfig":
858
+ // tsconfig is per-subproject, not root
859
+ break;
860
+ case "gitignore-node":
861
+ if (!existsSync(join(projectDir, ".gitignore"))) {
862
+ copyTemplate("gitignore/node.gitignore", join(projectDir, ".gitignore"));
863
+ console.log(chalk.green(` Created .gitignore`));
864
+ }
865
+ break;
866
+ case "gitignore-python":
867
+ if (!existsSync(join(projectDir, ".gitignore"))) {
868
+ copyTemplate("gitignore/python.gitignore", join(projectDir, ".gitignore"));
869
+ console.log(chalk.green(` Created .gitignore`));
870
+ }
871
+ break;
872
+ }
873
+ }
874
+ // Copy extras
875
+ if (selection.extras.includes("env")) {
876
+ const envContent = `# Environment variables
877
+ # Copy this file to .env and fill in the values
878
+
879
+ DATABASE_URL=
880
+ RENDER_API_KEY=
881
+ `;
882
+ writeFileSync(join(projectDir, ".env.example"), envContent);
883
+ console.log(chalk.green(` Created .env.example`));
884
+ }
885
+ if (selection.extras.includes("docker")) {
886
+ copyTemplate("docker-compose.example.yml", join(projectDir, "docker-compose.yml"));
887
+ console.log(chalk.green(` Created docker-compose.yml`));
888
+ }
889
+ // Initialize git
890
+ initGit(projectDir);
891
+ // Done!
892
+ console.log(chalk.green("\n✓ Composable project created successfully!\n"));
893
+ console.log(chalk.white("Project structure:\n"));
894
+ console.log(chalk.cyan(` ${selection.projectName}/`));
895
+ if (selection.frontend) {
896
+ console.log(chalk.cyan(` frontend/`));
897
+ }
898
+ for (const apiId of selection.apis) {
899
+ const comp = components.apis[apiId];
900
+ if (comp) {
901
+ console.log(chalk.cyan(` ${comp.subdir}/`));
902
+ }
903
+ }
904
+ for (const workerId of selection.workers) {
905
+ const comp = components.workers[workerId];
906
+ if (comp) {
907
+ const subdirName = `${comp.subdir}-${comp.runtime === "python" ? "py" : "ts"}`;
908
+ console.log(chalk.cyan(` ${subdirName}/`));
909
+ }
910
+ }
911
+ console.log(chalk.white("\nNext steps:\n"));
912
+ console.log(chalk.cyan(` cd ${selection.projectName}`));
913
+ console.log(chalk.cyan(` # Start each service in its directory`));
914
+ console.log(chalk.cyan(` render blueprint launch # Deploy to Render`));
915
+ console.log();
916
+ }
917
+ /**
918
+ * Main init command handler
919
+ */
920
+ export async function init(nameArg, options) {
921
+ const presetsConfig = loadPresets();
922
+ const presetChoices = Object.entries(presetsConfig.presets).map(([id, preset]) => ({
923
+ name: `${preset.name} (${preset.description})`,
924
+ value: id,
925
+ }));
926
+ presetChoices.push({
927
+ name: "Custom (pick individual components)",
928
+ value: "custom",
929
+ });
930
+ let projectName = nameArg ?? options.name;
931
+ let selectedPreset = null;
932
+ let selectedPresetId = null;
933
+ let selectedRules = [];
934
+ let selectedConfigs = [];
935
+ let selectedExtras = [];
936
+ // Interactive prompts
937
+ if (!projectName || !options.preset) {
938
+ // biome-ignore lint/suspicious/noExplicitAny: inquirer types are complex
939
+ const questions = [];
940
+ if (!projectName) {
941
+ questions.push({
942
+ type: "input",
943
+ name: "projectName",
944
+ message: "What is your project name?",
945
+ validate: validateProjectName,
946
+ default: "my-demo",
947
+ });
948
+ }
949
+ if (!options.preset) {
950
+ questions.push({
951
+ type: "list",
952
+ name: "preset",
953
+ message: "Select a stack preset:",
954
+ choices: presetChoices,
955
+ });
956
+ questions.push({
957
+ type: "checkbox",
958
+ name: "extras",
959
+ message: "Include extras:",
960
+ choices: [
961
+ { name: ".env.example template", value: "env", checked: true },
962
+ { name: "docker-compose.yml", value: "docker", checked: false },
963
+ ],
964
+ });
965
+ }
966
+ const answers = await inquirer.prompt(questions);
967
+ projectName = projectName ?? answers.projectName;
968
+ selectedPresetId = options.preset ?? answers.preset;
969
+ selectedExtras = answers.extras ?? (options.yes ? ["env"] : []);
970
+ }
971
+ else {
972
+ selectedPresetId = options.preset;
973
+ selectedExtras = options.yes ? ["env"] : [];
974
+ }
975
+ // Validate preset
976
+ if (selectedPresetId && selectedPresetId !== "custom") {
977
+ selectedPreset = presetsConfig.presets[selectedPresetId] ?? null;
978
+ if (!selectedPreset) {
979
+ console.log(chalk.red(`Unknown preset: ${selectedPresetId}`));
980
+ console.log(chalk.yellow(`Available presets: ${Object.keys(presetsConfig.presets).join(", ")}`));
981
+ process.exit(1);
982
+ }
983
+ }
984
+ // Handle custom/composable preset
985
+ if (selectedPresetId === "custom") {
986
+ const components = presetsConfig.components;
987
+ if (!components) {
988
+ console.log(chalk.red("Components configuration not found."));
989
+ process.exit(1);
990
+ }
991
+ // Build choices for each component category
992
+ const frontendChoices = [
993
+ { name: "None", value: "none" },
994
+ ...Object.entries(components.frontends).map(([id, comp]) => ({
995
+ name: `${comp.name} - ${comp.description}`,
996
+ value: id,
997
+ })),
998
+ ];
999
+ const apiChoices = Object.entries(components.apis).map(([id, comp]) => ({
1000
+ name: `${comp.name} - ${comp.description}`,
1001
+ value: id,
1002
+ }));
1003
+ const workerChoices = Object.entries(components.workers).map(([id, comp]) => ({
1004
+ name: `${comp.name} - ${comp.description}`,
1005
+ value: id,
1006
+ }));
1007
+ const databaseChoices = [
1008
+ { name: "None", value: "none" },
1009
+ ...Object.entries(components.databases).map(([id, comp]) => ({
1010
+ name: `${comp.name} - ${comp.description}`,
1011
+ value: id,
1012
+ })),
1013
+ ];
1014
+ const cacheChoices = [
1015
+ { name: "None", value: "none" },
1016
+ ...Object.entries(components.caches).map(([id, comp]) => ({
1017
+ name: `${comp.name} - ${comp.description}`,
1018
+ value: id,
1019
+ })),
1020
+ ];
1021
+ // Step 1: Select frontend
1022
+ const { frontend } = await inquirer.prompt([
1023
+ {
1024
+ type: "list",
1025
+ name: "frontend",
1026
+ message: "Select frontend:",
1027
+ choices: frontendChoices,
1028
+ },
1029
+ ]);
1030
+ // Step 2: If frontend selected and supports webservice, ask for deploy type
1031
+ let frontendDeployType = null;
1032
+ if (frontend !== "none") {
1033
+ const frontendComp = components.frontends[frontend];
1034
+ if (frontendComp?.supportsWebservice !== false) {
1035
+ // Frontend supports both static and webservice
1036
+ const { deployType } = await inquirer.prompt([
1037
+ {
1038
+ type: "list",
1039
+ name: "deployType",
1040
+ message: "Deploy frontend as:",
1041
+ choices: [
1042
+ { name: "Static Site - CDN-hosted, fast, no server-side features", value: "static" },
1043
+ {
1044
+ name: "Web Service - Server-side rendering, API routes, dynamic",
1045
+ value: "webservice",
1046
+ },
1047
+ ],
1048
+ },
1049
+ ]);
1050
+ frontendDeployType = deployType;
1051
+ }
1052
+ else {
1053
+ // Frontend only supports static (e.g., Vite SPA)
1054
+ frontendDeployType = "static";
1055
+ }
1056
+ }
1057
+ // Step 3: Rest of the prompts
1058
+ const restAnswers = await inquirer.prompt([
1059
+ {
1060
+ type: "checkbox",
1061
+ name: "apis",
1062
+ message: "Select API backends (optional, press Enter to skip):",
1063
+ choices: apiChoices,
1064
+ },
1065
+ {
1066
+ type: "checkbox",
1067
+ name: "workers",
1068
+ message: "Select background workers (optional, press Enter to skip):",
1069
+ choices: workerChoices,
1070
+ },
1071
+ {
1072
+ type: "list",
1073
+ name: "database",
1074
+ message: "Add database?",
1075
+ choices: databaseChoices,
1076
+ },
1077
+ {
1078
+ type: "list",
1079
+ name: "cache",
1080
+ message: "Add cache?",
1081
+ choices: cacheChoices,
1082
+ },
1083
+ {
1084
+ type: "checkbox",
1085
+ name: "extras",
1086
+ message: "Include extras:",
1087
+ choices: [
1088
+ { name: ".env.example template", value: "env", checked: true },
1089
+ { name: "docker-compose.yml", value: "docker", checked: false },
1090
+ ],
1091
+ },
1092
+ ]);
1093
+ const selection = {
1094
+ projectName,
1095
+ frontend: frontend === "none" ? null : frontend,
1096
+ frontendDeployType,
1097
+ apis: restAnswers.apis,
1098
+ workers: restAnswers.workers,
1099
+ database: restAnswers.database === "none" ? null : restAnswers.database,
1100
+ cache: restAnswers.cache === "none" ? null : restAnswers.cache,
1101
+ extras: restAnswers.extras,
1102
+ };
1103
+ // Scaffold the composable project
1104
+ await scaffoldComposableProject(selection, components, options.skipInstall ?? false);
1105
+ return;
1106
+ }
1107
+ // Get files for preset
1108
+ if (selectedPreset) {
1109
+ const files = getFilesForPreset(selectedPreset, selectedExtras);
1110
+ selectedRules = files.rules;
1111
+ selectedConfigs = files.configs;
1112
+ }
1113
+ const projectDir = resolve(process.cwd(), projectName);
1114
+ const isPython = selectedPreset?.packageManager === "pip";
1115
+ const hasCreateCommand = !!selectedPreset?.createCommand;
1116
+ // === SCAFFOLDING FLOW ===
1117
+ if (hasCreateCommand && selectedPreset?.createCommand) {
1118
+ // Flow 1: Use official create command (create-next-app, create-vite, etc.)
1119
+ runCreateCommand(selectedPreset.createCommand, projectName);
1120
+ if (!options.skipInstall) {
1121
+ // Add post-create dependencies
1122
+ const postDeps = selectedPreset.postCreateDependencies ?? [];
1123
+ const postDevDeps = selectedPreset.postCreateDevDependencies ?? [];
1124
+ const packageManager = selectedPreset.packageManager ?? "npm";
1125
+ if (postDeps.length > 0 || postDevDeps.length > 0) {
1126
+ console.log(chalk.blue("\nAdding additional dependencies...\n"));
1127
+ addDependencies(projectDir, postDeps, false, packageManager);
1128
+ addDependencies(projectDir, postDevDeps, true, packageManager);
1129
+ }
1130
+ }
1131
+ // Add post-create scripts
1132
+ if (selectedPreset.postCreateScripts) {
1133
+ addScriptsToPackageJson(projectDir, selectedPreset.postCreateScripts);
1134
+ }
1135
+ // Delete unwanted files from create command
1136
+ if (selectedPreset.postCreateDelete) {
1137
+ deletePostCreateFiles(projectDir, selectedPreset.postCreateDelete);
1138
+ }
1139
+ // Copy post-create files (e.g., Drizzle config)
1140
+ if (selectedPreset.postCreateFiles) {
1141
+ console.log(chalk.blue("\nAdding additional files...\n"));
1142
+ copyPostCreateFiles(projectDir, selectedPreset.postCreateFiles, projectName);
1143
+ }
1144
+ }
1145
+ else if (isPython && selectedPreset) {
1146
+ // Flow 2: Python project (no create command)
1147
+ console.log(chalk.blue(`\nCreating project in ${chalk.bold(projectDir)}...\n`));
1148
+ ensureDir(projectDir);
1149
+ const requirementsTxt = generateRequirementsTxt(selectedPreset);
1150
+ writeFileSync(join(projectDir, "requirements.txt"), requirementsTxt);
1151
+ console.log(chalk.green(` Created requirements.txt`));
1152
+ // Copy scaffold files
1153
+ if (selectedPreset.scaffoldFiles) {
1154
+ copyPostCreateFiles(projectDir, selectedPreset.scaffoldFiles, projectName);
1155
+ }
1156
+ if (!options.skipInstall) {
1157
+ installPythonDependencies(projectDir);
1158
+ }
1159
+ }
1160
+ else if (selectedPreset) {
1161
+ // Flow 3: Node project without create command (e.g., Fastify API)
1162
+ console.log(chalk.blue(`\nCreating project in ${chalk.bold(projectDir)}...\n`));
1163
+ ensureDir(projectDir);
1164
+ const packageJson = generatePackageJson(projectName, selectedPreset);
1165
+ writeFileSync(join(projectDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
1166
+ console.log(chalk.green(` Created package.json`));
1167
+ // Copy scaffold files
1168
+ if (selectedPreset.scaffoldFiles) {
1169
+ copyPostCreateFiles(projectDir, selectedPreset.scaffoldFiles, projectName);
1170
+ }
1171
+ if (!options.skipInstall) {
1172
+ const packageManager = selectedPreset.packageManager ?? "npm";
1173
+ installNpmDependencies(projectDir, selectedPreset, packageManager);
1174
+ }
1175
+ }
1176
+ // Generate render.yaml Blueprint
1177
+ if (selectedPreset?.blueprint) {
1178
+ const renderYaml = generateRenderYaml(projectName, selectedPreset);
1179
+ writeFileSync(join(projectDir, "render.yaml"), renderYaml);
1180
+ console.log(chalk.green(` Created render.yaml`));
1181
+ }
1182
+ // Copy config files and Cursor rules
1183
+ copyConfigFiles(projectDir, selectedRules, selectedConfigs, selectedExtras, projectName);
1184
+ // Initialize git (if not already done by create command)
1185
+ initGit(projectDir);
1186
+ // Done!
1187
+ console.log(chalk.green("\n✓ Project created successfully!\n"));
1188
+ console.log(chalk.white("Next steps:\n"));
1189
+ console.log(chalk.cyan(` cd ${projectName}`));
1190
+ if (isPython) {
1191
+ console.log(chalk.cyan(" source .venv/bin/activate"));
1192
+ console.log(chalk.cyan(" uvicorn main:app --reload"));
1193
+ }
1194
+ else {
1195
+ if (options.skipInstall && !hasCreateCommand) {
1196
+ console.log(chalk.cyan(" npm install"));
1197
+ }
1198
+ console.log(chalk.cyan(" npm run dev"));
1199
+ }
1200
+ console.log();
1201
+ }