@navios/commander 0.5.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 (72) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +242 -0
  3. package/dist/src/commander.application.d.mts +29 -0
  4. package/dist/src/commander.application.d.mts.map +1 -0
  5. package/dist/src/commander.factory.d.mts +8 -0
  6. package/dist/src/commander.factory.d.mts.map +1 -0
  7. package/dist/src/decorators/cli-module.decorator.d.mts +7 -0
  8. package/dist/src/decorators/cli-module.decorator.d.mts.map +1 -0
  9. package/dist/src/decorators/command.decorator.d.mts +8 -0
  10. package/dist/src/decorators/command.decorator.d.mts.map +1 -0
  11. package/dist/src/decorators/index.d.mts +3 -0
  12. package/dist/src/decorators/index.d.mts.map +1 -0
  13. package/dist/src/index.d.mts +8 -0
  14. package/dist/src/index.d.mts.map +1 -0
  15. package/dist/src/interfaces/cli-module.interface.d.mts +5 -0
  16. package/dist/src/interfaces/cli-module.interface.d.mts.map +1 -0
  17. package/dist/src/interfaces/command-handler.interface.d.mts +4 -0
  18. package/dist/src/interfaces/command-handler.interface.d.mts.map +1 -0
  19. package/dist/src/interfaces/index.d.mts +3 -0
  20. package/dist/src/interfaces/index.d.mts.map +1 -0
  21. package/dist/src/interfaces/module.interface.d.mts +5 -0
  22. package/dist/src/interfaces/module.interface.d.mts.map +1 -0
  23. package/dist/src/metadata/cli-module.metadata.d.mts +11 -0
  24. package/dist/src/metadata/cli-module.metadata.d.mts.map +1 -0
  25. package/dist/src/metadata/command.metadata.d.mts +12 -0
  26. package/dist/src/metadata/command.metadata.d.mts.map +1 -0
  27. package/dist/src/metadata/index.d.mts +3 -0
  28. package/dist/src/metadata/index.d.mts.map +1 -0
  29. package/dist/src/services/cli-parser.service.d.mts +44 -0
  30. package/dist/src/services/cli-parser.service.d.mts.map +1 -0
  31. package/dist/src/services/index.d.mts +3 -0
  32. package/dist/src/services/index.d.mts.map +1 -0
  33. package/dist/src/services/module-loader.service.d.mts +33 -0
  34. package/dist/src/services/module-loader.service.d.mts.map +1 -0
  35. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -0
  37. package/dist/tsup.config.d.mts +3 -0
  38. package/dist/tsup.config.d.mts.map +1 -0
  39. package/dist/vitest.config.d.mts +3 -0
  40. package/dist/vitest.config.d.mts.map +1 -0
  41. package/lib/_tsup-dts-rollup.d.mts +456 -0
  42. package/lib/_tsup-dts-rollup.d.ts +456 -0
  43. package/lib/index.d.mts +98 -0
  44. package/lib/index.d.ts +98 -0
  45. package/lib/index.js +541 -0
  46. package/lib/index.js.map +1 -0
  47. package/lib/index.mjs +524 -0
  48. package/lib/index.mjs.map +1 -0
  49. package/package.json +40 -0
  50. package/project.json +66 -0
  51. package/src/__tests__/commander.factory.e2e.spec.mts +965 -0
  52. package/src/commander.application.mts +159 -0
  53. package/src/commander.factory.mts +20 -0
  54. package/src/decorators/cli-module.decorator.mts +39 -0
  55. package/src/decorators/command.decorator.mts +29 -0
  56. package/src/decorators/index.mts +2 -0
  57. package/src/index.mts +7 -0
  58. package/src/interfaces/command-handler.interface.mts +3 -0
  59. package/src/interfaces/index.mts +2 -0
  60. package/src/interfaces/module.interface.mts +4 -0
  61. package/src/metadata/cli-module.metadata.mts +54 -0
  62. package/src/metadata/command.metadata.mts +54 -0
  63. package/src/metadata/index.mts +2 -0
  64. package/src/services/__tests__/cli-parser.service.spec.mts +404 -0
  65. package/src/services/cli-parser.service.mts +231 -0
  66. package/src/services/index.mts +2 -0
  67. package/src/services/module-loader.service.mts +120 -0
  68. package/tsconfig.json +18 -0
  69. package/tsconfig.lib.json +8 -0
  70. package/tsconfig.spec.json +13 -0
  71. package/tsup.config.mts +12 -0
  72. package/vitest.config.mts +9 -0
@@ -0,0 +1,965 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { z } from 'zod'
3
+
4
+ import type { CommandHandler } from '../interfaces/command-handler.interface.mjs'
5
+
6
+ import { CommanderFactory } from '../commander.factory.mjs'
7
+ import { CliModule } from '../decorators/cli-module.decorator.mjs'
8
+ import { Command } from '../decorators/command.decorator.mjs'
9
+
10
+ describe('CommanderApplication E2E - run() with different argv', () => {
11
+ describe('simple command without options', () => {
12
+ const executeMock = vi.fn()
13
+
14
+ @Command({ path: 'hello' })
15
+ class HelloCommand implements CommandHandler {
16
+ async execute() {
17
+ executeMock()
18
+ console.log('Hello, World!')
19
+ }
20
+ }
21
+
22
+ @CliModule({
23
+ commands: [HelloCommand],
24
+ })
25
+ class TestModule {}
26
+
27
+ beforeEach(() => {
28
+ executeMock.mockClear()
29
+ })
30
+
31
+ it('should execute command with minimal argv', async () => {
32
+ const app = await CommanderFactory.create(TestModule)
33
+ await app.init()
34
+
35
+ await app.run(['node', 'script.js', 'hello'])
36
+
37
+ expect(executeMock).toHaveBeenCalledTimes(1)
38
+ await app.close()
39
+ })
40
+
41
+ it('should execute command with absolute path in argv', async () => {
42
+ const app = await CommanderFactory.create(TestModule)
43
+ await app.init()
44
+
45
+ await app.run(['/usr/local/bin/node', '/path/to/script.js', 'hello'])
46
+
47
+ expect(executeMock).toHaveBeenCalledTimes(1)
48
+ await app.close()
49
+ })
50
+ })
51
+
52
+ describe('command with boolean flags', () => {
53
+ const executeMock = vi.fn()
54
+
55
+ const optionsSchema = z.object({
56
+ verbose: z.boolean().optional().default(false),
57
+ debug: z.boolean().optional().default(false),
58
+ quiet: z.boolean().optional().default(false),
59
+ })
60
+
61
+ @Command({ path: 'test', optionsSchema })
62
+ class TestCommand implements CommandHandler<z.infer<typeof optionsSchema>> {
63
+ async execute(options: z.infer<typeof optionsSchema>) {
64
+ executeMock(options)
65
+ }
66
+ }
67
+
68
+ @CliModule({
69
+ commands: [TestCommand],
70
+ })
71
+ class TestModule {}
72
+
73
+ beforeEach(() => {
74
+ executeMock.mockClear()
75
+ })
76
+
77
+ it('should parse single boolean flag', async () => {
78
+ const app = await CommanderFactory.create(TestModule)
79
+ await app.init()
80
+
81
+ await app.run(['node', 'script.js', 'test', '--verbose'])
82
+
83
+ expect(executeMock).toHaveBeenCalledWith({
84
+ verbose: true,
85
+ debug: false,
86
+ quiet: false,
87
+ })
88
+ await app.close()
89
+ })
90
+
91
+ it('should parse multiple boolean flags', async () => {
92
+ const app = await CommanderFactory.create(TestModule)
93
+ await app.init()
94
+
95
+ await app.run(['node', 'script.js', 'test', '--verbose', '--debug'])
96
+
97
+ expect(executeMock).toHaveBeenCalledWith({
98
+ verbose: true,
99
+ debug: true,
100
+ quiet: false,
101
+ })
102
+ await app.close()
103
+ })
104
+
105
+ it('should parse kebab-case flags to camelCase', async () => {
106
+ const optionsSchema = z.object({
107
+ dryRun: z.boolean().optional().default(false),
108
+ })
109
+
110
+ @Command({ path: 'deploy', optionsSchema })
111
+ class DeployCommand
112
+ implements CommandHandler<z.infer<typeof optionsSchema>>
113
+ {
114
+ async execute(options: z.infer<typeof optionsSchema>) {
115
+ executeMock(options)
116
+ }
117
+ }
118
+
119
+ @CliModule({
120
+ commands: [DeployCommand],
121
+ })
122
+ class DeployModule {}
123
+
124
+ const app = await CommanderFactory.create(DeployModule)
125
+ await app.init()
126
+
127
+ await app.run(['node', 'script.js', 'deploy', '--dry-run'])
128
+
129
+ expect(executeMock).toHaveBeenCalledWith({
130
+ dryRun: true,
131
+ })
132
+ await app.close()
133
+ })
134
+
135
+ it('should handle short flags', async () => {
136
+ const optionsSchema = z.object({
137
+ v: z.boolean().optional().default(false),
138
+ d: z.boolean().optional().default(false),
139
+ })
140
+
141
+ @Command({ path: 'build', optionsSchema })
142
+ class BuildCommand
143
+ implements CommandHandler<z.infer<typeof optionsSchema>>
144
+ {
145
+ async execute(options: z.infer<typeof optionsSchema>) {
146
+ executeMock(options)
147
+ }
148
+ }
149
+
150
+ @CliModule({
151
+ commands: [BuildCommand],
152
+ })
153
+ class BuildModule {}
154
+
155
+ const app = await CommanderFactory.create(BuildModule)
156
+ await app.init()
157
+
158
+ await app.run(['node', 'script.js', 'build', '-v', '-d'])
159
+
160
+ expect(executeMock).toHaveBeenCalledWith({
161
+ v: true,
162
+ d: true,
163
+ })
164
+ await app.close()
165
+ })
166
+ })
167
+
168
+ describe('command with string options', () => {
169
+ const executeMock = vi.fn()
170
+
171
+ const optionsSchema = z.object({
172
+ name: z.string(),
173
+ greeting: z.string().optional().default('Hello'),
174
+ message: z.string().optional(),
175
+ })
176
+
177
+ @Command({ path: 'greet', optionsSchema })
178
+ class GreetCommand
179
+ implements CommandHandler<z.infer<typeof optionsSchema>>
180
+ {
181
+ async execute(options: z.infer<typeof optionsSchema>) {
182
+ executeMock(options)
183
+ }
184
+ }
185
+
186
+ @CliModule({
187
+ commands: [GreetCommand],
188
+ })
189
+ class TestModule {}
190
+
191
+ beforeEach(() => {
192
+ executeMock.mockClear()
193
+ })
194
+
195
+ it('should parse required string option', async () => {
196
+ const app = await CommanderFactory.create(TestModule)
197
+ await app.init()
198
+
199
+ await app.run(['node', 'script.js', 'greet', '--name', 'Alice'])
200
+
201
+ expect(executeMock).toHaveBeenCalledWith({
202
+ name: 'Alice',
203
+ greeting: 'Hello',
204
+ message: undefined,
205
+ })
206
+ await app.close()
207
+ })
208
+
209
+ it('should parse multiple string options', async () => {
210
+ const app = await CommanderFactory.create(TestModule)
211
+ await app.init()
212
+
213
+ await app.run([
214
+ 'node',
215
+ 'script.js',
216
+ 'greet',
217
+ '--name',
218
+ 'Bob',
219
+ '--greeting',
220
+ 'Hi',
221
+ '--message',
222
+ 'How are you?',
223
+ ])
224
+
225
+ expect(executeMock).toHaveBeenCalledWith({
226
+ name: 'Bob',
227
+ greeting: 'Hi',
228
+ message: 'How are you?',
229
+ })
230
+ await app.close()
231
+ })
232
+
233
+ it('should parse options with equal sign syntax', async () => {
234
+ const app = await CommanderFactory.create(TestModule)
235
+ await app.init()
236
+
237
+ await app.run([
238
+ 'node',
239
+ 'script.js',
240
+ 'greet',
241
+ '--name=Charlie',
242
+ '--greeting=Hey',
243
+ ])
244
+
245
+ expect(executeMock).toHaveBeenCalledWith({
246
+ name: 'Charlie',
247
+ greeting: 'Hey',
248
+ message: undefined,
249
+ })
250
+ await app.close()
251
+ })
252
+
253
+ it('should handle string values with spaces', async () => {
254
+ const app = await CommanderFactory.create(TestModule)
255
+ await app.init()
256
+
257
+ await app.run([
258
+ 'node',
259
+ 'script.js',
260
+ 'greet',
261
+ '--name',
262
+ 'Alice Smith',
263
+ '--message',
264
+ 'Good morning everyone',
265
+ ])
266
+
267
+ expect(executeMock).toHaveBeenCalledWith({
268
+ name: 'Alice Smith',
269
+ greeting: 'Hello',
270
+ message: 'Good morning everyone',
271
+ })
272
+ await app.close()
273
+ })
274
+ })
275
+
276
+ describe('command with numeric options', () => {
277
+ const executeMock = vi.fn()
278
+
279
+ const optionsSchema = z.object({
280
+ port: z.number(),
281
+ timeout: z.number().optional().default(5000),
282
+ retries: z.number().optional(),
283
+ })
284
+
285
+ @Command({ path: 'serve', optionsSchema })
286
+ class ServeCommand
287
+ implements CommandHandler<z.infer<typeof optionsSchema>>
288
+ {
289
+ async execute(options: z.infer<typeof optionsSchema>) {
290
+ executeMock(options)
291
+ }
292
+ }
293
+
294
+ @CliModule({
295
+ commands: [ServeCommand],
296
+ })
297
+ class TestModule {}
298
+
299
+ beforeEach(() => {
300
+ executeMock.mockClear()
301
+ })
302
+
303
+ it('should parse integer values', async () => {
304
+ const app = await CommanderFactory.create(TestModule)
305
+ await app.init()
306
+
307
+ await app.run(['node', 'script.js', 'serve', '--port', '3000'])
308
+
309
+ expect(executeMock).toHaveBeenCalledWith({
310
+ port: 3000,
311
+ timeout: 5000,
312
+ retries: undefined,
313
+ })
314
+ await app.close()
315
+ })
316
+
317
+ it('should parse multiple numeric options', async () => {
318
+ const app = await CommanderFactory.create(TestModule)
319
+ await app.init()
320
+
321
+ await app.run([
322
+ 'node',
323
+ 'script.js',
324
+ 'serve',
325
+ '--port',
326
+ '8080',
327
+ '--timeout',
328
+ '10000',
329
+ '--retries',
330
+ '3',
331
+ ])
332
+
333
+ expect(executeMock).toHaveBeenCalledWith({
334
+ port: 8080,
335
+ timeout: 10000,
336
+ retries: 3,
337
+ })
338
+ await app.close()
339
+ })
340
+
341
+ it('should parse numeric values with equal sign', async () => {
342
+ const app = await CommanderFactory.create(TestModule)
343
+ await app.init()
344
+
345
+ await app.run(['node', 'script.js', 'serve', '--port=4000'])
346
+
347
+ expect(executeMock).toHaveBeenCalledWith({
348
+ port: 4000,
349
+ timeout: 5000,
350
+ retries: undefined,
351
+ })
352
+ await app.close()
353
+ })
354
+ })
355
+
356
+ describe('command with mixed option types', () => {
357
+ const executeMock = vi.fn()
358
+
359
+ const optionsSchema = z.object({
360
+ env: z.string(),
361
+ port: z.number().optional().default(3000),
362
+ verbose: z.boolean().optional().default(false),
363
+ workers: z.number().optional(),
364
+ config: z.string().optional(),
365
+ })
366
+
367
+ @Command({ path: 'start', optionsSchema })
368
+ class StartCommand
369
+ implements CommandHandler<z.infer<typeof optionsSchema>>
370
+ {
371
+ async execute(options: z.infer<typeof optionsSchema>) {
372
+ executeMock(options)
373
+ }
374
+ }
375
+
376
+ @CliModule({
377
+ commands: [StartCommand],
378
+ })
379
+ class TestModule {}
380
+
381
+ beforeEach(() => {
382
+ executeMock.mockClear()
383
+ })
384
+
385
+ it('should parse mixed types in correct order', async () => {
386
+ const app = await CommanderFactory.create(TestModule)
387
+ await app.init()
388
+
389
+ await app.run([
390
+ 'node',
391
+ 'script.js',
392
+ 'start',
393
+ '--env',
394
+ 'production',
395
+ '--port',
396
+ '8080',
397
+ '--verbose',
398
+ '--workers',
399
+ '4',
400
+ ])
401
+
402
+ expect(executeMock).toHaveBeenCalledWith({
403
+ env: 'production',
404
+ port: 8080,
405
+ verbose: true,
406
+ workers: 4,
407
+ config: undefined,
408
+ })
409
+ await app.close()
410
+ })
411
+
412
+ it('should parse mixed types in random order', async () => {
413
+ const app = await CommanderFactory.create(TestModule)
414
+ await app.init()
415
+
416
+ await app.run([
417
+ 'node',
418
+ 'script.js',
419
+ 'start',
420
+ '--verbose',
421
+ '--env',
422
+ 'staging',
423
+ '--workers',
424
+ '2',
425
+ '--config',
426
+ 'app.json',
427
+ '--port',
428
+ '5000',
429
+ ])
430
+
431
+ expect(executeMock).toHaveBeenCalledWith({
432
+ env: 'staging',
433
+ port: 5000,
434
+ verbose: true,
435
+ workers: 2,
436
+ config: 'app.json',
437
+ })
438
+ await app.close()
439
+ })
440
+ })
441
+
442
+ describe('multi-word commands', () => {
443
+ const executeMock = vi.fn()
444
+
445
+ @Command({ path: 'db migrate' })
446
+ class DbMigrateCommand implements CommandHandler {
447
+ async execute() {
448
+ executeMock('migrate')
449
+ }
450
+ }
451
+
452
+ @Command({ path: 'db seed' })
453
+ class DbSeedCommand implements CommandHandler {
454
+ async execute() {
455
+ executeMock('seed')
456
+ }
457
+ }
458
+
459
+ @Command({ path: 'db rollback' })
460
+ class DbRollbackCommand implements CommandHandler {
461
+ async execute() {
462
+ executeMock('rollback')
463
+ }
464
+ }
465
+
466
+ @CliModule({
467
+ commands: [DbMigrateCommand, DbSeedCommand, DbRollbackCommand],
468
+ })
469
+ class TestModule {}
470
+
471
+ beforeEach(() => {
472
+ executeMock.mockClear()
473
+ })
474
+
475
+ it('should execute multi-word command', async () => {
476
+ const app = await CommanderFactory.create(TestModule)
477
+ await app.init()
478
+
479
+ await app.run(['node', 'script.js', 'db', 'migrate'])
480
+
481
+ expect(executeMock).toHaveBeenCalledWith('migrate')
482
+ await app.close()
483
+ })
484
+
485
+ it('should execute different multi-word commands', async () => {
486
+ const app = await CommanderFactory.create(TestModule)
487
+ await app.init()
488
+
489
+ await app.run(['node', 'script.js', 'db', 'seed'])
490
+ expect(executeMock).toHaveBeenCalledWith('seed')
491
+
492
+ executeMock.mockClear()
493
+
494
+ await app.run(['node', 'script.js', 'db', 'rollback'])
495
+ expect(executeMock).toHaveBeenCalledWith('rollback')
496
+
497
+ await app.close()
498
+ })
499
+
500
+ it('should execute multi-word command with options', async () => {
501
+ const optionsSchema = z.object({
502
+ steps: z.number().optional(),
503
+ })
504
+
505
+ @Command({ path: 'db rollback', optionsSchema })
506
+ class DbRollbackWithOptionsCommand
507
+ implements CommandHandler<z.infer<typeof optionsSchema>>
508
+ {
509
+ async execute(options: z.infer<typeof optionsSchema>) {
510
+ executeMock(options)
511
+ }
512
+ }
513
+
514
+ @CliModule({
515
+ commands: [DbRollbackWithOptionsCommand],
516
+ })
517
+ class RollbackModule {}
518
+
519
+ const app = await CommanderFactory.create(RollbackModule)
520
+ await app.init()
521
+
522
+ await app.run(['node', 'script.js', 'db', 'rollback', '--steps', '3'])
523
+
524
+ expect(executeMock).toHaveBeenCalledWith({ steps: 3 })
525
+ await app.close()
526
+ })
527
+ })
528
+
529
+ describe('error handling', () => {
530
+ @Command({ path: 'valid' })
531
+ class ValidCommand implements CommandHandler {
532
+ async execute() {
533
+ console.log('Valid command executed')
534
+ }
535
+ }
536
+
537
+ @CliModule({
538
+ commands: [ValidCommand],
539
+ })
540
+ class TestModule {}
541
+
542
+ it('should throw error when command not found', async () => {
543
+ const app = await CommanderFactory.create(TestModule)
544
+ await app.init()
545
+
546
+ await expect(app.run(['node', 'script.js', 'invalid'])).rejects.toThrow(
547
+ '[Navios Commander] Command not found: invalid',
548
+ )
549
+
550
+ await app.close()
551
+ })
552
+
553
+ it('should throw error when no command provided', async () => {
554
+ const app = await CommanderFactory.create(TestModule)
555
+ await app.init()
556
+
557
+ await expect(app.run(['node', 'script.js'])).rejects.toThrow(
558
+ '[Navios Commander] No command provided',
559
+ )
560
+
561
+ await app.close()
562
+ })
563
+ })
564
+
565
+ describe('command with positional arguments', () => {
566
+ const executeMock = vi.fn()
567
+
568
+ const optionsSchema = z.object({
569
+ force: z.boolean().optional().default(false),
570
+ })
571
+
572
+ @Command({ path: 'copy', optionsSchema })
573
+ class CopyCommand implements CommandHandler<z.infer<typeof optionsSchema>> {
574
+ async execute(options: z.infer<typeof optionsSchema>) {
575
+ executeMock(options)
576
+ }
577
+ }
578
+
579
+ @CliModule({
580
+ commands: [CopyCommand],
581
+ })
582
+ class TestModule {}
583
+
584
+ beforeEach(() => {
585
+ executeMock.mockClear()
586
+ })
587
+
588
+ it('should parse command with options only', async () => {
589
+ const app = await CommanderFactory.create(TestModule)
590
+ await app.init()
591
+
592
+ await app.run(['node', 'script.js', 'copy', '--force'])
593
+
594
+ expect(executeMock).toHaveBeenCalledWith({
595
+ force: true,
596
+ })
597
+ await app.close()
598
+ })
599
+
600
+ it('should parse command with mixed options and positionals', async () => {
601
+ const app = await CommanderFactory.create(TestModule)
602
+ await app.init()
603
+
604
+ // Note: positionals are not extracted in this test, but the command should still execute
605
+ await app.run(['node', 'script.js', 'copy', '--force'])
606
+
607
+ expect(executeMock).toHaveBeenCalledWith({
608
+ force: true,
609
+ })
610
+ await app.close()
611
+ })
612
+ })
613
+
614
+ describe('complex real-world scenarios', () => {
615
+ const executeMock = vi.fn()
616
+
617
+ const deploySchema = z.object({
618
+ env: z.string(),
619
+ branch: z.string().optional().default('main'),
620
+ verbose: z.boolean().optional().default(false),
621
+ dryRun: z.boolean().optional().default(false),
622
+ timeout: z.number().optional().default(300),
623
+ workers: z.number().optional(),
624
+ skipTests: z.boolean().optional().default(false),
625
+ buildArgs: z.string().optional(),
626
+ })
627
+
628
+ @Command({ path: 'app deploy', optionsSchema: deploySchema })
629
+ class DeployCommand
630
+ implements CommandHandler<z.infer<typeof deploySchema>>
631
+ {
632
+ async execute(options: z.infer<typeof deploySchema>) {
633
+ executeMock(options)
634
+ }
635
+ }
636
+
637
+ @CliModule({
638
+ commands: [DeployCommand],
639
+ })
640
+ class TestModule {}
641
+
642
+ beforeEach(() => {
643
+ executeMock.mockClear()
644
+ })
645
+
646
+ it('should handle production deployment scenario', async () => {
647
+ const app = await CommanderFactory.create(TestModule)
648
+ await app.init()
649
+
650
+ await app.run([
651
+ 'node',
652
+ 'script.js',
653
+ 'app',
654
+ 'deploy',
655
+ '--env',
656
+ 'production',
657
+ '--branch',
658
+ 'release/v1.2.0',
659
+ '--verbose',
660
+ '--timeout',
661
+ '600',
662
+ '--workers',
663
+ '8',
664
+ ])
665
+
666
+ expect(executeMock).toHaveBeenCalledWith({
667
+ env: 'production',
668
+ branch: 'release/v1.2.0',
669
+ verbose: true,
670
+ dryRun: false,
671
+ timeout: 600,
672
+ workers: 8,
673
+ skipTests: false,
674
+ buildArgs: undefined,
675
+ })
676
+ await app.close()
677
+ })
678
+
679
+ it('should handle staging dry-run deployment', async () => {
680
+ const app = await CommanderFactory.create(TestModule)
681
+ await app.init()
682
+
683
+ await app.run([
684
+ 'node',
685
+ 'script.js',
686
+ 'app',
687
+ 'deploy',
688
+ '--env',
689
+ 'staging',
690
+ '--dry-run',
691
+ '--skip-tests',
692
+ ])
693
+
694
+ expect(executeMock).toHaveBeenCalledWith({
695
+ env: 'staging',
696
+ branch: 'main',
697
+ verbose: false,
698
+ dryRun: true,
699
+ timeout: 300,
700
+ workers: undefined,
701
+ skipTests: true,
702
+ buildArgs: undefined,
703
+ })
704
+ await app.close()
705
+ })
706
+
707
+ it('should handle deployment with build arguments', async () => {
708
+ const app = await CommanderFactory.create(TestModule)
709
+ await app.init()
710
+
711
+ await app.run([
712
+ 'node',
713
+ 'script.js',
714
+ 'app',
715
+ 'deploy',
716
+ '--env=development',
717
+ '--build-args',
718
+ 'NODE_ENV=development API_KEY=test',
719
+ '--verbose',
720
+ ])
721
+
722
+ expect(executeMock).toHaveBeenCalledWith({
723
+ env: 'development',
724
+ branch: 'main',
725
+ verbose: true,
726
+ dryRun: false,
727
+ timeout: 300,
728
+ workers: undefined,
729
+ skipTests: false,
730
+ buildArgs: 'NODE_ENV=development API_KEY=test',
731
+ })
732
+ await app.close()
733
+ })
734
+ })
735
+
736
+ describe('edge cases and special characters', () => {
737
+ const executeMock = vi.fn()
738
+
739
+ const optionsSchema = z.object({
740
+ path: z.string().optional(),
741
+ url: z.string().optional(),
742
+ data: z.string().optional(),
743
+ })
744
+
745
+ @Command({ path: 'process', optionsSchema })
746
+ class ProcessCommand
747
+ implements CommandHandler<z.infer<typeof optionsSchema>>
748
+ {
749
+ async execute(options: z.infer<typeof optionsSchema>) {
750
+ executeMock(options)
751
+ }
752
+ }
753
+
754
+ @CliModule({
755
+ commands: [ProcessCommand],
756
+ })
757
+ class TestModule {}
758
+
759
+ beforeEach(() => {
760
+ executeMock.mockClear()
761
+ })
762
+
763
+ it('should handle paths with slashes', async () => {
764
+ const app = await CommanderFactory.create(TestModule)
765
+ await app.init()
766
+
767
+ await app.run([
768
+ 'node',
769
+ 'script.js',
770
+ 'process',
771
+ '--path',
772
+ '/usr/local/bin',
773
+ ])
774
+
775
+ expect(executeMock).toHaveBeenCalledWith({
776
+ path: '/usr/local/bin',
777
+ url: undefined,
778
+ data: undefined,
779
+ })
780
+ await app.close()
781
+ })
782
+
783
+ it('should handle URLs', async () => {
784
+ const app = await CommanderFactory.create(TestModule)
785
+ await app.init()
786
+
787
+ await app.run([
788
+ 'node',
789
+ 'script.js',
790
+ 'process',
791
+ '--url',
792
+ 'https://api.example.com/v1/users',
793
+ ])
794
+
795
+ expect(executeMock).toHaveBeenCalledWith({
796
+ path: undefined,
797
+ url: 'https://api.example.com/v1/users',
798
+ data: undefined,
799
+ })
800
+ await app.close()
801
+ })
802
+
803
+ it('should handle JSON-like strings (auto-parsed)', async () => {
804
+ const app = await CommanderFactory.create(TestModule)
805
+ await app.init()
806
+
807
+ // Note: The CLI parser automatically parses valid JSON strings
808
+ // If you need to pass raw JSON as a string, wrap it in quotes
809
+ const jsonSchema = z.object({
810
+ data: z.record(z.string(), z.any()).optional(),
811
+ })
812
+
813
+ @Command({ path: 'json-process', optionsSchema: jsonSchema })
814
+ class JsonProcessCommand
815
+ implements CommandHandler<z.infer<typeof jsonSchema>>
816
+ {
817
+ async execute(options: z.infer<typeof jsonSchema>) {
818
+ executeMock(options)
819
+ }
820
+ }
821
+
822
+ @CliModule({
823
+ commands: [JsonProcessCommand],
824
+ })
825
+ class JsonModule {}
826
+
827
+ const jsonApp = await CommanderFactory.create(JsonModule)
828
+ await jsonApp.init()
829
+
830
+ await jsonApp.run([
831
+ 'node',
832
+ 'script.js',
833
+ 'json-process',
834
+ '--data',
835
+ '{"key":"value","count":42}',
836
+ ])
837
+
838
+ expect(executeMock).toHaveBeenCalledWith({
839
+ data: { key: 'value', count: 42 },
840
+ })
841
+ await jsonApp.close()
842
+ })
843
+
844
+ it('should handle values with dashes', async () => {
845
+ const app = await CommanderFactory.create(TestModule)
846
+ await app.init()
847
+
848
+ await app.run([
849
+ 'node',
850
+ 'script.js',
851
+ 'process',
852
+ '--path',
853
+ 'my-app-folder',
854
+ '--data',
855
+ 'user-name-123',
856
+ ])
857
+
858
+ expect(executeMock).toHaveBeenCalledWith({
859
+ path: 'my-app-folder',
860
+ url: undefined,
861
+ data: 'user-name-123',
862
+ })
863
+ await app.close()
864
+ })
865
+ })
866
+
867
+ describe('default values', () => {
868
+ const executeMock = vi.fn()
869
+
870
+ const optionsSchema = z.object({
871
+ name: z.string(),
872
+ greeting: z.string().default('Hello'),
873
+ count: z.number().default(1),
874
+ verbose: z.boolean().default(false),
875
+ optional: z.string().optional(),
876
+ })
877
+
878
+ @Command({ path: 'welcome', optionsSchema })
879
+ class WelcomeCommand
880
+ implements CommandHandler<z.infer<typeof optionsSchema>>
881
+ {
882
+ async execute(options: z.infer<typeof optionsSchema>) {
883
+ executeMock(options)
884
+ }
885
+ }
886
+
887
+ @CliModule({
888
+ commands: [WelcomeCommand],
889
+ })
890
+ class TestModule {}
891
+
892
+ beforeEach(() => {
893
+ executeMock.mockClear()
894
+ })
895
+
896
+ it('should use default values when options not provided', async () => {
897
+ const app = await CommanderFactory.create(TestModule)
898
+ await app.init()
899
+
900
+ await app.run(['node', 'script.js', 'welcome', '--name', 'Alice'])
901
+
902
+ expect(executeMock).toHaveBeenCalledWith({
903
+ name: 'Alice',
904
+ greeting: 'Hello',
905
+ count: 1,
906
+ verbose: false,
907
+ optional: undefined,
908
+ })
909
+ await app.close()
910
+ })
911
+
912
+ it('should override default values when provided', async () => {
913
+ const app = await CommanderFactory.create(TestModule)
914
+ await app.init()
915
+
916
+ await app.run([
917
+ 'node',
918
+ 'script.js',
919
+ 'welcome',
920
+ '--name',
921
+ 'Bob',
922
+ '--greeting',
923
+ 'Hi',
924
+ '--count',
925
+ '5',
926
+ '--verbose',
927
+ '--optional',
928
+ 'extra',
929
+ ])
930
+
931
+ expect(executeMock).toHaveBeenCalledWith({
932
+ name: 'Bob',
933
+ greeting: 'Hi',
934
+ count: 5,
935
+ verbose: true,
936
+ optional: 'extra',
937
+ })
938
+ await app.close()
939
+ })
940
+
941
+ it('should mix defaults and provided values', async () => {
942
+ const app = await CommanderFactory.create(TestModule)
943
+ await app.init()
944
+
945
+ await app.run([
946
+ 'node',
947
+ 'script.js',
948
+ 'welcome',
949
+ '--name',
950
+ 'Charlie',
951
+ '--count',
952
+ '3',
953
+ ])
954
+
955
+ expect(executeMock).toHaveBeenCalledWith({
956
+ name: 'Charlie',
957
+ greeting: 'Hello',
958
+ count: 3,
959
+ verbose: false,
960
+ optional: undefined,
961
+ })
962
+ await app.close()
963
+ })
964
+ })
965
+ })