create-filament 1.0.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 +417 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +937 -0
- package/package.json +38 -0
- package/templates/api/src/config/app.ts +17 -0
- package/templates/api/src/handlers/errors.ts +32 -0
- package/templates/api/src/handlers/finalizers.ts +27 -0
- package/templates/api/src/handlers/transforms.ts +18 -0
- package/templates/api/src/index.ts +45 -0
- package/templates/api/src/meta/defaults.ts +28 -0
- package/templates/api/src/meta/index.ts +50 -0
- package/templates/api/src/middleware/index.ts +36 -0
- package/templates/api/src/routes/index.ts +26 -0
- package/templates/api/tests/integration/example.test.ts +20 -0
- package/templates/full/src/config/app.ts +17 -0
- package/templates/full/src/handlers/errors.ts +32 -0
- package/templates/full/src/handlers/finalizers.ts +27 -0
- package/templates/full/src/handlers/transforms.ts +18 -0
- package/templates/full/src/index.ts +45 -0
- package/templates/full/src/meta/defaults.ts +28 -0
- package/templates/full/src/meta/index.ts +31 -0
- package/templates/full/src/middleware/index.ts +36 -0
- package/templates/full/src/routes/index.ts +26 -0
- package/templates/full/tests/integration/example.test.ts +20 -0
- package/templates/minimal/src/config/app.ts +17 -0
- package/templates/minimal/src/handlers/errors.ts +32 -0
- package/templates/minimal/src/handlers/finalizers.ts +27 -0
- package/templates/minimal/src/handlers/transforms.ts +18 -0
- package/templates/minimal/src/index.ts +45 -0
- package/templates/minimal/src/meta/defaults.ts +28 -0
- package/templates/minimal/src/meta/index.ts +31 -0
- package/templates/minimal/src/middleware/index.ts +36 -0
- package/templates/minimal/src/routes/index.ts +26 -0
- package/templates/minimal/tests/integration/example.test.ts +20 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, resolve } from 'path';
|
|
4
|
+
import { existsSync, mkdirSync, cpSync, writeFileSync } from 'fs';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import prompts from 'prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const TEMPLATES_DIR = resolve(__dirname, '../templates');
|
|
12
|
+
function printBanner() {
|
|
13
|
+
console.log(chalk.cyan(`
|
|
14
|
+
_____ _ _ _
|
|
15
|
+
| ___(_) | __ _ _ __ ___ ___ _ __ | |_
|
|
16
|
+
| |_ | | |/ _\` | '_ \` _ \\ / _ \\ '_ \\| __|
|
|
17
|
+
| _| | | | (_| | | | | | | __/ | | | |_
|
|
18
|
+
|_| |_|_|\\__,_|_| |_| |_|\\___|_| |_|\\__|
|
|
19
|
+
`));
|
|
20
|
+
console.log(chalk.gray(' A TypeScript API framework with metadata-driven middleware\n'));
|
|
21
|
+
}
|
|
22
|
+
async function promptForConfig(projectName) {
|
|
23
|
+
const questions = [];
|
|
24
|
+
if (!projectName) {
|
|
25
|
+
questions.push({
|
|
26
|
+
type: 'text',
|
|
27
|
+
name: 'name',
|
|
28
|
+
message: 'Project name:',
|
|
29
|
+
initial: 'my-api',
|
|
30
|
+
validate: (value) => {
|
|
31
|
+
if (!value)
|
|
32
|
+
return 'Project name is required';
|
|
33
|
+
if (!/^[a-z0-9-_]+$/.test(value)) {
|
|
34
|
+
return 'Project name can only contain lowercase letters, numbers, hyphens, and underscores';
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
questions.push({
|
|
41
|
+
type: 'select',
|
|
42
|
+
name: 'template',
|
|
43
|
+
message: 'Choose a template:',
|
|
44
|
+
choices: [
|
|
45
|
+
{ title: 'minimal - Core framework + tooling', value: 'minimal' },
|
|
46
|
+
{ title: 'api - + Auth + RBAC + OpenAPI + Redis', value: 'api' },
|
|
47
|
+
{ title: 'full - + Observability + Analytics + CI/CD', value: 'full' }
|
|
48
|
+
],
|
|
49
|
+
initial: 0
|
|
50
|
+
}, {
|
|
51
|
+
type: (prev) => prev === 'minimal' ? 'multiselect' : null,
|
|
52
|
+
name: 'additionalFeatures',
|
|
53
|
+
message: 'Additional features:',
|
|
54
|
+
choices: [
|
|
55
|
+
{ title: 'Docker support', value: 'docker', selected: true },
|
|
56
|
+
{ title: 'Docker Compose (app + Redis)', value: 'dockerCompose' },
|
|
57
|
+
{ title: 'GitHub Actions CI/CD', value: 'ci-github', selected: true },
|
|
58
|
+
{ title: 'OpenAPI/Swagger generation', value: 'openapi', selected: true },
|
|
59
|
+
{ title: 'Authentication (JWT + OAuth + Sessions)', value: 'auth' },
|
|
60
|
+
{ title: 'Observability (OpenTelemetry + Metrics)', value: 'observability' }
|
|
61
|
+
],
|
|
62
|
+
instructions: 'Space to select, Enter to continue'
|
|
63
|
+
}, {
|
|
64
|
+
type: 'select',
|
|
65
|
+
name: 'packageManager',
|
|
66
|
+
message: 'Package manager:',
|
|
67
|
+
choices: [
|
|
68
|
+
{ title: 'npm', value: 'npm' },
|
|
69
|
+
{ title: 'pnpm', value: 'pnpm' },
|
|
70
|
+
{ title: 'yarn', value: 'yarn' },
|
|
71
|
+
{ title: 'bun', value: 'bun' }
|
|
72
|
+
],
|
|
73
|
+
initial: 0
|
|
74
|
+
}, {
|
|
75
|
+
type: 'confirm',
|
|
76
|
+
name: 'git',
|
|
77
|
+
message: 'Initialize git repository?',
|
|
78
|
+
initial: true
|
|
79
|
+
}, {
|
|
80
|
+
type: (prev) => prev ? 'confirm' : null,
|
|
81
|
+
name: 'gitCommit',
|
|
82
|
+
message: 'Create initial commit?',
|
|
83
|
+
initial: true
|
|
84
|
+
});
|
|
85
|
+
const answers = await prompts(questions, {
|
|
86
|
+
onCancel: () => {
|
|
87
|
+
console.log(chalk.red('\nā Operation cancelled'));
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Parse features based on template and additional selections
|
|
92
|
+
let features = {
|
|
93
|
+
docker: false,
|
|
94
|
+
dockerCompose: false,
|
|
95
|
+
ci: 'none',
|
|
96
|
+
openapi: false,
|
|
97
|
+
auth: false,
|
|
98
|
+
observability: false
|
|
99
|
+
};
|
|
100
|
+
if (answers.template === 'api' || answers.template === 'full') {
|
|
101
|
+
features.docker = true;
|
|
102
|
+
features.dockerCompose = true;
|
|
103
|
+
features.ci = 'github';
|
|
104
|
+
features.openapi = true;
|
|
105
|
+
features.auth = true;
|
|
106
|
+
}
|
|
107
|
+
if (answers.template === 'full') {
|
|
108
|
+
features.observability = true;
|
|
109
|
+
}
|
|
110
|
+
// Override with additional features for minimal template
|
|
111
|
+
if (answers.template === 'minimal' && answers.additionalFeatures) {
|
|
112
|
+
features.docker = answers.additionalFeatures.includes('docker');
|
|
113
|
+
features.dockerCompose = answers.additionalFeatures.includes('dockerCompose');
|
|
114
|
+
features.ci = answers.additionalFeatures.includes('ci-github') ? 'github' : 'none';
|
|
115
|
+
features.openapi = answers.additionalFeatures.includes('openapi');
|
|
116
|
+
features.auth = answers.additionalFeatures.includes('auth');
|
|
117
|
+
features.observability = answers.additionalFeatures.includes('observability');
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
name: projectName || answers.name,
|
|
121
|
+
template: answers.template,
|
|
122
|
+
features,
|
|
123
|
+
packageManager: answers.packageManager,
|
|
124
|
+
git: answers.git,
|
|
125
|
+
gitCommit: answers.gitCommit,
|
|
126
|
+
install: true
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function createProjectStructure(projectPath, config) {
|
|
130
|
+
const spinner = ora('Creating project structure...').start();
|
|
131
|
+
try {
|
|
132
|
+
// Create base directories
|
|
133
|
+
mkdirSync(projectPath, { recursive: true });
|
|
134
|
+
mkdirSync(resolve(projectPath, 'src'), { recursive: true });
|
|
135
|
+
mkdirSync(resolve(projectPath, 'src/meta'), { recursive: true });
|
|
136
|
+
mkdirSync(resolve(projectPath, 'src/middleware'), { recursive: true });
|
|
137
|
+
mkdirSync(resolve(projectPath, 'src/routes'), { recursive: true });
|
|
138
|
+
mkdirSync(resolve(projectPath, 'src/handlers'), { recursive: true });
|
|
139
|
+
mkdirSync(resolve(projectPath, 'src/config'), { recursive: true });
|
|
140
|
+
mkdirSync(resolve(projectPath, 'tests/integration'), { recursive: true });
|
|
141
|
+
mkdirSync(resolve(projectPath, 'tests/unit'), { recursive: true });
|
|
142
|
+
spinner.succeed('Created project structure');
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
spinner.fail('Failed to create project structure');
|
|
147
|
+
console.error(error);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function copyTemplateFiles(projectPath, config) {
|
|
152
|
+
const spinner = ora('Copying template files...').start();
|
|
153
|
+
try {
|
|
154
|
+
const templatePath = resolve(TEMPLATES_DIR, config.template);
|
|
155
|
+
// Copy entire src directory from template
|
|
156
|
+
if (existsSync(templatePath)) {
|
|
157
|
+
const templateSrc = resolve(templatePath, 'src');
|
|
158
|
+
const projectSrc = resolve(projectPath, 'src');
|
|
159
|
+
if (existsSync(templateSrc)) {
|
|
160
|
+
// Remove empty src directory created by createProjectStructure
|
|
161
|
+
if (existsSync(projectSrc)) {
|
|
162
|
+
execSync(`rm -rf ${projectSrc}`);
|
|
163
|
+
}
|
|
164
|
+
// Copy template src to project
|
|
165
|
+
cpSync(templateSrc, projectSrc, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
// Copy tests directory from template
|
|
168
|
+
const templateTests = resolve(templatePath, 'tests');
|
|
169
|
+
const projectTests = resolve(projectPath, 'tests');
|
|
170
|
+
if (existsSync(templateTests)) {
|
|
171
|
+
// Remove empty tests directory created by createProjectStructure
|
|
172
|
+
if (existsSync(projectTests)) {
|
|
173
|
+
execSync(`rm -rf ${projectTests}`);
|
|
174
|
+
}
|
|
175
|
+
// Copy template tests to project
|
|
176
|
+
cpSync(templateTests, projectTests, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
spinner.succeed('Copied template files');
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
spinner.fail('Failed to copy template files');
|
|
184
|
+
console.error(error);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function generatePackageJson(projectPath, config) {
|
|
189
|
+
const spinner = ora('Generating package.json...').start();
|
|
190
|
+
try {
|
|
191
|
+
const dependencies = {
|
|
192
|
+
'filamentjs': '^0.1.0'
|
|
193
|
+
};
|
|
194
|
+
const devDependencies = {
|
|
195
|
+
'@types/node': '^20.0.0',
|
|
196
|
+
'typescript': '^5.0.0',
|
|
197
|
+
'tsx': '^4.7.0',
|
|
198
|
+
'eslint': '^9.0.0',
|
|
199
|
+
'@typescript-eslint/parser': '^8.0.0',
|
|
200
|
+
'@typescript-eslint/eslint-plugin': '^8.0.0',
|
|
201
|
+
'prettier': '^3.2.4',
|
|
202
|
+
'husky': '^9.0.0',
|
|
203
|
+
'lint-staged': '^15.2.0',
|
|
204
|
+
'@commitlint/cli': '^19.0.0',
|
|
205
|
+
'@commitlint/config-conventional': '^19.0.0',
|
|
206
|
+
'depcheck': '^1.4.7',
|
|
207
|
+
'test-battery': '^3.2.1'
|
|
208
|
+
};
|
|
209
|
+
// Add dependencies based on features
|
|
210
|
+
if (config.features.auth) {
|
|
211
|
+
dependencies['redis'] = '^4.6.0';
|
|
212
|
+
dependencies['jsonwebtoken'] = '^9.0.2';
|
|
213
|
+
devDependencies['@types/jsonwebtoken'] = '^9.0.5';
|
|
214
|
+
}
|
|
215
|
+
if (config.features.openapi) {
|
|
216
|
+
dependencies['swagger-ui-express'] = '^5.0.0';
|
|
217
|
+
devDependencies['@types/swagger-ui-express'] = '^4.1.6';
|
|
218
|
+
}
|
|
219
|
+
if (config.features.observability) {
|
|
220
|
+
dependencies['@opentelemetry/api'] = '^1.9.0';
|
|
221
|
+
dependencies['@opentelemetry/sdk-node'] = '^0.54.0';
|
|
222
|
+
dependencies['prom-client'] = '^15.1.0';
|
|
223
|
+
}
|
|
224
|
+
// Always include pino
|
|
225
|
+
dependencies['pino'] = '^8.17.0';
|
|
226
|
+
dependencies['pino-pretty'] = '^10.3.0';
|
|
227
|
+
const packageJson = {
|
|
228
|
+
name: config.name,
|
|
229
|
+
version: '1.0.0',
|
|
230
|
+
description: 'A Filament API application',
|
|
231
|
+
type: 'module',
|
|
232
|
+
scripts: {
|
|
233
|
+
dev: 'tsx watch src/index.ts',
|
|
234
|
+
build: 'tsc',
|
|
235
|
+
start: 'node dist/index.js',
|
|
236
|
+
test: 'node --test --experimental-test-coverage tests/**/*.test.ts',
|
|
237
|
+
'test:watch': 'node --test --watch tests/**/*.test.ts',
|
|
238
|
+
lint: 'eslint src/**/*.ts',
|
|
239
|
+
'lint:fix': 'eslint src/**/*.ts --fix',
|
|
240
|
+
format: 'prettier --write src/**/*.ts',
|
|
241
|
+
'type-check': 'tsc --noEmit',
|
|
242
|
+
prepare: 'husky',
|
|
243
|
+
depcheck: 'depcheck'
|
|
244
|
+
},
|
|
245
|
+
dependencies,
|
|
246
|
+
devDependencies
|
|
247
|
+
};
|
|
248
|
+
// Add docker scripts if enabled
|
|
249
|
+
if (config.features.docker) {
|
|
250
|
+
packageJson.scripts['docker:build'] = 'docker build -t ' + config.name + ' .';
|
|
251
|
+
packageJson.scripts['docker:run'] = 'docker run -p 3000:3000 ' + config.name;
|
|
252
|
+
}
|
|
253
|
+
if (config.features.dockerCompose) {
|
|
254
|
+
packageJson.scripts['docker:up'] = 'docker-compose up';
|
|
255
|
+
packageJson.scripts['docker:down'] = 'docker-compose down';
|
|
256
|
+
}
|
|
257
|
+
writeFileSync(resolve(projectPath, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
258
|
+
spinner.succeed('Generated package.json');
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
spinner.fail('Failed to generate package.json');
|
|
263
|
+
console.error(error);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function generateConfigFiles(projectPath, config) {
|
|
268
|
+
const spinner = ora('Generating configuration files...').start();
|
|
269
|
+
try {
|
|
270
|
+
// TypeScript config
|
|
271
|
+
const tsConfig = {
|
|
272
|
+
compilerOptions: {
|
|
273
|
+
declaration: true,
|
|
274
|
+
declarationMap: true,
|
|
275
|
+
esModuleInterop: true,
|
|
276
|
+
forceConsistentCasingInFileNames: true,
|
|
277
|
+
lib: ['ES2022'],
|
|
278
|
+
module: 'ES2022',
|
|
279
|
+
moduleResolution: 'node',
|
|
280
|
+
outDir: './dist',
|
|
281
|
+
paths: {
|
|
282
|
+
'@/*': ['./src/*']
|
|
283
|
+
},
|
|
284
|
+
resolveJsonModule: true,
|
|
285
|
+
rootDir: './src',
|
|
286
|
+
skipLibCheck: true,
|
|
287
|
+
sourceMap: true,
|
|
288
|
+
strict: true,
|
|
289
|
+
target: 'ES2022',
|
|
290
|
+
types: ["node"]
|
|
291
|
+
},
|
|
292
|
+
include: ['src/**/*'],
|
|
293
|
+
exclude: ['node_modules', 'dist', 'tests']
|
|
294
|
+
};
|
|
295
|
+
writeFileSync(resolve(projectPath, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
|
|
296
|
+
// ESLint config (flat config for ESLint 9+)
|
|
297
|
+
const eslintConfig = `import tseslint from '@typescript-eslint/eslint-plugin';
|
|
298
|
+
import tsparser from '@typescript-eslint/parser';
|
|
299
|
+
|
|
300
|
+
export default [
|
|
301
|
+
{
|
|
302
|
+
files: ['src/**/*.ts'],
|
|
303
|
+
languageOptions: {
|
|
304
|
+
parser: tsparser,
|
|
305
|
+
parserOptions: {
|
|
306
|
+
ecmaVersion: 2022,
|
|
307
|
+
sourceType: 'module'
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
plugins: {
|
|
311
|
+
'@typescript-eslint': tseslint
|
|
312
|
+
},
|
|
313
|
+
rules: {
|
|
314
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
315
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
];
|
|
319
|
+
`;
|
|
320
|
+
writeFileSync(resolve(projectPath, 'eslint.config.js'), eslintConfig);
|
|
321
|
+
// Prettier config
|
|
322
|
+
const prettierConfig = {
|
|
323
|
+
semi: true,
|
|
324
|
+
trailingComma: 'es5',
|
|
325
|
+
singleQuote: true,
|
|
326
|
+
printWidth: 100,
|
|
327
|
+
tabWidth: 2
|
|
328
|
+
};
|
|
329
|
+
writeFileSync(resolve(projectPath, '.prettierrc'), JSON.stringify(prettierConfig, null, 2));
|
|
330
|
+
// .gitignore
|
|
331
|
+
const gitignore = `node_modules/
|
|
332
|
+
dist/
|
|
333
|
+
*.log
|
|
334
|
+
.env
|
|
335
|
+
.DS_Store
|
|
336
|
+
coverage/
|
|
337
|
+
.vscode/
|
|
338
|
+
.idea/
|
|
339
|
+
*.swp
|
|
340
|
+
*.swo
|
|
341
|
+
`;
|
|
342
|
+
writeFileSync(resolve(projectPath, '.gitignore'), gitignore);
|
|
343
|
+
// .env.example
|
|
344
|
+
let envExample = `# Server
|
|
345
|
+
PORT=3000
|
|
346
|
+
NODE_ENV=development
|
|
347
|
+
|
|
348
|
+
# Logging
|
|
349
|
+
LOG_LEVEL=info
|
|
350
|
+
`;
|
|
351
|
+
if (config.features.auth) {
|
|
352
|
+
envExample += `
|
|
353
|
+
# Authentication
|
|
354
|
+
JWT_SECRET=your-secret-here-change-in-production
|
|
355
|
+
JWT_EXPIRY=7d
|
|
356
|
+
|
|
357
|
+
# Redis
|
|
358
|
+
REDIS_URL=redis://localhost:6379
|
|
359
|
+
`;
|
|
360
|
+
}
|
|
361
|
+
if (config.features.observability) {
|
|
362
|
+
envExample += `
|
|
363
|
+
# OpenTelemetry
|
|
364
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
writeFileSync(resolve(projectPath, '.env.example'), envExample);
|
|
368
|
+
// Husky and lint-staged
|
|
369
|
+
const huskyPath = resolve(projectPath, '.husky');
|
|
370
|
+
mkdirSync(huskyPath, { recursive: true });
|
|
371
|
+
const preCommitHook = `#!/usr/bin/env sh
|
|
372
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
373
|
+
|
|
374
|
+
npx lint-staged
|
|
375
|
+
`;
|
|
376
|
+
writeFileSync(resolve(huskyPath, 'pre-commit'), preCommitHook);
|
|
377
|
+
const commitMsgHook = `#!/usr/bin/env sh
|
|
378
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
379
|
+
|
|
380
|
+
npx --no -- commitlint --edit $1
|
|
381
|
+
`;
|
|
382
|
+
writeFileSync(resolve(huskyPath, 'commit-msg'), commitMsgHook);
|
|
383
|
+
// Make hooks executable
|
|
384
|
+
try {
|
|
385
|
+
execSync(`chmod +x ${resolve(huskyPath, 'pre-commit')}`);
|
|
386
|
+
execSync(`chmod +x ${resolve(huskyPath, 'commit-msg')}`);
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
// Ignore on Windows
|
|
390
|
+
}
|
|
391
|
+
// lint-staged config
|
|
392
|
+
const lintStagedConfig = {
|
|
393
|
+
'*.ts': [
|
|
394
|
+
'eslint --fix',
|
|
395
|
+
'prettier --write'
|
|
396
|
+
]
|
|
397
|
+
};
|
|
398
|
+
writeFileSync(resolve(projectPath, '.lintstagedrc'), JSON.stringify(lintStagedConfig, null, 2));
|
|
399
|
+
// commitlint config
|
|
400
|
+
const commitlintConfig = `export default { extends: ['@commitlint/config-conventional'] };`;
|
|
401
|
+
writeFileSync(resolve(projectPath, 'commitlint.config.js'), commitlintConfig);
|
|
402
|
+
spinner.succeed('Generated configuration files');
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
spinner.fail('Failed to generate configuration files');
|
|
407
|
+
console.error(error);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function generateDockerFiles(projectPath, config) {
|
|
412
|
+
if (!config.features.docker && !config.features.dockerCompose) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
const spinner = ora('Generating Docker files...').start();
|
|
416
|
+
try {
|
|
417
|
+
if (config.features.docker) {
|
|
418
|
+
const dockerfile = `# Build stage
|
|
419
|
+
FROM node:20-alpine AS builder
|
|
420
|
+
|
|
421
|
+
WORKDIR /app
|
|
422
|
+
|
|
423
|
+
# Copy package files
|
|
424
|
+
COPY package*.json ./
|
|
425
|
+
|
|
426
|
+
# Install dependencies
|
|
427
|
+
RUN npm ci
|
|
428
|
+
|
|
429
|
+
# Copy source
|
|
430
|
+
COPY . .
|
|
431
|
+
|
|
432
|
+
# Build
|
|
433
|
+
RUN npm run build
|
|
434
|
+
|
|
435
|
+
# Runtime stage
|
|
436
|
+
FROM node:20-alpine
|
|
437
|
+
|
|
438
|
+
WORKDIR /app
|
|
439
|
+
|
|
440
|
+
# Copy package files
|
|
441
|
+
COPY package*.json ./
|
|
442
|
+
|
|
443
|
+
# Install production dependencies only
|
|
444
|
+
RUN npm ci --production
|
|
445
|
+
|
|
446
|
+
# Copy built files
|
|
447
|
+
COPY --from=builder /app/dist ./dist
|
|
448
|
+
|
|
449
|
+
# Create non-root user
|
|
450
|
+
RUN addgroup -g 1001 -S nodejs && \\
|
|
451
|
+
adduser -S nodejs -u 1001
|
|
452
|
+
|
|
453
|
+
USER nodejs
|
|
454
|
+
|
|
455
|
+
EXPOSE 3000
|
|
456
|
+
|
|
457
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
458
|
+
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
|
459
|
+
|
|
460
|
+
CMD ["node", "dist/index.js"]
|
|
461
|
+
`;
|
|
462
|
+
writeFileSync(resolve(projectPath, 'Dockerfile'), dockerfile);
|
|
463
|
+
const dockerignore = `node_modules
|
|
464
|
+
dist
|
|
465
|
+
*.log
|
|
466
|
+
.env
|
|
467
|
+
.git
|
|
468
|
+
.github
|
|
469
|
+
.vscode
|
|
470
|
+
.idea
|
|
471
|
+
*.md
|
|
472
|
+
tests
|
|
473
|
+
coverage
|
|
474
|
+
`;
|
|
475
|
+
writeFileSync(resolve(projectPath, '.dockerignore'), dockerignore);
|
|
476
|
+
}
|
|
477
|
+
if (config.features.dockerCompose) {
|
|
478
|
+
let composeServices = {
|
|
479
|
+
app: {
|
|
480
|
+
build: '.',
|
|
481
|
+
ports: ['3000:3000'],
|
|
482
|
+
environment: [
|
|
483
|
+
'NODE_ENV=production',
|
|
484
|
+
'LOG_LEVEL=info'
|
|
485
|
+
],
|
|
486
|
+
depends_on: [],
|
|
487
|
+
restart: 'unless-stopped'
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
if (config.features.auth) {
|
|
491
|
+
composeServices.app.environment.push('REDIS_URL=redis://redis:6379');
|
|
492
|
+
composeServices.app.depends_on.push('redis');
|
|
493
|
+
composeServices.redis = {
|
|
494
|
+
image: 'redis:7-alpine',
|
|
495
|
+
ports: ['6379:6379'],
|
|
496
|
+
volumes: ['redis-data:/data'],
|
|
497
|
+
restart: 'unless-stopped'
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const dockerCompose = {
|
|
501
|
+
version: '3.8',
|
|
502
|
+
services: composeServices,
|
|
503
|
+
volumes: config.features.auth ? { 'redis-data': {} } : undefined
|
|
504
|
+
};
|
|
505
|
+
writeFileSync(resolve(projectPath, 'docker-compose.yml'), JSON.stringify(dockerCompose, null, 2));
|
|
506
|
+
}
|
|
507
|
+
spinner.succeed('Generated Docker files');
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
spinner.fail('Failed to generate Docker files');
|
|
512
|
+
console.error(error);
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function generateCIFiles(projectPath, config) {
|
|
517
|
+
if (config.features.ci === 'none') {
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
const spinner = ora('Generating CI/CD files...').start();
|
|
521
|
+
try {
|
|
522
|
+
if (config.features.ci === 'github') {
|
|
523
|
+
const githubPath = resolve(projectPath, '.github/workflows');
|
|
524
|
+
mkdirSync(githubPath, { recursive: true });
|
|
525
|
+
const ciWorkflow = `name: CI
|
|
526
|
+
|
|
527
|
+
on:
|
|
528
|
+
push:
|
|
529
|
+
branches: [ main, develop ]
|
|
530
|
+
pull_request:
|
|
531
|
+
branches: [ main, develop ]
|
|
532
|
+
|
|
533
|
+
jobs:
|
|
534
|
+
test:
|
|
535
|
+
runs-on: ubuntu-latest
|
|
536
|
+
|
|
537
|
+
steps:
|
|
538
|
+
- uses: actions/checkout@v4
|
|
539
|
+
|
|
540
|
+
- name: Setup Node.js
|
|
541
|
+
uses: actions/setup-node@v4
|
|
542
|
+
with:
|
|
543
|
+
node-version: '20'
|
|
544
|
+
cache: 'npm'
|
|
545
|
+
|
|
546
|
+
- name: Install dependencies
|
|
547
|
+
run: npm ci
|
|
548
|
+
|
|
549
|
+
- name: Run linter
|
|
550
|
+
run: npm run lint
|
|
551
|
+
|
|
552
|
+
- name: Type check
|
|
553
|
+
run: npm run type-check
|
|
554
|
+
|
|
555
|
+
- name: Run tests
|
|
556
|
+
run: npm test
|
|
557
|
+
|
|
558
|
+
- name: Check dependencies
|
|
559
|
+
run: npm run depcheck
|
|
560
|
+
|
|
561
|
+
- name: Build
|
|
562
|
+
run: npm run build
|
|
563
|
+
`;
|
|
564
|
+
let dockerJob = '';
|
|
565
|
+
if (config.features.docker) {
|
|
566
|
+
dockerJob = `
|
|
567
|
+
docker:
|
|
568
|
+
runs-on: ubuntu-latest
|
|
569
|
+
needs: test
|
|
570
|
+
if: github.ref == 'refs/heads/main'
|
|
571
|
+
|
|
572
|
+
steps:
|
|
573
|
+
- uses: actions/checkout@v4
|
|
574
|
+
|
|
575
|
+
- name: Build Docker image
|
|
576
|
+
run: docker build -t ${config.name}:latest .
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
writeFileSync(resolve(githubPath, 'ci.yml'), ciWorkflow + dockerJob);
|
|
580
|
+
}
|
|
581
|
+
spinner.succeed('Generated CI/CD files');
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
spinner.fail('Failed to generate CI/CD files');
|
|
586
|
+
console.error(error);
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function generateREADME(projectPath, config) {
|
|
591
|
+
const spinner = ora('Generating documentation...').start();
|
|
592
|
+
try {
|
|
593
|
+
const readme = `# ${config.name}
|
|
594
|
+
|
|
595
|
+
A Filament API application.
|
|
596
|
+
|
|
597
|
+
## Quick Start
|
|
598
|
+
|
|
599
|
+
\`\`\`bash
|
|
600
|
+
# Install dependencies
|
|
601
|
+
${config.packageManager} install
|
|
602
|
+
|
|
603
|
+
# Start development server
|
|
604
|
+
${config.packageManager} run dev
|
|
605
|
+
|
|
606
|
+
# Server running at http://localhost:3000
|
|
607
|
+
\`\`\`
|
|
608
|
+
|
|
609
|
+
## Available Scripts
|
|
610
|
+
|
|
611
|
+
- \`${config.packageManager} run dev\` - Start with hot reload
|
|
612
|
+
- \`${config.packageManager} test\` - Run tests
|
|
613
|
+
- \`${config.packageManager} run build\` - Build for production
|
|
614
|
+
- \`${config.packageManager} start\` - Run production build
|
|
615
|
+
- \`${config.packageManager} run lint\` - Lint code
|
|
616
|
+
- \`${config.packageManager} run format\` - Format code
|
|
617
|
+
${config.features.docker ? `- \`${config.packageManager} run docker:build\` - Build Docker image\n` : ''}${config.features.dockerCompose ? `- \`${config.packageManager} run docker:up\` - Run with Docker Compose\n` : ''}
|
|
618
|
+
## Project Structure
|
|
619
|
+
|
|
620
|
+
\`\`\`text
|
|
621
|
+
${config.name}/
|
|
622
|
+
āāā src/
|
|
623
|
+
ā āāā meta/ # Metadata interface and defaults
|
|
624
|
+
ā āāā middleware/ # Middleware functions
|
|
625
|
+
ā āāā routes/ # Route definitions
|
|
626
|
+
ā āāā handlers/ # Post-request handlers
|
|
627
|
+
ā āāā config/ # Application configuration
|
|
628
|
+
ā āāā index.ts # Entry point
|
|
629
|
+
āāā tests/
|
|
630
|
+
ā āāā integration/ # Integration tests
|
|
631
|
+
ā āāā unit/ # Unit tests
|
|
632
|
+
${config.features.docker ? 'āāā Dockerfile\n' : ''}${config.features.dockerCompose ? 'āāā docker-compose.yml\n' : ''}āāā package.json
|
|
633
|
+
\`\`\`
|
|
634
|
+
|
|
635
|
+
## Endpoints
|
|
636
|
+
|
|
637
|
+
- \`GET /health\` - Health check
|
|
638
|
+
|
|
639
|
+
${config.features.auth ? `
|
|
640
|
+
## Authentication
|
|
641
|
+
|
|
642
|
+
This project includes JWT and OAuth middleware. See \`src/middleware/\` for implementation.
|
|
643
|
+
|
|
644
|
+
Configure in \`.env\`:
|
|
645
|
+
|
|
646
|
+
\`\`\`bash
|
|
647
|
+
JWT_SECRET=your-secret-here
|
|
648
|
+
REDIS_URL=redis://localhost:6379
|
|
649
|
+
\`\`\`
|
|
650
|
+
` : ''}
|
|
651
|
+
|
|
652
|
+
${config.features.openapi ? `
|
|
653
|
+
## API Documentation
|
|
654
|
+
|
|
655
|
+
OpenAPI documentation available at: [http://localhost:3000/docs](http://localhost:3000/docs)
|
|
656
|
+
` : ''}
|
|
657
|
+
|
|
658
|
+
${config.features.observability ? `
|
|
659
|
+
## Observability
|
|
660
|
+
|
|
661
|
+
This project includes OpenTelemetry tracing and Prometheus metrics.
|
|
662
|
+
|
|
663
|
+
Metrics endpoint: [http://localhost:3000/metrics](http://localhost:3000/metrics)
|
|
664
|
+
` : ''}
|
|
665
|
+
|
|
666
|
+
## Database Integration
|
|
667
|
+
|
|
668
|
+
Filament doesn't include a database ORM by design. Choose what works for you:
|
|
669
|
+
|
|
670
|
+
### Recommended Options
|
|
671
|
+
|
|
672
|
+
**Prisma** (TypeScript-first, great DX)
|
|
673
|
+
|
|
674
|
+
\`\`\`bash
|
|
675
|
+
${config.packageManager} install prisma @prisma/client
|
|
676
|
+
npx prisma init
|
|
677
|
+
\`\`\`
|
|
678
|
+
|
|
679
|
+
**Drizzle** (Lightweight, SQL-like)
|
|
680
|
+
|
|
681
|
+
\`\`\`bash
|
|
682
|
+
${config.packageManager} install drizzle-orm
|
|
683
|
+
\`\`\`
|
|
684
|
+
|
|
685
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for integration examples.
|
|
686
|
+
|
|
687
|
+
## Development
|
|
688
|
+
|
|
689
|
+
1. Copy \`.env.example\` to \`.env\` and configure
|
|
690
|
+
2. Run \`${config.packageManager} run dev\`
|
|
691
|
+
3. Make changes - server auto-reloads
|
|
692
|
+
|
|
693
|
+
## Production
|
|
694
|
+
|
|
695
|
+
\`\`\`bash
|
|
696
|
+
${config.packageManager} run build
|
|
697
|
+
${config.packageManager} start
|
|
698
|
+
\`\`\`
|
|
699
|
+
|
|
700
|
+
${config.features.docker ? `
|
|
701
|
+
Or with Docker:
|
|
702
|
+
|
|
703
|
+
\`\`\`bash
|
|
704
|
+
docker build -t ${config.name} .
|
|
705
|
+
docker run -p 3000:3000 ${config.name}
|
|
706
|
+
\`\`\`
|
|
707
|
+
` : ''}
|
|
708
|
+
|
|
709
|
+
## Testing
|
|
710
|
+
|
|
711
|
+
\`\`\`bash
|
|
712
|
+
${config.packageManager} test
|
|
713
|
+
\`\`\`
|
|
714
|
+
|
|
715
|
+
## License
|
|
716
|
+
|
|
717
|
+
ISC
|
|
718
|
+
`.replace(/\n{2,}/g, '\n\n');
|
|
719
|
+
writeFileSync(resolve(projectPath, 'README.md'), readme);
|
|
720
|
+
// Generate ARCHITECTURE.md
|
|
721
|
+
const architecture = `# Architecture
|
|
722
|
+
|
|
723
|
+
## Metadata-Driven Design
|
|
724
|
+
|
|
725
|
+
This Filament application uses metadata to control middleware behavior.
|
|
726
|
+
|
|
727
|
+
### Metadata Interface
|
|
728
|
+
|
|
729
|
+
See \`src/meta/index.ts\` for your metadata interface definition.
|
|
730
|
+
|
|
731
|
+
### How It Works
|
|
732
|
+
|
|
733
|
+
1. Define default metadata in \`src/meta/defaults.ts\`
|
|
734
|
+
2. Override per-endpoint in route definitions
|
|
735
|
+
3. Middleware inspects \`req.endpointMeta\` to decide behavior
|
|
736
|
+
|
|
737
|
+
Example:
|
|
738
|
+
|
|
739
|
+
\`\`\`typescript
|
|
740
|
+
// src/routes/users.ts
|
|
741
|
+
app.get('/users',
|
|
742
|
+
{ requiresAuth: true, rateLimit: 50 },
|
|
743
|
+
async (req, res) => {
|
|
744
|
+
res.json({ users: [] });
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
// src/middleware/auth.ts
|
|
749
|
+
app.use(async (req, res, next) => {
|
|
750
|
+
if (req.endpointMeta.requiresAuth) {
|
|
751
|
+
// Perform authentication
|
|
752
|
+
}
|
|
753
|
+
await next();
|
|
754
|
+
});
|
|
755
|
+
\`\`\`
|
|
756
|
+
|
|
757
|
+
## Request Lifecycle
|
|
758
|
+
|
|
759
|
+
1. Incoming request
|
|
760
|
+
2. Route matching ā \`req.endpointMeta\` populated
|
|
761
|
+
3. Middleware chain executes
|
|
762
|
+
4. Route handler executes
|
|
763
|
+
5. Response transformers (success)
|
|
764
|
+
6. Error handlers (if error)
|
|
765
|
+
7. Finalizers (always)
|
|
766
|
+
8. Response sent
|
|
767
|
+
|
|
768
|
+
## Adding Routes
|
|
769
|
+
|
|
770
|
+
1. Create route file in \`src/routes/\`
|
|
771
|
+
2. Export a mount function
|
|
772
|
+
3. Import and call in \`src/routes/index.ts\`
|
|
773
|
+
|
|
774
|
+
## Adding Middleware
|
|
775
|
+
|
|
776
|
+
1. Create middleware file in \`src/middleware/\`
|
|
777
|
+
2. Middleware should check \`req.endpointMeta\` for configuration
|
|
778
|
+
3. Export and register in \`src/index.ts\`
|
|
779
|
+
|
|
780
|
+
## Database Integration
|
|
781
|
+
|
|
782
|
+
Example with Prisma:
|
|
783
|
+
|
|
784
|
+
\`\`\`typescript
|
|
785
|
+
// src/db/index.ts
|
|
786
|
+
import { PrismaClient } from '@prisma/client';
|
|
787
|
+
export const db = new PrismaClient();
|
|
788
|
+
|
|
789
|
+
// src/routes/users.ts
|
|
790
|
+
import { db } from '../db';
|
|
791
|
+
|
|
792
|
+
app.get('/users', PUBLIC, async (req, res) => {
|
|
793
|
+
const users = await db.user.findMany();
|
|
794
|
+
res.json({ users });
|
|
795
|
+
});
|
|
796
|
+
\`\`\`
|
|
797
|
+
|
|
798
|
+
## Testing
|
|
799
|
+
|
|
800
|
+
Tests use Node's native test runner with test-battery for assertions.
|
|
801
|
+
|
|
802
|
+
Example:
|
|
803
|
+
|
|
804
|
+
\`\`\`typescript
|
|
805
|
+
import { describe, it } from 'node:test';
|
|
806
|
+
import { expect } from 'test-battery';
|
|
807
|
+
|
|
808
|
+
describe('Users API', () => {
|
|
809
|
+
it('should list users', async () => {
|
|
810
|
+
// Test implementation
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
\`\`\`
|
|
814
|
+
`.replace(/\n{2,}/g, '\n\n');
|
|
815
|
+
writeFileSync(resolve(projectPath, 'ARCHITECTURE.md'), architecture);
|
|
816
|
+
spinner.succeed('Generated documentation');
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
spinner.fail('Failed to generate documentation');
|
|
821
|
+
console.error(error);
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function installDependencies(projectPath, config) {
|
|
826
|
+
if (!config.install) {
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
const spinner = ora('Installing dependencies...').start();
|
|
830
|
+
try {
|
|
831
|
+
const commands = {
|
|
832
|
+
npm: 'npm install',
|
|
833
|
+
pnpm: 'pnpm install',
|
|
834
|
+
yarn: 'yarn',
|
|
835
|
+
bun: 'bun install'
|
|
836
|
+
};
|
|
837
|
+
execSync(commands[config.packageManager], {
|
|
838
|
+
cwd: projectPath,
|
|
839
|
+
stdio: 'pipe'
|
|
840
|
+
});
|
|
841
|
+
spinner.succeed('Installed dependencies');
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
spinner.fail('Failed to install dependencies');
|
|
846
|
+
console.error('\nYou can install dependencies manually by running:');
|
|
847
|
+
console.error(chalk.cyan(` cd ${config.name} && ${config.packageManager} install\n`));
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function initializeGit(projectPath, config) {
|
|
852
|
+
if (!config.git) {
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
const spinner = ora('Initializing git repository...').start();
|
|
856
|
+
try {
|
|
857
|
+
execSync('git init', { cwd: projectPath, stdio: 'pipe' });
|
|
858
|
+
execSync('git add .', { cwd: projectPath, stdio: 'pipe' });
|
|
859
|
+
if (config.gitCommit) {
|
|
860
|
+
execSync('git commit -m "feat: initial commit from create-filament"', {
|
|
861
|
+
cwd: projectPath,
|
|
862
|
+
stdio: 'pipe'
|
|
863
|
+
});
|
|
864
|
+
spinner.succeed('Initialized git repository with initial commit');
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
spinner.succeed('Initialized git repository');
|
|
868
|
+
}
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
spinner.fail('Failed to initialize git repository');
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function printNextSteps(config) {
|
|
877
|
+
console.log(chalk.green('\nš Success! Created ' + config.name + '\n'));
|
|
878
|
+
console.log('Next steps:\n');
|
|
879
|
+
console.log(chalk.cyan(' cd ' + config.name));
|
|
880
|
+
if (!config.install) {
|
|
881
|
+
console.log(chalk.cyan(` ${config.packageManager} install`));
|
|
882
|
+
}
|
|
883
|
+
console.log(chalk.cyan(` ${config.packageManager} run dev`));
|
|
884
|
+
console.log('\nYour app will be running at ' + chalk.cyan('http://localhost:3000'));
|
|
885
|
+
console.log('\nCommands:');
|
|
886
|
+
console.log(` ${chalk.cyan(config.packageManager + ' run dev')} Start development server`);
|
|
887
|
+
console.log(` ${chalk.cyan(config.packageManager + ' test')} Run tests`);
|
|
888
|
+
console.log(` ${chalk.cyan(config.packageManager + ' run build')} Build for production`);
|
|
889
|
+
if (config.features.docker) {
|
|
890
|
+
console.log(` ${chalk.cyan(config.packageManager + ' run docker:build')} Build Docker image`);
|
|
891
|
+
}
|
|
892
|
+
console.log('\nDocumentation:');
|
|
893
|
+
console.log(' README.md Getting started guide');
|
|
894
|
+
console.log(' ARCHITECTURE.md Project structure explained');
|
|
895
|
+
console.log(chalk.gray('\nHappy coding! š„\n'));
|
|
896
|
+
}
|
|
897
|
+
async function main() {
|
|
898
|
+
printBanner();
|
|
899
|
+
const args = process.argv.slice(2);
|
|
900
|
+
const projectName = args[0];
|
|
901
|
+
// Check if directory exists
|
|
902
|
+
if (projectName && existsSync(projectName)) {
|
|
903
|
+
console.error(chalk.red(`\nā Directory "${projectName}" already exists\n`));
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
const config = await promptForConfig(projectName);
|
|
907
|
+
const projectPath = resolve(process.cwd(), config.name);
|
|
908
|
+
// Check again after prompts
|
|
909
|
+
if (existsSync(projectPath)) {
|
|
910
|
+
console.error(chalk.red(`\nā Directory "${config.name}" already exists\n`));
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
// Execute all steps
|
|
914
|
+
const steps = [
|
|
915
|
+
() => createProjectStructure(projectPath, config),
|
|
916
|
+
() => copyTemplateFiles(projectPath, config),
|
|
917
|
+
() => generatePackageJson(projectPath, config),
|
|
918
|
+
() => generateConfigFiles(projectPath, config),
|
|
919
|
+
() => generateDockerFiles(projectPath, config),
|
|
920
|
+
() => generateCIFiles(projectPath, config),
|
|
921
|
+
() => generateREADME(projectPath, config),
|
|
922
|
+
() => installDependencies(projectPath, config),
|
|
923
|
+
() => initializeGit(projectPath, config)
|
|
924
|
+
];
|
|
925
|
+
for (const step of steps) {
|
|
926
|
+
if (!step()) {
|
|
927
|
+
console.error(chalk.red('\nā Setup failed\n'));
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
printNextSteps(config);
|
|
932
|
+
}
|
|
933
|
+
main().catch((error) => {
|
|
934
|
+
console.error(chalk.red('\nā An error occurred:\n'));
|
|
935
|
+
console.error(error);
|
|
936
|
+
process.exit(1);
|
|
937
|
+
});
|