@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,892 @@
1
+ /**
2
+ * Unit Tests for CLI Commands
3
+ *
4
+ * Tests command registration, generate command, migration command,
5
+ * help command, and command execution.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
9
+ import { registry, defineCommand, type CommandHandler, type RegisteredCommand } from '../../src/cli/commands';
10
+ import type { CommandDefinition, ParsedArgs } from '../../src/cli/core/args';
11
+ import { CLIError, CLIErrorType, run } from '../../src/cli/index';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+
15
+ // ============================================================================
16
+ // Command Registry Tests
17
+ // ============================================================================
18
+
19
+ describe('Command Registry', () => {
20
+ // Create a fresh registry for each test by using the existing registry
21
+ const testCommands: CommandDefinition[] = [];
22
+
23
+ beforeEach(() => {
24
+ // Clear any test commands we've added
25
+ testCommands.length = 0;
26
+ });
27
+
28
+ describe('register', () => {
29
+ test('should register a command with definition and handler', () => {
30
+ const definition: CommandDefinition = {
31
+ name: 'test-cmd',
32
+ description: 'Test command',
33
+ };
34
+ const handler: CommandHandler = () => {};
35
+
36
+ defineCommand(definition, handler);
37
+
38
+ expect(registry.has('test-cmd')).toBe(true);
39
+ });
40
+
41
+ test('should register command with alias', () => {
42
+ const definition: CommandDefinition = {
43
+ name: 'test-alias-cmd',
44
+ alias: 'tac',
45
+ description: 'Test command with alias',
46
+ };
47
+ const handler: CommandHandler = () => {};
48
+
49
+ defineCommand(definition, handler);
50
+
51
+ expect(registry.has('test-alias-cmd')).toBe(true);
52
+ expect(registry.has('tac')).toBe(true);
53
+ });
54
+ });
55
+
56
+ describe('get', () => {
57
+ test('should get registered command by name', () => {
58
+ const definition: CommandDefinition = {
59
+ name: 'get-test-cmd',
60
+ description: 'Test command',
61
+ };
62
+ const handler: CommandHandler = () => {};
63
+ defineCommand(definition, handler);
64
+
65
+ const cmd = registry.get('get-test-cmd');
66
+ expect(cmd).toBeDefined();
67
+ expect(cmd?.definition.name).toBe('get-test-cmd');
68
+ });
69
+
70
+ test('should get command by alias', () => {
71
+ const definition: CommandDefinition = {
72
+ name: 'get-alias-test',
73
+ alias: 'gat',
74
+ description: 'Test command',
75
+ };
76
+ const handler: CommandHandler = () => {};
77
+ defineCommand(definition, handler);
78
+
79
+ const cmd = registry.get('gat');
80
+ expect(cmd).toBeDefined();
81
+ expect(cmd?.definition.name).toBe('get-alias-test');
82
+ });
83
+
84
+ test('should return undefined for non-existent command', () => {
85
+ const cmd = registry.get('non-existent-command-xyz');
86
+ expect(cmd).toBeUndefined();
87
+ });
88
+ });
89
+
90
+ describe('has', () => {
91
+ test('should return true for registered command', () => {
92
+ const definition: CommandDefinition = {
93
+ name: 'has-test-cmd',
94
+ description: 'Test command',
95
+ };
96
+ defineCommand(definition, () => {});
97
+
98
+ expect(registry.has('has-test-cmd')).toBe(true);
99
+ });
100
+
101
+ test('should return false for non-existent command', () => {
102
+ expect(registry.has('non-existent-cmd-xyz')).toBe(false);
103
+ });
104
+ });
105
+
106
+ describe('getAll', () => {
107
+ test('should return all command definitions', () => {
108
+ const commands = registry.getAll();
109
+ expect(Array.isArray(commands)).toBe(true);
110
+ expect(commands.length).toBeGreaterThan(0);
111
+
112
+ // Check that built-in commands are registered
113
+ const names = commands.map(c => c.name);
114
+ expect(names).toContain('generate');
115
+ expect(names).toContain('help');
116
+ });
117
+ });
118
+
119
+ describe('execute', () => {
120
+ test('should execute command handler', async () => {
121
+ let executed = false;
122
+ const definition: CommandDefinition = {
123
+ name: 'execute-test-cmd',
124
+ description: 'Test command',
125
+ };
126
+ defineCommand(definition, () => {
127
+ executed = true;
128
+ });
129
+
130
+ const args: ParsedArgs = {
131
+ command: 'execute-test-cmd',
132
+ positionals: [],
133
+ options: {},
134
+ flags: new Set(),
135
+ };
136
+
137
+ await registry.execute('execute-test-cmd', args);
138
+ expect(executed).toBe(true);
139
+ });
140
+
141
+ test('should throw for non-existent command', async () => {
142
+ const args: ParsedArgs = {
143
+ command: 'non-existent-cmd',
144
+ positionals: [],
145
+ options: {},
146
+ flags: new Set(),
147
+ };
148
+
149
+ expect(async () => {
150
+ await registry.execute('non-existent-cmd', args);
151
+ }).toThrow('Unknown command: non-existent-cmd');
152
+ });
153
+
154
+ test('should pass arguments to handler', async () => {
155
+ let receivedArgs: ParsedArgs | null = null;
156
+ const definition: CommandDefinition = {
157
+ name: 'args-test-cmd',
158
+ description: 'Test command',
159
+ };
160
+ defineCommand(definition, (args) => {
161
+ receivedArgs = args;
162
+ });
163
+
164
+ const args: ParsedArgs = {
165
+ command: 'args-test-cmd',
166
+ positionals: ['pos1', 'pos2'],
167
+ options: { flag: true },
168
+ flags: new Set(['flag']),
169
+ };
170
+
171
+ await registry.execute('args-test-cmd', args);
172
+ expect(receivedArgs).toEqual(args);
173
+ });
174
+ });
175
+ });
176
+
177
+ // ============================================================================
178
+ // Generate Command Tests
179
+ // ============================================================================
180
+
181
+ describe('Generate Command', () => {
182
+ const testDir = path.join(process.cwd(), 'test-temp-generate');
183
+
184
+ beforeEach(async () => {
185
+ // Clean up and create test directory
186
+ if (fs.existsSync(testDir)) {
187
+ fs.rmSync(testDir, { recursive: true, force: true });
188
+ }
189
+ fs.mkdirSync(testDir, { recursive: true });
190
+
191
+ // Create a minimal Bueno project structure
192
+ fs.mkdirSync(path.join(testDir, 'server'), { recursive: true });
193
+ fs.mkdirSync(path.join(testDir, 'server', 'modules'), { recursive: true });
194
+ fs.mkdirSync(path.join(testDir, 'server', 'common'), { recursive: true });
195
+ fs.mkdirSync(path.join(testDir, 'server', 'database', 'migrations'), { recursive: true });
196
+
197
+ // Create package.json
198
+ fs.writeFileSync(
199
+ path.join(testDir, 'package.json'),
200
+ JSON.stringify({ name: 'test-project', dependencies: { bueno: 'latest' } }),
201
+ );
202
+ });
203
+
204
+ afterEach(async () => {
205
+ // Clean up test directory
206
+ if (fs.existsSync(testDir)) {
207
+ fs.rmSync(testDir, { recursive: true, force: true });
208
+ }
209
+ });
210
+
211
+ describe('Generator Types', () => {
212
+ const generatorTypes = [
213
+ { type: 'controller', short: 'c', expectedDir: 'modules' },
214
+ { type: 'service', short: 's', expectedDir: 'modules' },
215
+ { type: 'module', short: 'm', expectedDir: 'modules' },
216
+ { type: 'guard', short: 'gu', expectedDir: 'common/guards' },
217
+ { type: 'interceptor', short: 'i', expectedDir: 'common/interceptors' },
218
+ { type: 'pipe', short: 'p', expectedDir: 'common/pipes' },
219
+ { type: 'filter', short: 'f', expectedDir: 'common/filters' },
220
+ { type: 'dto', short: 'd', expectedDir: 'modules' },
221
+ { type: 'middleware', short: 'mw', expectedDir: 'common/middleware' },
222
+ { type: 'migration', short: 'mi', expectedDir: 'database/migrations' },
223
+ ];
224
+
225
+ for (const { type, short } of generatorTypes) {
226
+ test(`should have '${type}' generator registered with alias '${short}'`, () => {
227
+ expect(registry.has('generate')).toBe(true);
228
+ expect(registry.has('g')).toBe(true);
229
+ });
230
+ }
231
+ });
232
+
233
+ describe('Template Content', () => {
234
+ test('generate command should be registered', () => {
235
+ const cmd = registry.get('generate');
236
+ expect(cmd).toBeDefined();
237
+ expect(cmd?.definition.name).toBe('generate');
238
+ expect(cmd?.definition.alias).toBe('g');
239
+ });
240
+
241
+ test('generate command should have correct positionals defined', () => {
242
+ const cmd = registry.get('generate');
243
+ expect(cmd?.definition.positionals).toBeDefined();
244
+ expect(cmd?.definition.positionals?.length).toBe(2);
245
+ expect(cmd?.definition.positionals?.[0]?.name).toBe('type');
246
+ expect(cmd?.definition.positionals?.[1]?.name).toBe('name');
247
+ });
248
+
249
+ test('generate command should have options defined', () => {
250
+ const cmd = registry.get('generate');
251
+ expect(cmd?.definition.options).toBeDefined();
252
+ const optionNames = cmd?.definition.options?.map(o => o.name) ?? [];
253
+ expect(optionNames).toContain('module');
254
+ expect(optionNames).toContain('path');
255
+ expect(optionNames).toContain('dry-run');
256
+ expect(optionNames).toContain('force');
257
+ });
258
+ });
259
+ });
260
+
261
+ // ============================================================================
262
+ // Migration Command Tests
263
+ // ============================================================================
264
+
265
+ describe('Migration Command', () => {
266
+ test('migration command should be registered', () => {
267
+ const cmd = registry.get('migration');
268
+ expect(cmd).toBeDefined();
269
+ expect(cmd?.definition.name).toBe('migration');
270
+ });
271
+
272
+ test('migration command should have correct description', () => {
273
+ const cmd = registry.get('migration');
274
+ expect(cmd?.definition.description).toContain('migration');
275
+ });
276
+ });
277
+
278
+ // ============================================================================
279
+ // Help Command Tests
280
+ // ============================================================================
281
+
282
+ describe('Help Command', () => {
283
+ test('help command should be registered', () => {
284
+ const cmd = registry.get('help');
285
+ expect(cmd).toBeDefined();
286
+ expect(cmd?.definition.name).toBe('help');
287
+ });
288
+
289
+ test('help command should have correct description', () => {
290
+ const cmd = registry.get('help');
291
+ expect(cmd?.definition.description).toContain('help');
292
+ });
293
+
294
+ test('help command should have optional command positional', () => {
295
+ const cmd = registry.get('help');
296
+ expect(cmd?.definition.positionals).toBeDefined();
297
+ expect(cmd?.definition.positionals?.[0]?.name).toBe('command');
298
+ expect(cmd?.definition.positionals?.[0]?.required).toBe(false);
299
+ });
300
+
301
+ test('help command should have --all option', () => {
302
+ const cmd = registry.get('help');
303
+ const options = cmd?.definition.options ?? [];
304
+ const allOption = options.find(o => o.name === 'all');
305
+ expect(allOption).toBeDefined();
306
+ expect(allOption?.alias).toBe('a');
307
+ });
308
+ });
309
+
310
+ // ============================================================================
311
+ // Dev Command Tests
312
+ // ============================================================================
313
+
314
+ describe('Dev Command', () => {
315
+ test('dev command should be registered', () => {
316
+ const cmd = registry.get('dev');
317
+ expect(cmd).toBeDefined();
318
+ expect(cmd?.definition.name).toBe('dev');
319
+ });
320
+
321
+ test('dev command should have port option', () => {
322
+ const cmd = registry.get('dev');
323
+ const options = cmd?.definition.options ?? [];
324
+ const portOption = options.find(o => o.name === 'port');
325
+ expect(portOption).toBeDefined();
326
+ expect(portOption?.alias).toBe('p');
327
+ });
328
+ });
329
+
330
+ // ============================================================================
331
+ // Build Command Tests
332
+ // ============================================================================
333
+
334
+ describe('Build Command', () => {
335
+ test('build command should be registered', () => {
336
+ const cmd = registry.get('build');
337
+ expect(cmd).toBeDefined();
338
+ expect(cmd?.definition.name).toBe('build');
339
+ });
340
+
341
+ test('build command should have target option', () => {
342
+ const cmd = registry.get('build');
343
+ const options = cmd?.definition.options ?? [];
344
+ const targetOption = options.find(o => o.name === 'target');
345
+ expect(targetOption).toBeDefined();
346
+ expect(targetOption?.alias).toBe('t');
347
+ });
348
+ });
349
+
350
+ // ============================================================================
351
+ // Start Command Tests
352
+ // ============================================================================
353
+
354
+ describe('Start Command', () => {
355
+ test('start command should be registered', () => {
356
+ const cmd = registry.get('start');
357
+ expect(cmd).toBeDefined();
358
+ expect(cmd?.definition.name).toBe('start');
359
+ });
360
+ });
361
+
362
+ // ============================================================================
363
+ // New Command Tests
364
+ // ============================================================================
365
+
366
+ describe('New Command', () => {
367
+ test('new command should be registered', () => {
368
+ const cmd = registry.get('new');
369
+ expect(cmd).toBeDefined();
370
+ expect(cmd?.definition.name).toBe('new');
371
+ });
372
+
373
+ test('new command should have template option', () => {
374
+ const cmd = registry.get('new');
375
+ const options = cmd?.definition.options ?? [];
376
+ const templateOption = options.find(o => o.name === 'template');
377
+ expect(templateOption).toBeDefined();
378
+ expect(templateOption?.alias).toBe('t');
379
+ });
380
+
381
+ test('new command should have framework option', () => {
382
+ const cmd = registry.get('new');
383
+ const options = cmd?.definition.options ?? [];
384
+ const frameworkOption = options.find(o => o.name === 'framework');
385
+ expect(frameworkOption).toBeDefined();
386
+ expect(frameworkOption?.alias).toBe('f');
387
+ });
388
+ });
389
+
390
+ // ============================================================================
391
+ // CLI Error Tests
392
+ // ============================================================================
393
+
394
+ describe('CLIError', () => {
395
+ test('should create error with message and type', () => {
396
+ const error = new CLIError('Test error', CLIErrorType.INVALID_ARGS);
397
+ expect(error.message).toBe('Test error');
398
+ expect(error.type).toBe(CLIErrorType.INVALID_ARGS);
399
+ expect(error.name).toBe('CLIError');
400
+ });
401
+
402
+ test('should have default exit code of 1', () => {
403
+ const error = new CLIError('Test error', CLIErrorType.INVALID_ARGS);
404
+ expect(error.exitCode).toBe(1);
405
+ });
406
+
407
+ test('should accept custom exit code', () => {
408
+ const error = new CLIError('Test error', CLIErrorType.INVALID_ARGS, 2);
409
+ expect(error.exitCode).toBe(2);
410
+ });
411
+
412
+ test('should be instance of Error', () => {
413
+ const error = new CLIError('Test error', CLIErrorType.INVALID_ARGS);
414
+ expect(error).toBeInstanceOf(Error);
415
+ });
416
+ });
417
+
418
+ describe('CLIErrorType', () => {
419
+ test('should have all error types defined', () => {
420
+ expect(CLIErrorType.INVALID_ARGS).toBe('INVALID_ARGS');
421
+ expect(CLIErrorType.FILE_EXISTS).toBe('FILE_EXISTS');
422
+ expect(CLIErrorType.FILE_NOT_FOUND).toBe('FILE_NOT_FOUND');
423
+ expect(CLIErrorType.MODULE_NOT_FOUND).toBe('MODULE_NOT_FOUND');
424
+ expect(CLIErrorType.TEMPLATE_ERROR).toBe('TEMPLATE_ERROR');
425
+ expect(CLIErrorType.DATABASE_ERROR).toBe('DATABASE_ERROR');
426
+ expect(CLIErrorType.NETWORK_ERROR).toBe('NETWORK_ERROR');
427
+ expect(CLIErrorType.PERMISSION_ERROR).toBe('PERMISSION_ERROR');
428
+ });
429
+ });
430
+
431
+ // ============================================================================
432
+ // CLI Run Function Tests
433
+ // ============================================================================
434
+
435
+ describe('run function', () => {
436
+ // Note: These tests verify the run function behavior without actually executing commands
437
+
438
+ test('should show version with --version flag', async () => {
439
+ const originalExit = process.exit;
440
+ const exitMock = mock((code: number) => {
441
+ throw new Error(`Exit with code ${code}`);
442
+ });
443
+ process.exit = exitMock as typeof process.exit;
444
+
445
+ try {
446
+ await run(['--version']);
447
+ } catch (error) {
448
+ // Expected to throw due to process.exit mock
449
+ }
450
+
451
+ process.exit = originalExit;
452
+ });
453
+
454
+ test('should show help with --help flag', async () => {
455
+ const originalExit = process.exit;
456
+ const exitMock = mock((code: number) => {
457
+ throw new Error(`Exit with code ${code}`);
458
+ });
459
+ process.exit = exitMock as typeof process.exit;
460
+
461
+ try {
462
+ await run(['--help']);
463
+ } catch (error) {
464
+ // Expected to throw due to process.exit mock
465
+ }
466
+
467
+ process.exit = originalExit;
468
+ });
469
+
470
+ test('should show help with -h flag', async () => {
471
+ const originalExit = process.exit;
472
+ const exitMock = mock((code: number) => {
473
+ throw new Error(`Exit with code ${code}`);
474
+ });
475
+ process.exit = exitMock as typeof process.exit;
476
+
477
+ try {
478
+ await run(['-h']);
479
+ } catch (error) {
480
+ // Expected to throw due to process.exit mock
481
+ }
482
+
483
+ process.exit = originalExit;
484
+ });
485
+
486
+ test('should show help when no command provided', async () => {
487
+ const originalExit = process.exit;
488
+ const exitMock = mock((code: number) => {
489
+ throw new Error(`Exit with code ${code}`);
490
+ });
491
+ process.exit = exitMock as typeof process.exit;
492
+
493
+ try {
494
+ await run([]);
495
+ } catch (error) {
496
+ // Expected to throw due to process.exit mock
497
+ }
498
+
499
+ process.exit = originalExit;
500
+ });
501
+ });
502
+
503
+ // ============================================================================
504
+ // Command Definition Validation Tests
505
+ // ============================================================================
506
+
507
+ describe('Command Definition Validation', () => {
508
+ test('all built-in commands should have required properties', () => {
509
+ const commands = registry.getAll();
510
+
511
+ for (const cmd of commands) {
512
+ expect(cmd.name).toBeDefined();
513
+ expect(typeof cmd.name).toBe('string');
514
+ expect(cmd.name.length).toBeGreaterThan(0);
515
+ expect(cmd.description).toBeDefined();
516
+ expect(typeof cmd.description).toBe('string');
517
+ expect(cmd.description.length).toBeGreaterThan(0);
518
+ }
519
+ });
520
+
521
+ test('all commands with options should have valid option definitions', () => {
522
+ const commands = registry.getAll();
523
+
524
+ for (const cmd of commands) {
525
+ if (cmd.options) {
526
+ for (const opt of cmd.options) {
527
+ expect(opt.name).toBeDefined();
528
+ expect(typeof opt.name).toBe('string');
529
+ expect(opt.type).toBeDefined();
530
+ expect(['string', 'boolean', 'number']).toContain(opt.type);
531
+ expect(opt.description).toBeDefined();
532
+ expect(typeof opt.description).toBe('string');
533
+ }
534
+ }
535
+ }
536
+ });
537
+
538
+ test('all commands with positionals should have valid positional definitions', () => {
539
+ const commands = registry.getAll();
540
+
541
+ for (const cmd of commands) {
542
+ if (cmd.positionals) {
543
+ for (const pos of cmd.positionals) {
544
+ expect(pos.name).toBeDefined();
545
+ expect(typeof pos.name).toBe('string');
546
+ expect(pos.required).toBeDefined();
547
+ expect(typeof pos.required).toBe('boolean');
548
+ expect(pos.description).toBeDefined();
549
+ expect(typeof pos.description).toBe('string');
550
+ }
551
+ }
552
+ }
553
+ });
554
+
555
+ test('all command aliases should be unique', () => {
556
+ const commands = registry.getAll();
557
+ const aliases = new Set<string>();
558
+
559
+ for (const cmd of commands) {
560
+ if (cmd.alias) {
561
+ expect(aliases.has(cmd.alias)).toBe(false);
562
+ aliases.add(cmd.alias);
563
+ }
564
+ }
565
+ });
566
+
567
+ test('all command names should be unique', () => {
568
+ const commands = registry.getAll();
569
+ const names = new Set<string>();
570
+
571
+ for (const cmd of commands) {
572
+ expect(names.has(cmd.name)).toBe(false);
573
+ names.add(cmd.name);
574
+ }
575
+ });
576
+ });
577
+
578
+ // ============================================================================
579
+ // Generator Alias Tests
580
+ // ============================================================================
581
+
582
+ describe('Generator Aliases', () => {
583
+ const aliasMap: Record<string, string> = {
584
+ c: 'controller',
585
+ s: 'service',
586
+ m: 'module',
587
+ gu: 'guard',
588
+ i: 'interceptor',
589
+ p: 'pipe',
590
+ f: 'filter',
591
+ d: 'dto',
592
+ mw: 'middleware',
593
+ mi: 'migration',
594
+ };
595
+
596
+ test('generate command should accept all type aliases', () => {
597
+ const cmd = registry.get('generate');
598
+ expect(cmd).toBeDefined();
599
+
600
+ // The generate command should handle these aliases internally
601
+ // We verify the command is properly registered
602
+ expect(cmd?.definition.positionals?.[0]?.name).toBe('type');
603
+ });
604
+ });
605
+
606
+ // ============================================================================
607
+ // Spinner Tests
608
+ // ============================================================================
609
+
610
+ import { Spinner, spinner, ProgressBar, progressBar, runTasks } from '../../src/cli/core/spinner';
611
+
612
+ describe('Spinner', () => {
613
+ test('should create spinner with default options', () => {
614
+ const s = new Spinner();
615
+ expect(s).toBeDefined();
616
+ });
617
+
618
+ test('should create spinner with custom text', () => {
619
+ const s = new Spinner({ text: 'Loading...' });
620
+ expect(s).toBeDefined();
621
+ });
622
+
623
+ test('should create spinner with custom color', () => {
624
+ const s = new Spinner({ text: 'Loading...', color: 'green' });
625
+ expect(s).toBeDefined();
626
+ });
627
+
628
+ test('start should return spinner instance', () => {
629
+ const s = new Spinner({ text: 'Loading...' });
630
+ const result = s.start();
631
+ expect(result).toBe(s);
632
+ s.stop();
633
+ });
634
+
635
+ test('update should change text', () => {
636
+ const s = new Spinner({ text: 'Loading...' });
637
+ s.start();
638
+ const result = s.update('Still loading...');
639
+ expect(result).toBe(s);
640
+ s.stop();
641
+ });
642
+
643
+ test('success should stop with success symbol', () => {
644
+ const s = new Spinner({ text: 'Loading...' });
645
+ s.start();
646
+ const result = s.success('Done!');
647
+ expect(result).toBe(s);
648
+ });
649
+
650
+ test('error should stop with error symbol', () => {
651
+ const s = new Spinner({ text: 'Loading...' });
652
+ s.start();
653
+ const result = s.error('Failed!');
654
+ expect(result).toBe(s);
655
+ });
656
+
657
+ test('warn should stop with warning symbol', () => {
658
+ const s = new Spinner({ text: 'Loading...' });
659
+ s.start();
660
+ const result = s.warn('Warning!');
661
+ expect(result).toBe(s);
662
+ });
663
+
664
+ test('info should stop with info symbol', () => {
665
+ const s = new Spinner({ text: 'Loading...' });
666
+ s.start();
667
+ const result = s.info('Info!');
668
+ expect(result).toBe(s);
669
+ });
670
+ });
671
+
672
+ describe('spinner factory', () => {
673
+ test('should create and start spinner', () => {
674
+ const s = spinner('Loading...');
675
+ expect(s).toBeInstanceOf(Spinner);
676
+ s.stop();
677
+ });
678
+ });
679
+
680
+ describe('ProgressBar', () => {
681
+ test('should create progress bar with required options', () => {
682
+ const pb = new ProgressBar({ total: 100 });
683
+ expect(pb).toBeDefined();
684
+ });
685
+
686
+ test('should create progress bar with custom width', () => {
687
+ const pb = new ProgressBar({ total: 100, width: 50 });
688
+ expect(pb).toBeDefined();
689
+ });
690
+
691
+ test('start should return progress bar instance', () => {
692
+ const pb = new ProgressBar({ total: 100 });
693
+ const result = pb.start();
694
+ expect(result).toBe(pb);
695
+ });
696
+
697
+ test('update should change current value', () => {
698
+ const pb = new ProgressBar({ total: 100 });
699
+ pb.start();
700
+ const result = pb.update(50);
701
+ expect(result).toBe(pb);
702
+ });
703
+
704
+ test('increment should increase current value', () => {
705
+ const pb = new ProgressBar({ total: 100 });
706
+ pb.start();
707
+ const result = pb.increment(5);
708
+ expect(result).toBe(pb);
709
+ });
710
+
711
+ test('complete should set to total', () => {
712
+ const pb = new ProgressBar({ total: 100 });
713
+ pb.start();
714
+ const result = pb.complete();
715
+ expect(result).toBe(pb);
716
+ });
717
+ });
718
+
719
+ describe('progressBar factory', () => {
720
+ test('should create progress bar', () => {
721
+ const pb = progressBar({ total: 100 });
722
+ expect(pb).toBeInstanceOf(ProgressBar);
723
+ });
724
+ });
725
+
726
+ describe('runTasks', () => {
727
+ test('should run tasks sequentially', async () => {
728
+ const order: string[] = [];
729
+
730
+ await runTasks([
731
+ {
732
+ text: 'Task 1',
733
+ task: async () => {
734
+ order.push('1');
735
+ },
736
+ },
737
+ {
738
+ text: 'Task 2',
739
+ task: async () => {
740
+ order.push('2');
741
+ },
742
+ },
743
+ ]);
744
+
745
+ expect(order).toEqual(['1', '2']);
746
+ });
747
+
748
+ test('should throw on task failure', async () => {
749
+ expect(async () => {
750
+ await runTasks([
751
+ {
752
+ text: 'Failing task',
753
+ task: async () => {
754
+ throw new Error('Task failed');
755
+ },
756
+ },
757
+ ]);
758
+ }).toThrow();
759
+ });
760
+ });
761
+
762
+ // ============================================================================
763
+ // Prompt Tests (Non-Interactive Mode)
764
+ // ============================================================================
765
+
766
+ import {
767
+ isInteractive,
768
+ prompt,
769
+ confirm,
770
+ select,
771
+ multiSelect,
772
+ number,
773
+ password,
774
+ } from '../../src/cli/core/prompt';
775
+
776
+ describe('Prompt Utilities (Non-Interactive Mode)', () => {
777
+ describe('isInteractive', () => {
778
+ test('should return boolean or undefined (falsy value)', () => {
779
+ const result = isInteractive();
780
+ // Note: isInteractive() returns process.stdin.isTTY && process.stdout.isTTY
781
+ // In non-TTY environments, this can be undefined (not false) due to && behavior
782
+ // We test that it's a falsy value in CI environments
783
+ expect(!result || typeof result === 'boolean').toBe(true);
784
+ });
785
+ });
786
+
787
+ describe('prompt (non-interactive fallback)', () => {
788
+ test('should return default value when not interactive', async () => {
789
+ // This test assumes non-interactive mode in CI
790
+ if (!isInteractive()) {
791
+ const result = await prompt('Enter value', { default: 'default-value' });
792
+ expect(result).toBe('default-value');
793
+ }
794
+ });
795
+
796
+ test('should return empty string when no default and not interactive', async () => {
797
+ if (!isInteractive()) {
798
+ const result = await prompt('Enter value');
799
+ expect(result).toBe('');
800
+ }
801
+ });
802
+ });
803
+
804
+ describe('confirm (non-interactive fallback)', () => {
805
+ test('should return default value when not interactive', async () => {
806
+ if (!isInteractive()) {
807
+ const result = await confirm('Are you sure?', { default: true });
808
+ expect(result).toBe(true);
809
+ }
810
+ });
811
+
812
+ test('should return false when no default and not interactive', async () => {
813
+ if (!isInteractive()) {
814
+ const result = await confirm('Are you sure?');
815
+ expect(result).toBe(false);
816
+ }
817
+ });
818
+ });
819
+
820
+ describe('select (non-interactive fallback)', () => {
821
+ test('should return default value when not interactive', async () => {
822
+ if (!isInteractive()) {
823
+ const choices = [
824
+ { value: 'a', name: 'Option A' },
825
+ { value: 'b', name: 'Option B' },
826
+ ];
827
+ const result = await select('Choose option', choices, { default: 'b' });
828
+ expect(result).toBe('b');
829
+ }
830
+ });
831
+
832
+ test('should return first choice when no default and not interactive', async () => {
833
+ if (!isInteractive()) {
834
+ const choices = [
835
+ { value: 'a', name: 'Option A' },
836
+ { value: 'b', name: 'Option B' },
837
+ ];
838
+ const result = await select('Choose option', choices);
839
+ expect(result).toBe('a');
840
+ }
841
+ });
842
+ });
843
+
844
+ describe('multiSelect (non-interactive fallback)', () => {
845
+ test('should return default values when not interactive', async () => {
846
+ if (!isInteractive()) {
847
+ const choices = [
848
+ { value: 'a', name: 'Option A' },
849
+ { value: 'b', name: 'Option B' },
850
+ ];
851
+ const result = await multiSelect('Choose options', choices, { default: ['a'] });
852
+ expect(result).toEqual(['a']);
853
+ }
854
+ });
855
+
856
+ test('should return empty array when no default and not interactive', async () => {
857
+ if (!isInteractive()) {
858
+ const choices = [
859
+ { value: 'a', name: 'Option A' },
860
+ { value: 'b', name: 'Option B' },
861
+ ];
862
+ const result = await multiSelect('Choose options', choices);
863
+ expect(result).toEqual([]);
864
+ }
865
+ });
866
+ });
867
+
868
+ describe('number (non-interactive fallback)', () => {
869
+ test('should return default value when not interactive', async () => {
870
+ if (!isInteractive()) {
871
+ const result = await number('Enter number', { default: '42' });
872
+ expect(result).toBe(42);
873
+ }
874
+ });
875
+
876
+ test('should return 0 when no default and not interactive', async () => {
877
+ if (!isInteractive()) {
878
+ const result = await number('Enter number');
879
+ expect(result).toBe(0);
880
+ }
881
+ });
882
+ });
883
+
884
+ describe('password (non-interactive fallback)', () => {
885
+ test('should return empty string when not interactive', async () => {
886
+ if (!isInteractive()) {
887
+ const result = await password('Enter password');
888
+ expect(result).toBe('');
889
+ }
890
+ });
891
+ });
892
+ });