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.
Files changed (34) hide show
  1. package/README.md +417 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +937 -0
  4. package/package.json +38 -0
  5. package/templates/api/src/config/app.ts +17 -0
  6. package/templates/api/src/handlers/errors.ts +32 -0
  7. package/templates/api/src/handlers/finalizers.ts +27 -0
  8. package/templates/api/src/handlers/transforms.ts +18 -0
  9. package/templates/api/src/index.ts +45 -0
  10. package/templates/api/src/meta/defaults.ts +28 -0
  11. package/templates/api/src/meta/index.ts +50 -0
  12. package/templates/api/src/middleware/index.ts +36 -0
  13. package/templates/api/src/routes/index.ts +26 -0
  14. package/templates/api/tests/integration/example.test.ts +20 -0
  15. package/templates/full/src/config/app.ts +17 -0
  16. package/templates/full/src/handlers/errors.ts +32 -0
  17. package/templates/full/src/handlers/finalizers.ts +27 -0
  18. package/templates/full/src/handlers/transforms.ts +18 -0
  19. package/templates/full/src/index.ts +45 -0
  20. package/templates/full/src/meta/defaults.ts +28 -0
  21. package/templates/full/src/meta/index.ts +31 -0
  22. package/templates/full/src/middleware/index.ts +36 -0
  23. package/templates/full/src/routes/index.ts +26 -0
  24. package/templates/full/tests/integration/example.test.ts +20 -0
  25. package/templates/minimal/src/config/app.ts +17 -0
  26. package/templates/minimal/src/handlers/errors.ts +32 -0
  27. package/templates/minimal/src/handlers/finalizers.ts +27 -0
  28. package/templates/minimal/src/handlers/transforms.ts +18 -0
  29. package/templates/minimal/src/index.ts +45 -0
  30. package/templates/minimal/src/meta/defaults.ts +28 -0
  31. package/templates/minimal/src/meta/index.ts +31 -0
  32. package/templates/minimal/src/middleware/index.ts +36 -0
  33. package/templates/minimal/src/routes/index.ts +26 -0
  34. 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
+ });