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.
- package/README.md +207 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +45 -0
- package/dist/commands/check.d.ts +8 -0
- package/dist/commands/check.js +96 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +1201 -0
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.js +126 -0
- package/dist/types.d.ts +246 -0
- package/dist/types.js +4 -0
- package/dist/utils.d.ts +53 -0
- package/dist/utils.js +142 -0
- package/package.json +65 -0
- package/templates/LINTING_SETUP.md +205 -0
- package/templates/README_TEMPLATE.md +68 -0
- package/templates/STYLE_GUIDE.md +241 -0
- package/templates/assets/favicon.png +0 -0
- package/templates/assets/favicon.svg +17 -0
- package/templates/biome.json +43 -0
- package/templates/cursor/rules/drizzle.mdc +165 -0
- package/templates/cursor/rules/fastify.mdc +132 -0
- package/templates/cursor/rules/general.mdc +112 -0
- package/templates/cursor/rules/nextjs.mdc +89 -0
- package/templates/cursor/rules/python.mdc +89 -0
- package/templates/cursor/rules/react.mdc +200 -0
- package/templates/cursor/rules/sqlalchemy.mdc +205 -0
- package/templates/cursor/rules/tailwind.mdc +139 -0
- package/templates/cursor/rules/typescript.mdc +112 -0
- package/templates/cursor/rules/vite.mdc +169 -0
- package/templates/cursor/rules/workflows.mdc +349 -0
- package/templates/docker-compose.example.yml +55 -0
- package/templates/drizzle/db-index.ts +15 -0
- package/templates/drizzle/drizzle.config.ts +10 -0
- package/templates/drizzle/schema.ts +12 -0
- package/templates/env.example +15 -0
- package/templates/fastapi/app/__init__.py +1 -0
- package/templates/fastapi/app/config.py +12 -0
- package/templates/fastapi/app/database.py +16 -0
- package/templates/fastapi/app/models.py +13 -0
- package/templates/fastapi/main.py +22 -0
- package/templates/fastify/index.ts +40 -0
- package/templates/github/CODEOWNERS +10 -0
- package/templates/github/ISSUE_TEMPLATE/bug_report.md +39 -0
- package/templates/github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/templates/github/PULL_REQUEST_TEMPLATE.md +25 -0
- package/templates/gitignore/node.gitignore +41 -0
- package/templates/gitignore/python.gitignore +49 -0
- package/templates/multi-api/README.md +60 -0
- package/templates/multi-api/gitignore +28 -0
- package/templates/multi-api/node-api/drizzle.config.ts +10 -0
- package/templates/multi-api/node-api/package-simple.json +13 -0
- package/templates/multi-api/node-api/package.json +16 -0
- package/templates/multi-api/node-api/src/db/index.ts +13 -0
- package/templates/multi-api/node-api/src/db/schema.ts +9 -0
- package/templates/multi-api/node-api/src/index-simple.ts +36 -0
- package/templates/multi-api/node-api/src/index.ts +50 -0
- package/templates/multi-api/node-api/tsconfig.json +20 -0
- package/templates/multi-api/python-api/app/__init__.py +1 -0
- package/templates/multi-api/python-api/app/config.py +12 -0
- package/templates/multi-api/python-api/app/database.py +16 -0
- package/templates/multi-api/python-api/app/models.py +13 -0
- package/templates/multi-api/python-api/main-simple.py +25 -0
- package/templates/multi-api/python-api/main.py +44 -0
- package/templates/multi-api/python-api/requirements-simple.txt +3 -0
- package/templates/multi-api/python-api/requirements.txt +8 -0
- package/templates/next/globals.css +126 -0
- package/templates/next/layout.tsx +34 -0
- package/templates/next/next.config.static.ts +10 -0
- package/templates/next/page-fullstack.tsx +120 -0
- package/templates/next/page.tsx +72 -0
- package/templates/presets.json +581 -0
- package/templates/ruff.toml +30 -0
- package/templates/tsconfig.base.json +17 -0
- package/templates/vite/index.css +127 -0
- package/templates/vite/vite.config.ts +7 -0
- package/templates/worker/py/cron.py +53 -0
- package/templates/worker/py/worker.py +95 -0
- package/templates/worker/py/workflow.py +73 -0
- package/templates/worker/ts/cron.ts +49 -0
- package/templates/worker/ts/worker.ts +84 -0
- 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
|
+
}
|