@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,541 @@
1
+ /**
2
+ * Generate Command
3
+ *
4
+ * Generate code artifacts (controllers, services, modules, etc.)
5
+ */
6
+
7
+ import { defineCommand } from './index';
8
+ import { getOption, hasFlag, type ParsedArgs } from '../core/args';
9
+ import { cliConsole, colors } from '../core/console';
10
+ import { confirm, isInteractive } from '../core/prompt';
11
+ import { spinner } from '../core/spinner';
12
+ import {
13
+ fileExists,
14
+ writeFile,
15
+ readFile,
16
+ getProjectRoot,
17
+ isBuenoProject,
18
+ joinPaths,
19
+ processTemplate,
20
+ } from '../utils/fs';
21
+ import { kebabCase, pascalCase, camelCase } from '../utils/strings';
22
+ import { CLIError, CLIErrorType } from '../index';
23
+
24
+ /**
25
+ * Generator types
26
+ */
27
+ type GeneratorType =
28
+ | 'controller'
29
+ | 'service'
30
+ | 'module'
31
+ | 'guard'
32
+ | 'interceptor'
33
+ | 'pipe'
34
+ | 'filter'
35
+ | 'dto'
36
+ | 'middleware'
37
+ | 'migration';
38
+
39
+ /**
40
+ * Generator aliases
41
+ */
42
+ const GENERATOR_ALIASES: Record<string, GeneratorType> = {
43
+ c: 'controller',
44
+ s: 'service',
45
+ m: 'module',
46
+ gu: 'guard',
47
+ i: 'interceptor',
48
+ p: 'pipe',
49
+ f: 'filter',
50
+ d: 'dto',
51
+ mw: 'middleware',
52
+ mi: 'migration',
53
+ };
54
+
55
+ /**
56
+ * Generator configuration
57
+ */
58
+ interface GeneratorConfig {
59
+ type: GeneratorType;
60
+ name: string;
61
+ module?: string;
62
+ path?: string;
63
+ dryRun: boolean;
64
+ force: boolean;
65
+ }
66
+
67
+ /**
68
+ * Get template content for a generator type
69
+ */
70
+ function getTemplate(type: GeneratorType): string {
71
+ const templates: Record<GeneratorType, string> = {
72
+ controller: `import { Controller, Get, Post, Put, Delete{{#if path}} } from 'bueno'{{/if}}{{#if service}}, { {{pascalCase service}}Service } from './{{kebabCase service}}.service'{{/if}};
73
+ import type { Context } from 'bueno';
74
+
75
+ @Controller('{{path}}')
76
+ export class {{pascalCase name}}Controller {
77
+ {{#if service}}
78
+ constructor(private readonly {{camelCase service}}Service: {{pascalCase service}}Service) {}
79
+ {{/if}}
80
+
81
+ @Get()
82
+ async findAll(ctx: Context) {
83
+ return { message: '{{pascalCase name}} controller' };
84
+ }
85
+
86
+ @Get(':id')
87
+ async findOne(ctx: Context) {
88
+ const id = ctx.params.id;
89
+ return { id, message: '{{pascalCase name}} item' };
90
+ }
91
+
92
+ @Post()
93
+ async create(ctx: Context) {
94
+ const body = await ctx.body();
95
+ return { message: 'Created', data: body };
96
+ }
97
+
98
+ @Put(':id')
99
+ async update(ctx: Context) {
100
+ const id = ctx.params.id;
101
+ const body = await ctx.body();
102
+ return { id, message: 'Updated', data: body };
103
+ }
104
+
105
+ @Delete(':id')
106
+ async remove(ctx: Context) {
107
+ const id = ctx.params.id;
108
+ return { id, message: 'Deleted' };
109
+ }
110
+ }
111
+ `,
112
+ service: `import { Injectable } from 'bueno';
113
+
114
+ @Injectable()
115
+ export class {{pascalCase name}}Service {
116
+ async findAll() {
117
+ // TODO: Implement findAll
118
+ return [];
119
+ }
120
+
121
+ async findOne(id: string) {
122
+ // TODO: Implement findOne
123
+ return { id };
124
+ }
125
+
126
+ async create(data: unknown) {
127
+ // TODO: Implement create
128
+ return data;
129
+ }
130
+
131
+ async update(id: string, data: unknown) {
132
+ // TODO: Implement update
133
+ return { id, ...data };
134
+ }
135
+
136
+ async remove(id: string) {
137
+ // TODO: Implement remove
138
+ return { id };
139
+ }
140
+ }
141
+ `,
142
+ module: `import { Module } from 'bueno';
143
+ import { {{pascalCase name}}Controller } from './{{kebabCase name}}.controller';
144
+ import { {{pascalCase name}}Service } from './{{kebabCase name}}.service';
145
+
146
+ @Module({
147
+ controllers: [{{pascalCase name}}Controller],
148
+ providers: [{{pascalCase name}}Service],
149
+ exports: [{{pascalCase name}}Service],
150
+ })
151
+ export class {{pascalCase name}}Module {}
152
+ `,
153
+ guard: `import { Injectable, type CanActivate, type Context } from 'bueno';
154
+
155
+ @Injectable()
156
+ export class {{pascalCase name}}Guard implements CanActivate {
157
+ async canActivate(ctx: Context): Promise<boolean> {
158
+ // TODO: Implement guard logic
159
+ // Return true to allow access, false to deny
160
+ return true;
161
+ }
162
+ }
163
+ `,
164
+ interceptor: `import { Injectable, type NestInterceptor, type CallHandler, type Context } from 'bueno';
165
+ import type { Observable } from 'rxjs';
166
+
167
+ @Injectable()
168
+ export class {{pascalCase name}}Interceptor implements NestInterceptor {
169
+ async intercept(ctx: Context, next: CallHandler): Promise<Observable<unknown>> {
170
+ // Before handler execution
171
+ console.log('{{pascalCase name}}Interceptor - Before');
172
+
173
+ // Call the handler
174
+ const result = await next.handle();
175
+
176
+ // After handler execution
177
+ console.log('{{pascalCase name}}Interceptor - After');
178
+
179
+ return result;
180
+ }
181
+ }
182
+ `,
183
+ pipe: `import { Injectable, type PipeTransform, type Context } from 'bueno';
184
+
185
+ @Injectable()
186
+ export class {{pascalCase name}}Pipe implements PipeTransform {
187
+ async transform(value: unknown, ctx: Context): Promise<unknown> {
188
+ // TODO: Implement transformation/validation logic
189
+ // Throw an error to reject the value
190
+ return value;
191
+ }
192
+ }
193
+ `,
194
+ filter: `import { Injectable, type ExceptionFilter, type Context } from 'bueno';
195
+ import type { Response } from 'bueno';
196
+
197
+ @Injectable()
198
+ export class {{pascalCase name}}Filter implements ExceptionFilter {
199
+ async catch(exception: Error, ctx: Context): Promise<Response> {
200
+ // TODO: Implement exception handling
201
+ console.error('{{pascalCase name}}Filter caught:', exception);
202
+
203
+ return new Response(
204
+ JSON.stringify({
205
+ statusCode: 500,
206
+ message: 'Internal Server Error',
207
+ error: exception.message,
208
+ }),
209
+ {
210
+ status: 500,
211
+ headers: { 'Content-Type': 'application/json' },
212
+ }
213
+ );
214
+ }
215
+ }
216
+ `,
217
+ dto: `/**
218
+ * {{pascalCase name}} DTO
219
+ */
220
+ export interface {{pascalCase name}}Dto {
221
+ // TODO: Define properties
222
+ id?: string;
223
+ createdAt?: Date;
224
+ updatedAt?: Date;
225
+ }
226
+
227
+ /**
228
+ * Create {{pascalCase name}} DTO
229
+ */
230
+ export interface Create{{pascalCase name}}Dto {
231
+ // TODO: Define required properties for creation
232
+ }
233
+
234
+ /**
235
+ * Update {{pascalCase name}} DTO
236
+ */
237
+ export interface Update{{pascalCase name}}Dto extends Partial<Create{{pascalCase name}}Dto> {
238
+ // TODO: Define optional properties for update
239
+ }
240
+ `,
241
+ middleware: `import type { Middleware, Context, Handler } from 'bueno';
242
+
243
+ /**
244
+ * {{pascalCase name}} Middleware
245
+ */
246
+ export const {{camelCase name}}Middleware: Middleware = async (
247
+ ctx: Context,
248
+ next: Handler
249
+ ) => {
250
+ // Before handler execution
251
+ console.log('{{pascalCase name}}Middleware - Before');
252
+
253
+ // Call the next handler
254
+ const result = await next();
255
+
256
+ // After handler execution
257
+ console.log('{{pascalCase name}}Middleware - After');
258
+
259
+ return result;
260
+ };
261
+ `,
262
+ migration: `import { createMigration, type MigrationRunner } from 'bueno';
263
+
264
+ export default createMigration('{{migrationId}}', '{{migrationName}}')
265
+ .up(async (db: MigrationRunner) => {
266
+ // TODO: Add migration logic
267
+ // Example:
268
+ // await db.createTable({
269
+ // name: '{{tableName}}',
270
+ // columns: [
271
+ // { name: 'id', type: 'uuid', primary: true },
272
+ // { name: 'created_at', type: 'timestamp', default: 'NOW()' },
273
+ // ],
274
+ // });
275
+ })
276
+ .down(async (db: MigrationRunner) => {
277
+ // TODO: Add rollback logic
278
+ // Example:
279
+ // await db.dropTable('{{tableName}}');
280
+ });
281
+ `,
282
+ };
283
+
284
+ return templates[type];
285
+ }
286
+
287
+ /**
288
+ * Get file extension for generator type
289
+ */
290
+ function getFileExtension(type: GeneratorType): string {
291
+ return type === 'dto' ? '.dto.ts' : '.ts';
292
+ }
293
+
294
+ /**
295
+ * Get default directory for generator type
296
+ */
297
+ function getDefaultDirectory(type: GeneratorType): string {
298
+ switch (type) {
299
+ case 'controller':
300
+ case 'service':
301
+ case 'module':
302
+ case 'dto':
303
+ return 'modules';
304
+ case 'guard':
305
+ return 'common/guards';
306
+ case 'interceptor':
307
+ return 'common/interceptors';
308
+ case 'pipe':
309
+ return 'common/pipes';
310
+ case 'filter':
311
+ return 'common/filters';
312
+ case 'middleware':
313
+ return 'common/middleware';
314
+ case 'migration':
315
+ return 'database/migrations';
316
+ default:
317
+ return '';
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Generate a file
323
+ */
324
+ async function generateFile(config: GeneratorConfig): Promise<string> {
325
+ const { type, name, module, path: customPath, dryRun, force } = config;
326
+
327
+ // Get project root
328
+ const projectRoot = await getProjectRoot();
329
+ if (!projectRoot) {
330
+ throw new CLIError(
331
+ 'Not in a Bueno project directory',
332
+ CLIErrorType.NOT_FOUND,
333
+ );
334
+ }
335
+
336
+ // Determine file path
337
+ const kebabName = kebabCase(name);
338
+ const defaultDir = getDefaultDirectory(type);
339
+ let targetDir: string;
340
+
341
+ if (customPath) {
342
+ targetDir = joinPaths(projectRoot, customPath);
343
+ } else if (module) {
344
+ targetDir = joinPaths(projectRoot, 'server', defaultDir, kebabCase(module));
345
+ } else if (type === 'migration') {
346
+ targetDir = joinPaths(projectRoot, 'server', defaultDir);
347
+ } else {
348
+ targetDir = joinPaths(projectRoot, 'server', defaultDir, kebabName);
349
+ }
350
+
351
+ const fileName = type === 'migration'
352
+ ? `${generateMigrationId()}_${kebabName}${getFileExtension(type)}`
353
+ : `${kebabName}${getFileExtension(type)}`;
354
+ const filePath = joinPaths(targetDir, fileName);
355
+
356
+ // Check if file exists
357
+ if (!force && await fileExists(filePath)) {
358
+ if (isInteractive()) {
359
+ const shouldOverwrite = await confirm(
360
+ `File ${colors.cyan(filePath)} already exists. Overwrite?`,
361
+ { default: false },
362
+ );
363
+ if (!shouldOverwrite) {
364
+ throw new CLIError(
365
+ 'File already exists. Use --force to overwrite.',
366
+ CLIErrorType.FILE_EXISTS,
367
+ );
368
+ }
369
+ } else {
370
+ throw new CLIError(
371
+ `File already exists: ${filePath}. Use --force to overwrite.`,
372
+ CLIErrorType.FILE_EXISTS,
373
+ );
374
+ }
375
+ }
376
+
377
+ // Get template and process it
378
+ const template = getTemplate(type);
379
+ const content = processTemplate(template, {
380
+ name,
381
+ module: module ?? '',
382
+ path: customPath ?? kebabName,
383
+ service: type === 'controller' ? name : '',
384
+ migrationId: generateMigrationId(),
385
+ migrationName: name,
386
+ tableName: kebabName,
387
+ });
388
+
389
+ // Write file or show dry run
390
+ if (dryRun) {
391
+ cliConsole.log(`\n${colors.bold('File:')} ${filePath}`);
392
+ cliConsole.log(colors.bold('Content:'));
393
+ cliConsole.log(content);
394
+ cliConsole.log('');
395
+ } else {
396
+ await writeFile(filePath, content);
397
+ }
398
+
399
+ return filePath;
400
+ }
401
+
402
+ /**
403
+ * Generate migration ID
404
+ */
405
+ function generateMigrationId(): string {
406
+ const now = new Date();
407
+ const year = now.getFullYear();
408
+ const month = String(now.getMonth() + 1).padStart(2, '0');
409
+ const day = String(now.getDate()).padStart(2, '0');
410
+ const hour = String(now.getHours()).padStart(2, '0');
411
+ const minute = String(now.getMinutes()).padStart(2, '0');
412
+ const second = String(now.getSeconds()).padStart(2, '0');
413
+ return `${year}${month}${day}${hour}${minute}${second}`;
414
+ }
415
+
416
+ /**
417
+ * Handle generate command
418
+ */
419
+ async function handleGenerate(args: ParsedArgs): Promise<void> {
420
+ // Get generator type
421
+ const typeArg = args.positionals[0];
422
+ if (!typeArg) {
423
+ throw new CLIError(
424
+ 'Generator type is required. Usage: bueno generate <type> <name>',
425
+ CLIErrorType.INVALID_ARGS,
426
+ );
427
+ }
428
+
429
+ const type = GENERATOR_ALIASES[typeArg] ?? typeArg as GeneratorType;
430
+ if (!getTemplate(type)) {
431
+ throw new CLIError(
432
+ `Unknown generator type: ${typeArg}. Available types: controller, service, module, guard, interceptor, pipe, filter, dto, middleware, migration`,
433
+ CLIErrorType.INVALID_ARGS,
434
+ );
435
+ }
436
+
437
+ // Get name
438
+ const name = args.positionals[1];
439
+ if (!name) {
440
+ throw new CLIError(
441
+ 'Name is required. Usage: bueno generate <type> <name>',
442
+ CLIErrorType.INVALID_ARGS,
443
+ );
444
+ }
445
+
446
+ // Get options
447
+ const config: GeneratorConfig = {
448
+ type,
449
+ name,
450
+ module: getOption<string>(args, 'module', {
451
+ name: 'module',
452
+ type: 'string',
453
+ description: '',
454
+ }),
455
+ path: getOption<string>(args, 'path', {
456
+ name: 'path',
457
+ type: 'string',
458
+ description: '',
459
+ }),
460
+ dryRun: hasFlag(args, 'dry-run'),
461
+ force: hasFlag(args, 'force'),
462
+ };
463
+
464
+ // Check if in a Bueno project
465
+ if (!config.dryRun && !(await isBuenoProject())) {
466
+ throw new CLIError(
467
+ 'Not in a Bueno project directory. Run this command from a Bueno project.',
468
+ CLIErrorType.NOT_FOUND,
469
+ );
470
+ }
471
+
472
+ // Generate
473
+ const s = spinner(`Generating ${colors.cyan(type)} ${colors.cyan(name)}...`);
474
+
475
+ try {
476
+ const filePath = await generateFile(config);
477
+
478
+ if (config.dryRun) {
479
+ s.info('Dry run complete');
480
+ } else {
481
+ s.success(`Created ${colors.green(filePath)}`);
482
+ }
483
+ } catch (error) {
484
+ s.error();
485
+ throw error;
486
+ }
487
+ }
488
+
489
+ // Register the command
490
+ defineCommand(
491
+ {
492
+ name: 'generate',
493
+ alias: 'g',
494
+ description: 'Generate code artifacts (controllers, services, modules, etc.)',
495
+ positionals: [
496
+ {
497
+ name: 'type',
498
+ required: true,
499
+ description: 'Type of artifact to generate (controller, service, module, guard, interceptor, pipe, filter, dto, middleware, migration)',
500
+ },
501
+ {
502
+ name: 'name',
503
+ required: true,
504
+ description: 'Name of the artifact',
505
+ },
506
+ ],
507
+ options: [
508
+ {
509
+ name: 'module',
510
+ alias: 'm',
511
+ type: 'string',
512
+ description: 'Parent module to register with',
513
+ },
514
+ {
515
+ name: 'path',
516
+ type: 'string',
517
+ description: 'Custom path for controller routes',
518
+ },
519
+ {
520
+ name: 'dry-run',
521
+ type: 'boolean',
522
+ default: false,
523
+ description: 'Show what would be created without writing',
524
+ },
525
+ {
526
+ name: 'force',
527
+ type: 'boolean',
528
+ default: false,
529
+ description: 'Overwrite existing files',
530
+ },
531
+ ],
532
+ examples: [
533
+ 'bueno generate controller users',
534
+ 'bueno g service auth',
535
+ 'bueno g module posts',
536
+ 'bueno g guard auth-guard --module auth',
537
+ 'bueno g dto create-user --module users',
538
+ ],
539
+ },
540
+ handleGenerate,
541
+ );
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Help Command
3
+ *
4
+ * Display help information for commands
5
+ */
6
+
7
+ import { defineCommand } from './index';
8
+ import { generateGlobalHelpText, generateHelpText, hasFlag } from '../core/args';
9
+ import { cliConsole } from '../core/console';
10
+ import { registry } from './index';
11
+
12
+ defineCommand(
13
+ {
14
+ name: 'help',
15
+ description: 'Show help information for commands',
16
+ positionals: [
17
+ {
18
+ name: 'command',
19
+ required: false,
20
+ description: 'Command to show help for',
21
+ },
22
+ ],
23
+ options: [
24
+ {
25
+ name: 'all',
26
+ alias: 'a',
27
+ type: 'boolean',
28
+ default: false,
29
+ description: 'Show help for all commands',
30
+ },
31
+ ],
32
+ },
33
+ async (args) => {
34
+ const commandName = args.positionals[0];
35
+
36
+ if (commandName && registry.has(commandName)) {
37
+ // Show help for specific command
38
+ const cmd = registry.get(commandName);
39
+ if (cmd) {
40
+ cliConsole.log(generateHelpText(cmd.definition));
41
+ }
42
+ } else if (hasFlag(args, 'all')) {
43
+ // Show detailed help for all commands
44
+ cliConsole.log('\nBueno CLI - Available Commands\n');
45
+
46
+ for (const cmd of registry.getAll()) {
47
+ cliConsole.log(generateHelpText(cmd));
48
+ cliConsole.log('---');
49
+ }
50
+ } else {
51
+ // Show global help
52
+ cliConsole.log(generateGlobalHelpText(registry.getAll()));
53
+ }
54
+ },
55
+ );
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Command Registry for Bueno CLI
3
+ *
4
+ * Manages command registration and execution
5
+ */
6
+
7
+ import type { CommandDefinition, ParsedArgs } from '../core/args';
8
+
9
+ /**
10
+ * Command handler function type
11
+ */
12
+ export type CommandHandler = (args: ParsedArgs) => Promise<void> | void;
13
+
14
+ /**
15
+ * Registered command
16
+ */
17
+ export interface RegisteredCommand {
18
+ definition: CommandDefinition;
19
+ handler: CommandHandler;
20
+ }
21
+
22
+ /**
23
+ * Command registry
24
+ */
25
+ class CommandRegistry {
26
+ private commands: Map<string, RegisteredCommand> = new Map();
27
+ private aliases: Map<string, string> = new Map();
28
+
29
+ /**
30
+ * Register a command
31
+ */
32
+ register(
33
+ definition: CommandDefinition,
34
+ handler: CommandHandler,
35
+ ): void {
36
+ this.commands.set(definition.name, {
37
+ definition,
38
+ handler,
39
+ });
40
+
41
+ if (definition.alias) {
42
+ this.aliases.set(definition.alias, definition.name);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Get a command by name or alias
48
+ */
49
+ get(name: string): RegisteredCommand | undefined {
50
+ const commandName = this.aliases.get(name) ?? name;
51
+ return this.commands.get(commandName);
52
+ }
53
+
54
+ /**
55
+ * Check if a command exists
56
+ */
57
+ has(name: string): boolean {
58
+ const commandName = this.aliases.get(name) ?? name;
59
+ return this.commands.has(commandName);
60
+ }
61
+
62
+ /**
63
+ * Get all command definitions
64
+ */
65
+ getAll(): CommandDefinition[] {
66
+ return Array.from(this.commands.values()).map((c) => c.definition);
67
+ }
68
+
69
+ /**
70
+ * Get all registered commands
71
+ */
72
+ getCommands(): Map<string, RegisteredCommand> {
73
+ return new Map(this.commands);
74
+ }
75
+
76
+ /**
77
+ * Execute a command
78
+ */
79
+ async execute(name: string, args: ParsedArgs): Promise<void> {
80
+ const command = this.get(name);
81
+
82
+ if (!command) {
83
+ throw new Error(`Unknown command: ${name}`);
84
+ }
85
+
86
+ await command.handler(args);
87
+ }
88
+ }
89
+
90
+ // Global command registry instance
91
+ export const registry = new CommandRegistry();
92
+
93
+ /**
94
+ * Register a command (decorator-style)
95
+ */
96
+ export function command(
97
+ definition: CommandDefinition,
98
+ ): (handler: CommandHandler) => void {
99
+ return (handler: CommandHandler) => {
100
+ registry.register(definition, handler);
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Define a command with its handler
106
+ */
107
+ export function defineCommand(
108
+ definition: CommandDefinition,
109
+ handler: CommandHandler,
110
+ ): void {
111
+ registry.register(definition, handler);
112
+ }