@navios/commander 0.5.0 → 0.5.2

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 (33) hide show
  1. package/README.md +37 -0
  2. package/dist/src/commander.application.d.mts.map +1 -1
  3. package/dist/src/index.d.mts +1 -0
  4. package/dist/src/index.d.mts.map +1 -1
  5. package/dist/src/interfaces/commander-execution-context.interface.d.mts +11 -0
  6. package/dist/src/interfaces/commander-execution-context.interface.d.mts.map +1 -0
  7. package/dist/src/interfaces/index.d.mts +1 -0
  8. package/dist/src/interfaces/index.d.mts.map +1 -1
  9. package/dist/src/services/cli-parser.service.d.mts +10 -0
  10. package/dist/src/services/cli-parser.service.d.mts.map +1 -1
  11. package/dist/src/tokens/execution-context.token.d.mts +5 -0
  12. package/dist/src/tokens/execution-context.token.d.mts.map +1 -0
  13. package/dist/src/tokens/index.d.mts +2 -0
  14. package/dist/src/tokens/index.d.mts.map +1 -0
  15. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  16. package/dist/tsconfig.tsbuildinfo +1 -1
  17. package/lib/_tsup-dts-rollup.d.mts +33 -0
  18. package/lib/_tsup-dts-rollup.d.ts +33 -0
  19. package/lib/index.d.mts +3 -0
  20. package/lib/index.d.ts +3 -0
  21. package/lib/index.js +108 -7
  22. package/lib/index.js.map +1 -1
  23. package/lib/index.mjs +107 -9
  24. package/lib/index.mjs.map +1 -1
  25. package/package.json +1 -1
  26. package/src/commander.application.mts +32 -8
  27. package/src/index.mts +1 -0
  28. package/src/interfaces/commander-execution-context.interface.mts +21 -0
  29. package/src/interfaces/index.mts +1 -0
  30. package/src/services/__tests__/cli-parser.service.spec.mts +251 -0
  31. package/src/services/cli-parser.service.mts +81 -2
  32. package/src/tokens/execution-context.token.mts +10 -0
  33. package/src/tokens/index.mts +1 -0
@@ -401,4 +401,255 @@ describe('CliParserService', () => {
401
401
  expect(result.positionals).toEqual(['-'])
402
402
  })
403
403
  })
404
+
405
+ describe('array option parsing', () => {
406
+ it('should accumulate multiple values for array options', () => {
407
+ const schema = z.object({
408
+ tags: z.array(z.string()),
409
+ })
410
+
411
+ const result = parser.parse(
412
+ [
413
+ 'node',
414
+ 'script.js',
415
+ 'test',
416
+ '--tags',
417
+ 'foo',
418
+ '--tags',
419
+ 'bar',
420
+ '--tags',
421
+ 'baz',
422
+ ],
423
+ schema,
424
+ )
425
+
426
+ expect(result.options).toEqual({
427
+ tags: ['foo', 'bar', 'baz'],
428
+ })
429
+ })
430
+
431
+ it('should handle array options with equal sign syntax', () => {
432
+ const schema = z.object({
433
+ tags: z.array(z.string()),
434
+ })
435
+
436
+ const result = parser.parse(
437
+ [
438
+ 'node',
439
+ 'script.js',
440
+ 'test',
441
+ '--tags=foo',
442
+ '--tags=bar',
443
+ '--tags=baz',
444
+ ],
445
+ schema,
446
+ )
447
+
448
+ expect(result.options).toEqual({
449
+ tags: ['foo', 'bar', 'baz'],
450
+ })
451
+ })
452
+
453
+ it('should handle array options with kebab-case', () => {
454
+ const schema = z.object({
455
+ excludePaths: z.array(z.string()),
456
+ })
457
+
458
+ const result = parser.parse(
459
+ [
460
+ 'node',
461
+ 'script.js',
462
+ 'test',
463
+ '--exclude-paths',
464
+ 'node_modules',
465
+ '--exclude-paths',
466
+ 'dist',
467
+ ],
468
+ schema,
469
+ )
470
+
471
+ expect(result.options).toEqual({
472
+ excludePaths: ['node_modules', 'dist'],
473
+ })
474
+ })
475
+
476
+ it('should handle optional array options', () => {
477
+ const schema = z.object({
478
+ tags: z.array(z.string()).optional(),
479
+ verbose: z.boolean(),
480
+ })
481
+
482
+ const result = parser.parse(
483
+ [
484
+ 'node',
485
+ 'script.js',
486
+ 'test',
487
+ '--tags',
488
+ 'foo',
489
+ '--tags',
490
+ 'bar',
491
+ '--verbose',
492
+ ],
493
+ schema,
494
+ )
495
+
496
+ expect(result.options).toEqual({
497
+ tags: ['foo', 'bar'],
498
+ verbose: true,
499
+ })
500
+ })
501
+
502
+ it('should handle array options with default values', () => {
503
+ const schema = z.object({
504
+ tags: z.array(z.string()).default([]),
505
+ })
506
+
507
+ const result = parser.parse(
508
+ ['node', 'script.js', 'test', '--tags', 'foo', '--tags', 'bar'],
509
+ schema,
510
+ )
511
+
512
+ expect(result.options).toEqual({
513
+ tags: ['foo', 'bar'],
514
+ })
515
+ })
516
+
517
+ it('should handle mixed array, boolean, and string options', () => {
518
+ const schema = z.object({
519
+ tags: z.array(z.string()),
520
+ verbose: z.boolean(),
521
+ config: z.string(),
522
+ ports: z.array(z.number()),
523
+ })
524
+
525
+ const result = parser.parse(
526
+ [
527
+ 'node',
528
+ 'script.js',
529
+ 'test',
530
+ '--tags',
531
+ 'foo',
532
+ '--verbose',
533
+ '--config',
534
+ 'prod',
535
+ '--tags',
536
+ 'bar',
537
+ '--ports',
538
+ '3000',
539
+ '--ports',
540
+ '4000',
541
+ ],
542
+ schema,
543
+ )
544
+
545
+ expect(result.options).toEqual({
546
+ tags: ['foo', 'bar'],
547
+ verbose: true,
548
+ config: 'prod',
549
+ ports: [3000, 4000],
550
+ })
551
+ })
552
+
553
+ it('should handle short array options', () => {
554
+ const schema = z.object({
555
+ t: z.array(z.string()),
556
+ })
557
+
558
+ const result = parser.parse(
559
+ ['node', 'script.js', 'test', '-t', 'foo', '-t', 'bar'],
560
+ schema,
561
+ )
562
+
563
+ expect(result.options).toEqual({
564
+ t: ['foo', 'bar'],
565
+ })
566
+ })
567
+
568
+ it('should parse array option values with correct types', () => {
569
+ const schema = z.object({
570
+ ports: z.array(z.number()),
571
+ flags: z.array(z.boolean()),
572
+ })
573
+
574
+ const result = parser.parse(
575
+ [
576
+ 'node',
577
+ 'script.js',
578
+ 'test',
579
+ '--ports',
580
+ '3000',
581
+ '--ports',
582
+ '4000',
583
+ '--flags',
584
+ 'true',
585
+ '--flags',
586
+ 'false',
587
+ ],
588
+ schema,
589
+ )
590
+
591
+ expect(result.options).toEqual({
592
+ ports: [3000, 4000],
593
+ flags: [true, false],
594
+ })
595
+ })
596
+
597
+ it('should handle JSON array values for array options', () => {
598
+ const schema = z.object({
599
+ items: z.array(z.any()),
600
+ })
601
+
602
+ const result = parser.parse(
603
+ ['node', 'script.js', 'test', '--items', '[1,2,3]', '--items', '["a","b"]'],
604
+ schema,
605
+ )
606
+
607
+ expect(result.options).toEqual({
608
+ items: [[1, 2, 3], ['a', 'b']],
609
+ })
610
+ })
611
+
612
+ it('should handle array options with positionals', () => {
613
+ const schema = z.object({
614
+ tags: z.array(z.string()),
615
+ })
616
+
617
+ const result = parser.parse(
618
+ [
619
+ 'node',
620
+ 'script.js',
621
+ 'test',
622
+ '--tags',
623
+ 'foo',
624
+ 'file1.txt',
625
+ '--tags',
626
+ 'bar',
627
+ 'file2.txt',
628
+ ],
629
+ schema,
630
+ )
631
+
632
+ expect(result.options).toEqual({
633
+ tags: ['foo', 'bar'],
634
+ })
635
+ expect(result.positionals).toEqual(['file1.txt', 'file2.txt'])
636
+ })
637
+
638
+ it('should not treat non-array options as arrays without schema', () => {
639
+ // Without schema, multiple values should overwrite
640
+ const result = parser.parse([
641
+ 'node',
642
+ 'script.js',
643
+ 'test',
644
+ '--tag',
645
+ 'foo',
646
+ '--tag',
647
+ 'bar',
648
+ ])
649
+
650
+ expect(result.options).toEqual({
651
+ tag: 'bar', // Last value wins without schema
652
+ })
653
+ })
654
+ })
404
655
  })
@@ -27,10 +27,13 @@ export class CliParserService {
27
27
  throw new Error('[Navios Commander] No command provided')
28
28
  }
29
29
 
30
- // Extract boolean field names from schema for accurate parsing
30
+ // Extract boolean and array field names from schema for accurate parsing
31
31
  const booleanFields = optionsSchema
32
32
  ? this.extractBooleanFields(optionsSchema)
33
33
  : new Set<string>()
34
+ const arrayFields = optionsSchema
35
+ ? this.extractArrayFields(optionsSchema)
36
+ : new Set<string>()
34
37
 
35
38
  // Collect command words until we hit an argument that starts with '-' or '--'
36
39
  const commandParts: string[] = []
@@ -59,19 +62,38 @@ export class CliParserService {
59
62
  // Format: --key=value
60
63
  const optionName = key.slice(0, equalIndex)
61
64
  const optionValue = key.slice(equalIndex + 1)
62
- options[this.camelCase(optionName)] = this.parseValue(optionValue)
65
+ const camelCaseKey = this.camelCase(optionName)
66
+ const isArray = arrayFields.has(camelCaseKey) || arrayFields.has(optionName)
67
+
68
+ if (isArray) {
69
+ // For array fields, accumulate values
70
+ if (!options[camelCaseKey]) {
71
+ options[camelCaseKey] = []
72
+ }
73
+ options[camelCaseKey].push(this.parseValue(optionValue))
74
+ } else {
75
+ options[camelCaseKey] = this.parseValue(optionValue)
76
+ }
63
77
  i++
64
78
  } else {
65
79
  // Format: --key value or --boolean-flag
66
80
  const camelCaseKey = this.camelCase(key)
67
81
  const isBoolean =
68
82
  booleanFields.has(camelCaseKey) || booleanFields.has(key)
83
+ const isArray = arrayFields.has(camelCaseKey) || arrayFields.has(key)
69
84
  const nextArg = args[i + 1]
70
85
 
71
86
  if (isBoolean) {
72
87
  // Known boolean flag from schema
73
88
  options[camelCaseKey] = true
74
89
  i++
90
+ } else if (isArray && nextArg && !nextArg.startsWith('-')) {
91
+ // Known array field from schema - accumulate values
92
+ if (!options[camelCaseKey]) {
93
+ options[camelCaseKey] = []
94
+ }
95
+ options[camelCaseKey].push(this.parseValue(nextArg))
96
+ i += 2
75
97
  } else if (nextArg && !nextArg.startsWith('-')) {
76
98
  // Has a value
77
99
  options[camelCaseKey] = this.parseValue(nextArg)
@@ -89,12 +111,20 @@ export class CliParserService {
89
111
  if (flags.length === 1) {
90
112
  // Single short flag: -k value or -k
91
113
  const isBoolean = booleanFields.has(flags)
114
+ const isArray = arrayFields.has(flags)
92
115
  const nextArg = args[i + 1]
93
116
 
94
117
  if (isBoolean) {
95
118
  // Known boolean flag from schema
96
119
  options[flags] = true
97
120
  i++
121
+ } else if (isArray && nextArg && !nextArg.startsWith('-')) {
122
+ // Known array field from schema - accumulate values
123
+ if (!options[flags]) {
124
+ options[flags] = []
125
+ }
126
+ options[flags].push(this.parseValue(nextArg))
127
+ i += 2
98
128
  } else if (nextArg && !nextArg.startsWith('-')) {
99
129
  options[flags] = this.parseValue(nextArg)
100
130
  i += 2
@@ -197,6 +227,34 @@ export class CliParserService {
197
227
  return booleanFields
198
228
  }
199
229
 
230
+ /**
231
+ * Extracts array field names from a Zod schema
232
+ * Handles ZodObject, ZodOptional, and ZodDefault wrappers
233
+ */
234
+ private extractArrayFields(schema: ZodObject): Set<string> {
235
+ const arrayFields = new Set<string>()
236
+
237
+ try {
238
+ const typeName = schema.def.type
239
+
240
+ if (typeName === 'object') {
241
+ const shape = schema.def.shape
242
+
243
+ if (shape && typeof shape === 'object') {
244
+ for (const [key, fieldSchema] of Object.entries(shape)) {
245
+ if (this.isSchemaArray(fieldSchema as any)) {
246
+ arrayFields.add(key)
247
+ }
248
+ }
249
+ }
250
+ }
251
+ } catch {
252
+ // Silently fail if schema introspection fails
253
+ }
254
+
255
+ return arrayFields
256
+ }
257
+
200
258
  /**
201
259
  * Checks if a Zod schema represents a boolean type
202
260
  * Unwraps ZodOptional and ZodDefault
@@ -218,6 +276,27 @@ export class CliParserService {
218
276
  }
219
277
  }
220
278
 
279
+ /**
280
+ * Checks if a Zod schema represents an array type
281
+ * Unwraps ZodOptional and ZodDefault
282
+ */
283
+ private isSchemaArray(schema: ZodType): boolean {
284
+ try {
285
+ let currentSchema = schema
286
+ const typeName = currentSchema.def.type
287
+
288
+ // Unwrap ZodOptional and ZodDefault
289
+ if (typeName === 'optional' || typeName === 'default') {
290
+ currentSchema = (currentSchema as any)?._def?.innerType || currentSchema
291
+ }
292
+
293
+ const innerTypeName = currentSchema.def.type
294
+ return innerTypeName === 'array'
295
+ } catch {
296
+ return false
297
+ }
298
+ }
299
+
221
300
  /**
222
301
  * Formats help text for available commands
223
302
  */
@@ -0,0 +1,10 @@
1
+ import { InjectionToken } from '@navios/di'
2
+
3
+ import type { CommanderExecutionContext } from '../interfaces/index.mjs'
4
+
5
+ export const ExecutionContextInjectionToken =
6
+ 'CommanderExecutionContextInjectionToken'
7
+
8
+ export const ExecutionContext = InjectionToken.create<CommanderExecutionContext>(
9
+ ExecutionContextInjectionToken,
10
+ )
@@ -0,0 +1 @@
1
+ export * from './execution-context.token.mjs'