@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.
- package/README.md +37 -0
- package/dist/src/commander.application.d.mts.map +1 -1
- package/dist/src/index.d.mts +1 -0
- package/dist/src/index.d.mts.map +1 -1
- package/dist/src/interfaces/commander-execution-context.interface.d.mts +11 -0
- package/dist/src/interfaces/commander-execution-context.interface.d.mts.map +1 -0
- package/dist/src/interfaces/index.d.mts +1 -0
- package/dist/src/interfaces/index.d.mts.map +1 -1
- package/dist/src/services/cli-parser.service.d.mts +10 -0
- package/dist/src/services/cli-parser.service.d.mts.map +1 -1
- package/dist/src/tokens/execution-context.token.d.mts +5 -0
- package/dist/src/tokens/execution-context.token.d.mts.map +1 -0
- package/dist/src/tokens/index.d.mts +2 -0
- package/dist/src/tokens/index.d.mts.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/_tsup-dts-rollup.d.mts +33 -0
- package/lib/_tsup-dts-rollup.d.ts +33 -0
- package/lib/index.d.mts +3 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +108 -7
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +107 -9
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/commander.application.mts +32 -8
- package/src/index.mts +1 -0
- package/src/interfaces/commander-execution-context.interface.mts +21 -0
- package/src/interfaces/index.mts +1 -0
- package/src/services/__tests__/cli-parser.service.spec.mts +251 -0
- package/src/services/cli-parser.service.mts +81 -2
- package/src/tokens/execution-context.token.mts +10 -0
- 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
|
-
|
|
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'
|