@buenojs/bueno 0.8.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 (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,804 @@
1
+ /**
2
+ * New Command
3
+ *
4
+ * Create a new Bueno project
5
+ */
6
+
7
+ import { defineCommand } from './index';
8
+ import { getOption, hasFlag, getOptionValues, type ParsedArgs } from '../core/args';
9
+ import { cliConsole, colors, printTable } from '../core/console';
10
+ import { prompt, confirm, select, isInteractive } from '../core/prompt';
11
+ import { spinner, runTasks, type TaskOptions } from '../core/spinner';
12
+ import {
13
+ fileExists,
14
+ writeFile,
15
+ createDirectory,
16
+ copyDirectory,
17
+ joinPaths,
18
+ } from '../utils/fs';
19
+ import { kebabCase } from '../utils/strings';
20
+ import { CLIError, CLIErrorType } from '../index';
21
+ import {
22
+ getDockerfileTemplate,
23
+ getDockerignoreTemplate,
24
+ getDockerComposeTemplate,
25
+ getDockerEnvTemplate,
26
+ } from '../templates/docker';
27
+ import {
28
+ type DeployPlatform,
29
+ getDeployTemplate,
30
+ getDeployFilename,
31
+ getDeployPlatformName,
32
+ } from '../templates/deploy';
33
+
34
+ /**
35
+ * Project templates
36
+ */
37
+ type ProjectTemplate = 'default' | 'minimal' | 'fullstack' | 'api';
38
+
39
+ /**
40
+ * Frontend frameworks
41
+ */
42
+ type FrontendFramework = 'react' | 'vue' | 'svelte' | 'solid';
43
+
44
+ /**
45
+ * Database drivers
46
+ */
47
+ type DatabaseDriver = 'sqlite' | 'postgresql' | 'mysql';
48
+
49
+ /**
50
+ * Project configuration
51
+ */
52
+ interface ProjectConfig {
53
+ name: string;
54
+ template: ProjectTemplate;
55
+ framework: FrontendFramework;
56
+ database: DatabaseDriver;
57
+ skipInstall: boolean;
58
+ skipGit: boolean;
59
+ docker: boolean;
60
+ deploy: DeployPlatform[];
61
+ }
62
+
63
+ /**
64
+ * Validate project name
65
+ */
66
+ function validateProjectName(name: string): boolean | string {
67
+ if (!name || name.length === 0) {
68
+ return 'Project name is required';
69
+ }
70
+
71
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
72
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
73
+ }
74
+
75
+ if (name.startsWith('-') || name.startsWith('_')) {
76
+ return 'Project name cannot start with a hyphen or underscore';
77
+ }
78
+
79
+ if (name.length > 100) {
80
+ return 'Project name is too long (max 100 characters)';
81
+ }
82
+
83
+ return true;
84
+ }
85
+
86
+ /**
87
+ * Get package.json template
88
+ */
89
+ function getPackageJsonTemplate(config: ProjectConfig): string {
90
+ const dependencies: Record<string, string> = {
91
+ bueno: '^0.1.0',
92
+ };
93
+
94
+ const devDependencies: Record<string, string> = {
95
+ '@types/bun': 'latest',
96
+ typescript: '^5.3.0',
97
+ };
98
+
99
+ if (config.template === 'fullstack' || config.template === 'default') {
100
+ dependencies.zod = '^4.0.0';
101
+ }
102
+
103
+ const scripts: Record<string, string> = {
104
+ dev: 'bun run --watch server/main.ts',
105
+ build: 'bun build ./server/main.ts --outdir ./dist --target bun',
106
+ start: 'bun run dist/main.js',
107
+ test: 'bun test',
108
+ };
109
+
110
+ if (config.template === 'fullstack') {
111
+ scripts['dev:frontend'] = 'bun run --watch client/index.html';
112
+ }
113
+
114
+ return JSON.stringify(
115
+ {
116
+ name: kebabCase(config.name),
117
+ version: '0.1.0',
118
+ type: 'module',
119
+ scripts,
120
+ dependencies,
121
+ devDependencies,
122
+ },
123
+ null,
124
+ 2,
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Get tsconfig.json template
130
+ */
131
+ function getTsConfigTemplate(): string {
132
+ return JSON.stringify(
133
+ {
134
+ compilerOptions: {
135
+ target: 'ESNext',
136
+ module: 'ESNext',
137
+ moduleResolution: 'bundler',
138
+ strict: true,
139
+ skipLibCheck: true,
140
+ esModuleInterop: true,
141
+ allowSyntheticDefaultImports: true,
142
+ jsx: 'react-jsx',
143
+ paths: {
144
+ bueno: ['./node_modules/bueno/dist/index.d.ts'],
145
+ },
146
+ },
147
+ include: ['server/**/*', 'client/**/*'],
148
+ exclude: ['node_modules', 'dist'],
149
+ },
150
+ null,
151
+ 2,
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Get main.ts template
157
+ */
158
+ function getMainTemplate(config: ProjectConfig): string {
159
+ if (config.template === 'minimal') {
160
+ return `import { createServer } from 'bueno';
161
+
162
+ const app = createServer();
163
+
164
+ app.router.get('/', () => {
165
+ return { message: 'Hello, Bueno!' };
166
+ });
167
+
168
+ await app.listen(3000);
169
+ `;
170
+ }
171
+
172
+ return `import { createApp, Module, Controller, Get, Injectable } from 'bueno';
173
+ import type { Context } from 'bueno';
174
+
175
+ // Services
176
+ @Injectable()
177
+ export class AppService {
178
+ findAll() {
179
+ return { message: 'Welcome to Bueno!', items: [] };
180
+ }
181
+ }
182
+
183
+ // Controllers
184
+ @Controller()
185
+ export class AppController {
186
+ constructor(private readonly appService: AppService) {}
187
+
188
+ @Get()
189
+ findAll(ctx: Context) {
190
+ return this.appService.findAll();
191
+ }
192
+
193
+ @Get('health')
194
+ health(ctx: Context) {
195
+ return { status: 'ok', timestamp: new Date().toISOString() };
196
+ }
197
+ }
198
+
199
+ // Module
200
+ @Module({
201
+ controllers: [AppController],
202
+ providers: [AppService],
203
+ })
204
+ export class AppModule {}
205
+
206
+ // Bootstrap
207
+ const app = createApp(AppModule);
208
+ await app.listen(3000);
209
+ `;
210
+ }
211
+
212
+ /**
213
+ * Get bueno.config.ts template
214
+ */
215
+ function getConfigTemplate(config: ProjectConfig): string {
216
+ const dbConfig = config.database === 'sqlite'
217
+ ? `{ url: 'sqlite:./data.db' }`
218
+ : `{ url: process.env.DATABASE_URL ?? '${config.database}://localhost/${kebabCase(config.name)}' }`;
219
+
220
+ return `import { defineConfig } from 'bueno';
221
+
222
+ export default defineConfig({
223
+ server: {
224
+ port: 3000,
225
+ host: 'localhost',
226
+ },
227
+
228
+ database: ${dbConfig},
229
+
230
+ logger: {
231
+ level: 'info',
232
+ pretty: true,
233
+ },
234
+
235
+ health: {
236
+ enabled: true,
237
+ healthPath: '/health',
238
+ readyPath: '/ready',
239
+ },
240
+ });
241
+ `;
242
+ }
243
+
244
+ /**
245
+ * Get .env.example template
246
+ */
247
+ function getEnvExampleTemplate(config: ProjectConfig): string {
248
+ if (config.database === 'sqlite') {
249
+ return `# Bueno Environment Variables
250
+ NODE_ENV=development
251
+ `;
252
+ }
253
+
254
+ return `# Bueno Environment Variables
255
+ NODE_ENV=development
256
+ DATABASE_URL=${config.database}://user:password@localhost:5432/${kebabCase(config.name)}
257
+ `;
258
+ }
259
+
260
+ /**
261
+ * Get .gitignore template
262
+ */
263
+ function getGitignoreTemplate(): string {
264
+ return `# Dependencies
265
+ node_modules/
266
+
267
+ # Build output
268
+ dist/
269
+
270
+ # Environment files
271
+ .env
272
+ .env.local
273
+ .env.*.local
274
+
275
+ # IDE
276
+ .idea/
277
+ .vscode/
278
+ *.swp
279
+ *.swo
280
+
281
+ # OS
282
+ .DS_Store
283
+ Thumbs.db
284
+
285
+ # Logs
286
+ *.log
287
+ logs/
288
+
289
+ # Database
290
+ *.db
291
+ *.sqlite
292
+ *.sqlite3
293
+
294
+ # Test coverage
295
+ coverage/
296
+ `;
297
+ }
298
+
299
+ /**
300
+ * Get README.md template
301
+ */
302
+ function getReadmeTemplate(config: ProjectConfig): string {
303
+ return `# ${config.name}
304
+
305
+ A Bueno application.
306
+
307
+ ## Getting Started
308
+
309
+ \`\`\`bash
310
+ # Install dependencies
311
+ bun install
312
+
313
+ # Start development server
314
+ bun run dev
315
+
316
+ # Build for production
317
+ bun run build
318
+
319
+ # Start production server
320
+ bun run start
321
+ \`\`\`
322
+
323
+ ## Project Structure
324
+
325
+ \`\`\`
326
+ ├── server/ # Server-side code
327
+ │ ├── main.ts # Entry point
328
+ │ ├── modules/ # Feature modules
329
+ │ └── database/ # Database files
330
+ ├── client/ # Client-side code (if applicable)
331
+ ├── tests/ # Test files
332
+ └── bueno.config.ts # Configuration
333
+ \`\`\`
334
+
335
+ ## Learn More
336
+
337
+ - [Bueno Documentation](https://github.com/sivaraj/bueno#readme)
338
+ - [Bun Documentation](https://bun.sh/docs)
339
+ `;
340
+ }
341
+
342
+ /**
343
+ * Create project files
344
+ */
345
+ async function createProjectFiles(
346
+ projectPath: string,
347
+ config: ProjectConfig,
348
+ ): Promise<void> {
349
+ const tasks: TaskOptions[] = [];
350
+
351
+ // Create directories
352
+ tasks.push({
353
+ text: 'Creating project structure',
354
+ task: async () => {
355
+ await createDirectory(joinPaths(projectPath, 'server', 'modules', 'app'));
356
+ await createDirectory(joinPaths(projectPath, 'server', 'common', 'middleware'));
357
+ await createDirectory(joinPaths(projectPath, 'server', 'common', 'guards'));
358
+ await createDirectory(joinPaths(projectPath, 'server', 'common', 'interceptors'));
359
+ await createDirectory(joinPaths(projectPath, 'server', 'common', 'pipes'));
360
+ await createDirectory(joinPaths(projectPath, 'server', 'common', 'filters'));
361
+ await createDirectory(joinPaths(projectPath, 'server', 'database', 'migrations'));
362
+ await createDirectory(joinPaths(projectPath, 'server', 'config'));
363
+ await createDirectory(joinPaths(projectPath, 'tests', 'unit'));
364
+ await createDirectory(joinPaths(projectPath, 'tests', 'integration'));
365
+ },
366
+ });
367
+
368
+ // Create package.json
369
+ tasks.push({
370
+ text: 'Creating package.json',
371
+ task: async () => {
372
+ await writeFile(
373
+ joinPaths(projectPath, 'package.json'),
374
+ getPackageJsonTemplate(config),
375
+ );
376
+ },
377
+ });
378
+
379
+ // Create tsconfig.json
380
+ tasks.push({
381
+ text: 'Creating tsconfig.json',
382
+ task: async () => {
383
+ await writeFile(
384
+ joinPaths(projectPath, 'tsconfig.json'),
385
+ getTsConfigTemplate(),
386
+ );
387
+ },
388
+ });
389
+
390
+ // Create main.ts
391
+ tasks.push({
392
+ text: 'Creating server/main.ts',
393
+ task: async () => {
394
+ await writeFile(
395
+ joinPaths(projectPath, 'server', 'main.ts'),
396
+ getMainTemplate(config),
397
+ );
398
+ },
399
+ });
400
+
401
+ // Create bueno.config.ts
402
+ tasks.push({
403
+ text: 'Creating bueno.config.ts',
404
+ task: async () => {
405
+ await writeFile(
406
+ joinPaths(projectPath, 'bueno.config.ts'),
407
+ getConfigTemplate(config),
408
+ );
409
+ },
410
+ });
411
+
412
+ // Create .env.example
413
+ tasks.push({
414
+ text: 'Creating .env.example',
415
+ task: async () => {
416
+ await writeFile(
417
+ joinPaths(projectPath, '.env.example'),
418
+ getEnvExampleTemplate(config),
419
+ );
420
+ },
421
+ });
422
+
423
+ // Create .gitignore
424
+ tasks.push({
425
+ text: 'Creating .gitignore',
426
+ task: async () => {
427
+ await writeFile(
428
+ joinPaths(projectPath, '.gitignore'),
429
+ getGitignoreTemplate(),
430
+ );
431
+ },
432
+ });
433
+
434
+ // Create README.md
435
+ tasks.push({
436
+ text: 'Creating README.md',
437
+ task: async () => {
438
+ await writeFile(
439
+ joinPaths(projectPath, 'README.md'),
440
+ getReadmeTemplate(config),
441
+ );
442
+ },
443
+ });
444
+
445
+ // Create Docker files if enabled
446
+ if (config.docker) {
447
+ tasks.push({
448
+ text: 'Creating Dockerfile',
449
+ task: async () => {
450
+ await writeFile(
451
+ joinPaths(projectPath, 'Dockerfile'),
452
+ getDockerfileTemplate(config.name, config.database),
453
+ );
454
+ },
455
+ });
456
+
457
+ tasks.push({
458
+ text: 'Creating .dockerignore',
459
+ task: async () => {
460
+ await writeFile(
461
+ joinPaths(projectPath, '.dockerignore'),
462
+ getDockerignoreTemplate(),
463
+ );
464
+ },
465
+ });
466
+
467
+ tasks.push({
468
+ text: 'Creating docker-compose.yml',
469
+ task: async () => {
470
+ await writeFile(
471
+ joinPaths(projectPath, 'docker-compose.yml'),
472
+ getDockerComposeTemplate(config.name, config.database),
473
+ );
474
+ },
475
+ });
476
+
477
+ tasks.push({
478
+ text: 'Creating .env.docker',
479
+ task: async () => {
480
+ await writeFile(
481
+ joinPaths(projectPath, '.env.docker'),
482
+ getDockerEnvTemplate(config.name, config.database),
483
+ );
484
+ },
485
+ });
486
+ }
487
+
488
+ // Create deployment configuration files
489
+ for (const platform of config.deploy) {
490
+ const filename = getDeployFilename(platform);
491
+ tasks.push({
492
+ text: `Creating ${filename} for ${getDeployPlatformName(platform)}`,
493
+ task: async () => {
494
+ await writeFile(
495
+ joinPaths(projectPath, filename),
496
+ getDeployTemplate(platform, config.name, config.database),
497
+ );
498
+ },
499
+ });
500
+ }
501
+
502
+ await runTasks(tasks);
503
+ }
504
+
505
+ /**
506
+ * Handle new command
507
+ */
508
+ async function handleNew(args: ParsedArgs): Promise<void> {
509
+ // Get project name
510
+ let name = args.positionals[0];
511
+ const useDefaults = hasFlag(args, 'yes') || hasFlag(args, 'y');
512
+
513
+ // Interactive prompts if no name provided
514
+ if (!name && isInteractive()) {
515
+ name = await prompt('Project name:', {
516
+ validate: validateProjectName,
517
+ });
518
+ }
519
+
520
+ if (!name) {
521
+ throw new CLIError(
522
+ 'Project name is required. Usage: bueno new <project-name>',
523
+ CLIErrorType.INVALID_ARGS,
524
+ );
525
+ }
526
+
527
+ const validation = validateProjectName(name);
528
+ if (validation !== true) {
529
+ throw new CLIError(validation as string, CLIErrorType.INVALID_ARGS);
530
+ }
531
+
532
+ // Get options
533
+ let template = getOption(args, 'template', {
534
+ name: 'template',
535
+ alias: 't',
536
+ type: 'string',
537
+ description: '',
538
+ }) as ProjectTemplate;
539
+
540
+ let framework = getOption(args, 'framework', {
541
+ name: 'framework',
542
+ alias: 'f',
543
+ type: 'string',
544
+ description: '',
545
+ }) as FrontendFramework;
546
+
547
+ let database = getOption(args, 'database', {
548
+ name: 'database',
549
+ alias: 'd',
550
+ type: 'string',
551
+ description: '',
552
+ }) as DatabaseDriver;
553
+
554
+ const skipInstall = hasFlag(args, 'skip-install');
555
+ const skipGit = hasFlag(args, 'skip-git');
556
+ const docker = hasFlag(args, 'docker');
557
+
558
+ // Get deployment platforms (can be specified multiple times)
559
+ const deployPlatforms = getOptionValues(args, 'deploy');
560
+ const validPlatforms: DeployPlatform[] = ['render', 'fly', 'railway'];
561
+ const deploy: DeployPlatform[] = [];
562
+
563
+ for (const platform of deployPlatforms) {
564
+ if (validPlatforms.includes(platform as DeployPlatform)) {
565
+ if (!deploy.includes(platform as DeployPlatform)) {
566
+ deploy.push(platform as DeployPlatform);
567
+ }
568
+ } else {
569
+ throw new CLIError(
570
+ `Invalid deployment platform: ${platform}. Valid options are: ${validPlatforms.join(', ')}`,
571
+ CLIErrorType.INVALID_ARGS,
572
+ );
573
+ }
574
+ }
575
+
576
+ // Interactive prompts for missing options
577
+ if (!useDefaults && isInteractive()) {
578
+ if (!template) {
579
+ template = await select<ProjectTemplate>(
580
+ 'Select a template:',
581
+ [
582
+ { value: 'default', name: 'Default - Standard project with modules and database' },
583
+ { value: 'minimal', name: 'Minimal - Bare minimum project structure' },
584
+ { value: 'fullstack', name: 'Fullstack - Full-stack project with SSR and auth' },
585
+ { value: 'api', name: 'API - API-only project without frontend' },
586
+ ],
587
+ { default: 'default' },
588
+ );
589
+ }
590
+
591
+ if ((template === 'fullstack' || template === 'default') && !framework) {
592
+ framework = await select<FrontendFramework>(
593
+ 'Select a frontend framework:',
594
+ [
595
+ { value: 'react', name: 'React' },
596
+ { value: 'vue', name: 'Vue' },
597
+ { value: 'svelte', name: 'Svelte' },
598
+ { value: 'solid', name: 'Solid' },
599
+ ],
600
+ { default: 'react' },
601
+ );
602
+ }
603
+
604
+ if (!database) {
605
+ database = await select<DatabaseDriver>(
606
+ 'Select a database:',
607
+ [
608
+ { value: 'sqlite', name: 'SQLite - Local file-based database' },
609
+ { value: 'postgresql', name: 'PostgreSQL - Production-ready relational database' },
610
+ { value: 'mysql', name: 'MySQL - Popular relational database' },
611
+ ],
612
+ { default: 'sqlite' },
613
+ );
614
+ }
615
+ }
616
+
617
+ // Set defaults
618
+ template = template || 'default';
619
+ framework = framework || 'react';
620
+ database = database || 'sqlite';
621
+
622
+ const config: ProjectConfig = {
623
+ name,
624
+ template,
625
+ framework,
626
+ database,
627
+ skipInstall,
628
+ skipGit,
629
+ docker,
630
+ deploy,
631
+ };
632
+
633
+ // Check if directory exists
634
+ const projectPath = joinPaths(process.cwd(), kebabCase(name));
635
+ if (await fileExists(projectPath)) {
636
+ throw new CLIError(
637
+ `Directory already exists: ${kebabCase(name)}`,
638
+ CLIErrorType.FILE_EXISTS,
639
+ );
640
+ }
641
+
642
+ // Display project info
643
+ cliConsole.header(`Creating a new Bueno project: ${colors.cyan(name)}`);
644
+
645
+ const rows = [
646
+ ['Template', template],
647
+ ['Framework', framework],
648
+ ['Database', database],
649
+ ['Docker', docker ? colors.green('Yes') : colors.red('No')],
650
+ ['Deploy', deploy.length > 0 ? colors.green(deploy.map(getDeployPlatformName).join(', ')) : colors.red('None')],
651
+ ['Install dependencies', skipInstall ? colors.red('No') : colors.green('Yes')],
652
+ ['Initialize git', skipGit ? colors.red('No') : colors.green('Yes')],
653
+ ];
654
+
655
+ printTable(['Setting', 'Value'], rows);
656
+ cliConsole.log('');
657
+
658
+ // Create project
659
+ cliConsole.subheader('Creating project files...');
660
+ await createProjectFiles(projectPath, config);
661
+
662
+ // Install dependencies
663
+ if (!skipInstall) {
664
+ cliConsole.subheader('Installing dependencies...');
665
+ const installSpinner = spinner('Running bun install...');
666
+
667
+ try {
668
+ const proc = Bun.spawn(['bun', 'install'], {
669
+ cwd: projectPath,
670
+ stdout: 'pipe',
671
+ stderr: 'pipe',
672
+ });
673
+
674
+ const exitCode = await proc.exited;
675
+
676
+ if (exitCode === 0) {
677
+ installSpinner.success('Dependencies installed');
678
+ } else {
679
+ installSpinner.warn('Failed to install dependencies. Run `bun install` manually.');
680
+ }
681
+ } catch {
682
+ installSpinner.warn('Failed to install dependencies. Run `bun install` manually.');
683
+ }
684
+ }
685
+
686
+ // Initialize git
687
+ if (!skipGit) {
688
+ cliConsole.subheader('Initializing git repository...');
689
+ const gitSpinner = spinner('Running git init...');
690
+
691
+ try {
692
+ const proc = Bun.spawn(['git', 'init'], {
693
+ cwd: projectPath,
694
+ stdout: 'pipe',
695
+ stderr: 'pipe',
696
+ });
697
+
698
+ const exitCode = await proc.exited;
699
+
700
+ if (exitCode === 0) {
701
+ // Add all files
702
+ Bun.spawn(['git', 'add', '.'], { cwd: projectPath });
703
+ Bun.spawn(['git', 'commit', '-m', 'Initial commit from Bueno CLI'], {
704
+ cwd: projectPath,
705
+ });
706
+ gitSpinner.success('Git repository initialized');
707
+ } else {
708
+ gitSpinner.warn('Failed to initialize git. Run `git init` manually.');
709
+ }
710
+ } catch {
711
+ gitSpinner.warn('Failed to initialize git. Run `git init` manually.');
712
+ }
713
+ }
714
+
715
+ // Show success message
716
+ cliConsole.log('');
717
+ cliConsole.success(`Project created successfully!`);
718
+ cliConsole.log('');
719
+ cliConsole.log('Next steps:');
720
+ cliConsole.log(` ${colors.cyan(`cd ${kebabCase(name)}`)}`);
721
+ cliConsole.log(` ${colors.cyan('bun run dev')}`);
722
+ cliConsole.log('');
723
+ cliConsole.log(`Documentation: ${colors.dim('https://github.com/sivaraj/bueno')}`);
724
+ }
725
+
726
+ // Register the command
727
+ defineCommand(
728
+ {
729
+ name: 'new',
730
+ description: 'Create a new Bueno project',
731
+ positionals: [
732
+ {
733
+ name: 'name',
734
+ required: false,
735
+ description: 'Project name',
736
+ },
737
+ ],
738
+ options: [
739
+ {
740
+ name: 'template',
741
+ alias: 't',
742
+ type: 'string',
743
+ description: 'Project template (default, minimal, fullstack, api)',
744
+ },
745
+ {
746
+ name: 'framework',
747
+ alias: 'f',
748
+ type: 'string',
749
+ description: 'Frontend framework (react, vue, svelte, solid)',
750
+ },
751
+ {
752
+ name: 'database',
753
+ alias: 'd',
754
+ type: 'string',
755
+ description: 'Database driver (sqlite, postgresql, mysql)',
756
+ },
757
+ {
758
+ name: 'skip-install',
759
+ type: 'boolean',
760
+ default: false,
761
+ description: 'Skip dependency installation',
762
+ },
763
+ {
764
+ name: 'skip-git',
765
+ type: 'boolean',
766
+ default: false,
767
+ description: 'Skip git initialization',
768
+ },
769
+ {
770
+ name: 'docker',
771
+ type: 'boolean',
772
+ default: false,
773
+ description: 'Include Docker configuration (Dockerfile, docker-compose.yml)',
774
+ },
775
+ {
776
+ name: 'deploy',
777
+ type: 'string',
778
+ description: 'Deployment platform configuration (render, fly, railway). Can be specified multiple times.',
779
+ },
780
+ {
781
+ name: 'yes',
782
+ alias: 'y',
783
+ type: 'boolean',
784
+ default: false,
785
+ description: 'Use default options without prompts',
786
+ },
787
+ ],
788
+ examples: [
789
+ 'bueno new my-app',
790
+ 'bueno new my-api --template api',
791
+ 'bueno new my-fullstack --template fullstack --framework react',
792
+ 'bueno new my-project --database postgresql',
793
+ 'bueno new my-app --docker',
794
+ 'bueno new my-app --docker --database postgresql',
795
+ 'bueno new my-app --deploy render',
796
+ 'bueno new my-app --deploy fly',
797
+ 'bueno new my-app --deploy render --deploy fly',
798
+ 'bueno new my-app --docker --deploy render',
799
+ 'bueno new my-app --docker --database postgresql --deploy render',
800
+ 'bueno new my-app -y',
801
+ ],
802
+ },
803
+ handleNew,
804
+ );