@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.
- package/LICENSE +7 -0
- package/README.md +242 -0
- package/dist/src/commander.application.d.mts +29 -0
- package/dist/src/commander.application.d.mts.map +1 -0
- package/dist/src/commander.factory.d.mts +8 -0
- package/dist/src/commander.factory.d.mts.map +1 -0
- package/dist/src/decorators/cli-module.decorator.d.mts +7 -0
- package/dist/src/decorators/cli-module.decorator.d.mts.map +1 -0
- package/dist/src/decorators/command.decorator.d.mts +8 -0
- package/dist/src/decorators/command.decorator.d.mts.map +1 -0
- package/dist/src/decorators/index.d.mts +3 -0
- package/dist/src/decorators/index.d.mts.map +1 -0
- package/dist/src/index.d.mts +8 -0
- package/dist/src/index.d.mts.map +1 -0
- package/dist/src/interfaces/cli-module.interface.d.mts +5 -0
- package/dist/src/interfaces/cli-module.interface.d.mts.map +1 -0
- package/dist/src/interfaces/command-handler.interface.d.mts +4 -0
- package/dist/src/interfaces/command-handler.interface.d.mts.map +1 -0
- package/dist/src/interfaces/index.d.mts +3 -0
- package/dist/src/interfaces/index.d.mts.map +1 -0
- package/dist/src/interfaces/module.interface.d.mts +5 -0
- package/dist/src/interfaces/module.interface.d.mts.map +1 -0
- package/dist/src/metadata/cli-module.metadata.d.mts +11 -0
- package/dist/src/metadata/cli-module.metadata.d.mts.map +1 -0
- package/dist/src/metadata/command.metadata.d.mts +12 -0
- package/dist/src/metadata/command.metadata.d.mts.map +1 -0
- package/dist/src/metadata/index.d.mts +3 -0
- package/dist/src/metadata/index.d.mts.map +1 -0
- package/dist/src/services/cli-parser.service.d.mts +44 -0
- package/dist/src/services/cli-parser.service.d.mts.map +1 -0
- package/dist/src/services/index.d.mts +3 -0
- package/dist/src/services/index.d.mts.map +1 -0
- package/dist/src/services/module-loader.service.d.mts +33 -0
- package/dist/src/services/module-loader.service.d.mts.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/tsup.config.d.mts +3 -0
- package/dist/tsup.config.d.mts.map +1 -0
- package/dist/vitest.config.d.mts +3 -0
- package/dist/vitest.config.d.mts.map +1 -0
- package/lib/_tsup-dts-rollup.d.mts +456 -0
- package/lib/_tsup-dts-rollup.d.ts +456 -0
- package/lib/index.d.mts +98 -0
- package/lib/index.d.ts +98 -0
- package/lib/index.js +541 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +524 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +40 -0
- package/project.json +66 -0
- package/src/__tests__/commander.factory.e2e.spec.mts +965 -0
- package/src/commander.application.mts +159 -0
- package/src/commander.factory.mts +20 -0
- package/src/decorators/cli-module.decorator.mts +39 -0
- package/src/decorators/command.decorator.mts +29 -0
- package/src/decorators/index.mts +2 -0
- package/src/index.mts +7 -0
- package/src/interfaces/command-handler.interface.mts +3 -0
- package/src/interfaces/index.mts +2 -0
- package/src/interfaces/module.interface.mts +4 -0
- package/src/metadata/cli-module.metadata.mts +54 -0
- package/src/metadata/command.metadata.mts +54 -0
- package/src/metadata/index.mts +2 -0
- package/src/services/__tests__/cli-parser.service.spec.mts +404 -0
- package/src/services/cli-parser.service.mts +231 -0
- package/src/services/index.mts +2 -0
- package/src/services/module-loader.service.mts +120 -0
- package/tsconfig.json +18 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +13 -0
- package/tsup.config.mts +12 -0
- package/vitest.config.mts +9 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { Container } from '@navios/di'
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
import { CliParserService } from '../cli-parser.service.mjs'
|
|
7
|
+
|
|
8
|
+
describe('CliParserService', () => {
|
|
9
|
+
let parser: CliParserService
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
const container = new Container()
|
|
12
|
+
parser = await container.get(CliParserService)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('basic parsing', () => {
|
|
16
|
+
it('should parse simple command', () => {
|
|
17
|
+
const result = parser.parse(['node', 'script.js', 'test'])
|
|
18
|
+
expect(result.command).toBe('test')
|
|
19
|
+
expect(result.options).toEqual({})
|
|
20
|
+
expect(result.positionals).toEqual([])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should parse multi-word command', () => {
|
|
24
|
+
const result = parser.parse(['node', 'script.js', 'db', 'migrate'])
|
|
25
|
+
expect(result.command).toBe('db migrate')
|
|
26
|
+
expect(result.options).toEqual({})
|
|
27
|
+
expect(result.positionals).toEqual([])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should parse command with long options', () => {
|
|
31
|
+
const result = parser.parse(['node', 'script.js', 'test', '--verbose'])
|
|
32
|
+
expect(result.command).toBe('test')
|
|
33
|
+
expect(result.options).toEqual({ verbose: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should parse command with long options and values', () => {
|
|
37
|
+
const result = parser.parse([
|
|
38
|
+
'node',
|
|
39
|
+
'script.js',
|
|
40
|
+
'test',
|
|
41
|
+
'--config',
|
|
42
|
+
'prod',
|
|
43
|
+
])
|
|
44
|
+
expect(result.command).toBe('test')
|
|
45
|
+
expect(result.options).toEqual({ config: 'prod' })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should parse command with kebab-case options to camelCase', () => {
|
|
49
|
+
const result = parser.parse(['node', 'script.js', 'test', '--dry-run'])
|
|
50
|
+
expect(result.command).toBe('test')
|
|
51
|
+
expect(result.options).toEqual({ dryRun: true })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should parse command with short options', () => {
|
|
55
|
+
const result = parser.parse(['node', 'script.js', 'test', '-v'])
|
|
56
|
+
expect(result.command).toBe('test')
|
|
57
|
+
expect(result.options).toEqual({ v: true })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should parse command with short options and values', () => {
|
|
61
|
+
const result = parser.parse(['node', 'script.js', 'test', '-c', 'prod'])
|
|
62
|
+
expect(result.command).toBe('test')
|
|
63
|
+
expect(result.options).toEqual({ c: 'prod' })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should parse multiple short flags', () => {
|
|
67
|
+
const result = parser.parse(['node', 'script.js', 'test', '-abc'])
|
|
68
|
+
expect(result.command).toBe('test')
|
|
69
|
+
expect(result.options).toEqual({ a: true, b: true, c: true })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should parse mixed options and positionals', () => {
|
|
73
|
+
const result = parser.parse(
|
|
74
|
+
[
|
|
75
|
+
'node',
|
|
76
|
+
'script.js',
|
|
77
|
+
'test',
|
|
78
|
+
'--verbose',
|
|
79
|
+
'file1',
|
|
80
|
+
'-c',
|
|
81
|
+
'prod',
|
|
82
|
+
'file2',
|
|
83
|
+
],
|
|
84
|
+
z.object({
|
|
85
|
+
verbose: z.boolean(),
|
|
86
|
+
c: z.string(),
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
expect(result.command).toBe('test')
|
|
90
|
+
expect(result.options).toEqual({ verbose: true, c: 'prod' })
|
|
91
|
+
expect(result.positionals).toEqual(['file1', 'file2'])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should parse options with equal sign syntax', () => {
|
|
95
|
+
const result = parser.parse([
|
|
96
|
+
'node',
|
|
97
|
+
'script.js',
|
|
98
|
+
'test',
|
|
99
|
+
'--config=prod',
|
|
100
|
+
'--port=3000',
|
|
101
|
+
])
|
|
102
|
+
expect(result.command).toBe('test')
|
|
103
|
+
expect(result.options).toEqual({ config: 'prod', port: 3000 })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should throw error when no command provided', () => {
|
|
107
|
+
expect(() => parser.parse(['node', 'script.js'])).toThrow(
|
|
108
|
+
'[Navios Commander] No command provided',
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('value type parsing', () => {
|
|
114
|
+
it('should parse boolean values', () => {
|
|
115
|
+
const result = parser.parse([
|
|
116
|
+
'node',
|
|
117
|
+
'script.js',
|
|
118
|
+
'test',
|
|
119
|
+
'--flag',
|
|
120
|
+
'true',
|
|
121
|
+
'--other',
|
|
122
|
+
'false',
|
|
123
|
+
])
|
|
124
|
+
expect(result.options).toEqual({ flag: true, other: false })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should parse integer values', () => {
|
|
128
|
+
const result = parser.parse([
|
|
129
|
+
'node',
|
|
130
|
+
'script.js',
|
|
131
|
+
'test',
|
|
132
|
+
'--port',
|
|
133
|
+
'3000',
|
|
134
|
+
])
|
|
135
|
+
expect(result.options).toEqual({ port: 3000 })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should parse negative integer values', () => {
|
|
139
|
+
const result = parser.parse([
|
|
140
|
+
'node',
|
|
141
|
+
'script.js',
|
|
142
|
+
'test',
|
|
143
|
+
'--offset',
|
|
144
|
+
'"-10"',
|
|
145
|
+
])
|
|
146
|
+
expect(result.options).toEqual({ offset: '"-10"' })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should parse float values', () => {
|
|
150
|
+
const result = parser.parse([
|
|
151
|
+
'node',
|
|
152
|
+
'script.js',
|
|
153
|
+
'test',
|
|
154
|
+
'--ratio',
|
|
155
|
+
'3.14',
|
|
156
|
+
])
|
|
157
|
+
expect(result.options).toEqual({ ratio: 3.14 })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should parse null value', () => {
|
|
161
|
+
const result = parser.parse([
|
|
162
|
+
'node',
|
|
163
|
+
'script.js',
|
|
164
|
+
'test',
|
|
165
|
+
'--value',
|
|
166
|
+
'null',
|
|
167
|
+
])
|
|
168
|
+
expect(result.options).toEqual({ value: null })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should parse JSON object', () => {
|
|
172
|
+
const result = parser.parse([
|
|
173
|
+
'node',
|
|
174
|
+
'script.js',
|
|
175
|
+
'test',
|
|
176
|
+
'--data',
|
|
177
|
+
'{"key":"value"}',
|
|
178
|
+
])
|
|
179
|
+
expect(result.options).toEqual({ data: { key: 'value' } })
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should parse JSON array', () => {
|
|
183
|
+
const result = parser.parse([
|
|
184
|
+
'node',
|
|
185
|
+
'script.js',
|
|
186
|
+
'test',
|
|
187
|
+
'--items',
|
|
188
|
+
'[1,2,3]',
|
|
189
|
+
])
|
|
190
|
+
expect(result.options).toEqual({ items: [1, 2, 3] })
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe('schema-aware parsing', () => {
|
|
195
|
+
it('should detect boolean flags from schema', () => {
|
|
196
|
+
const schema = z.object({
|
|
197
|
+
verbose: z.boolean(),
|
|
198
|
+
config: z.string(),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const result = parser.parse(
|
|
202
|
+
['node', 'script.js', 'test', '--verbose', '--config', 'prod'],
|
|
203
|
+
schema,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
expect(result.options).toEqual({
|
|
207
|
+
verbose: true,
|
|
208
|
+
config: 'prod',
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should handle optional boolean flags', () => {
|
|
213
|
+
const schema = z.object({
|
|
214
|
+
verbose: z.boolean().optional(),
|
|
215
|
+
dryRun: z.boolean().optional(),
|
|
216
|
+
config: z.string(),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const result = parser.parse(
|
|
220
|
+
[
|
|
221
|
+
'node',
|
|
222
|
+
'script.js',
|
|
223
|
+
'test',
|
|
224
|
+
'--verbose',
|
|
225
|
+
'--dry-run',
|
|
226
|
+
'--config',
|
|
227
|
+
'prod',
|
|
228
|
+
],
|
|
229
|
+
schema,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
expect(result.options).toEqual({
|
|
233
|
+
verbose: true,
|
|
234
|
+
dryRun: true,
|
|
235
|
+
config: 'prod',
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should handle default boolean flags', () => {
|
|
240
|
+
const schema = z.object({
|
|
241
|
+
verbose: z.boolean().default(false),
|
|
242
|
+
config: z.string(),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const result = parser.parse(
|
|
246
|
+
['node', 'script.js', 'test', '--verbose', '--config', 'prod'],
|
|
247
|
+
schema,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
expect(result.options).toEqual({
|
|
251
|
+
verbose: true,
|
|
252
|
+
config: 'prod',
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should prevent boolean flag from consuming next option as value', () => {
|
|
257
|
+
const schema = z.object({
|
|
258
|
+
verbose: z.boolean(),
|
|
259
|
+
debug: z.boolean(),
|
|
260
|
+
config: z.string(),
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Without schema, --verbose would incorrectly consume '--debug' as its value
|
|
264
|
+
const result = parser.parse(
|
|
265
|
+
[
|
|
266
|
+
'node',
|
|
267
|
+
'script.js',
|
|
268
|
+
'test',
|
|
269
|
+
'--verbose',
|
|
270
|
+
'--debug',
|
|
271
|
+
'--config',
|
|
272
|
+
'prod',
|
|
273
|
+
],
|
|
274
|
+
schema,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
expect(result.options).toEqual({
|
|
278
|
+
verbose: true,
|
|
279
|
+
debug: true,
|
|
280
|
+
config: 'prod',
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should handle mixed boolean and non-boolean options', () => {
|
|
285
|
+
const schema = z.object({
|
|
286
|
+
verbose: z.boolean(),
|
|
287
|
+
port: z.number(),
|
|
288
|
+
host: z.string(),
|
|
289
|
+
dryRun: z.boolean(),
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const result = parser.parse(
|
|
293
|
+
[
|
|
294
|
+
'node',
|
|
295
|
+
'script.js',
|
|
296
|
+
'test',
|
|
297
|
+
'--verbose',
|
|
298
|
+
'--port',
|
|
299
|
+
'3000',
|
|
300
|
+
'--host',
|
|
301
|
+
'localhost',
|
|
302
|
+
'--dry-run',
|
|
303
|
+
],
|
|
304
|
+
schema,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
expect(result.options).toEqual({
|
|
308
|
+
verbose: true,
|
|
309
|
+
port: 3000,
|
|
310
|
+
host: 'localhost',
|
|
311
|
+
dryRun: true,
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('should work without schema (fallback behavior)', () => {
|
|
316
|
+
const result = parser.parse([
|
|
317
|
+
'node',
|
|
318
|
+
'script.js',
|
|
319
|
+
'test',
|
|
320
|
+
'--verbose',
|
|
321
|
+
'--config',
|
|
322
|
+
'prod',
|
|
323
|
+
])
|
|
324
|
+
|
|
325
|
+
// Without schema, parser uses heuristics
|
|
326
|
+
expect(result.options).toEqual({
|
|
327
|
+
verbose: true,
|
|
328
|
+
config: 'prod',
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should handle short boolean flags from schema', () => {
|
|
333
|
+
const schema = z.object({
|
|
334
|
+
v: z.boolean(),
|
|
335
|
+
c: z.string(),
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const result = parser.parse(
|
|
339
|
+
['node', 'script.js', 'test', '-v', '-c', 'prod'],
|
|
340
|
+
schema,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
expect(result.options).toEqual({
|
|
344
|
+
v: true,
|
|
345
|
+
c: 'prod',
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should handle complex schema with nested optional booleans', () => {
|
|
350
|
+
const schema = z.object({
|
|
351
|
+
verbose: z.boolean().optional(),
|
|
352
|
+
debug: z.boolean().default(false),
|
|
353
|
+
quiet: z.boolean(),
|
|
354
|
+
config: z.string(),
|
|
355
|
+
port: z.number().optional(),
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const result = parser.parse(
|
|
359
|
+
[
|
|
360
|
+
'node',
|
|
361
|
+
'script.js',
|
|
362
|
+
'test',
|
|
363
|
+
'--verbose',
|
|
364
|
+
'--debug',
|
|
365
|
+
'--quiet',
|
|
366
|
+
'--config',
|
|
367
|
+
'production',
|
|
368
|
+
],
|
|
369
|
+
schema,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
expect(result.options).toEqual({
|
|
373
|
+
verbose: true,
|
|
374
|
+
debug: true,
|
|
375
|
+
quiet: true,
|
|
376
|
+
config: 'production',
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
describe('edge cases', () => {
|
|
382
|
+
it('should handle command followed immediately by options', () => {
|
|
383
|
+
const result = parser.parse(['node', 'script.js', 'test', '--flag'])
|
|
384
|
+
expect(result.command).toBe('test')
|
|
385
|
+
expect(result.options).toEqual({ flag: true })
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('should handle options with dashes in values', () => {
|
|
389
|
+
const result = parser.parse([
|
|
390
|
+
'node',
|
|
391
|
+
'script.js',
|
|
392
|
+
'test',
|
|
393
|
+
'--branch',
|
|
394
|
+
'feature-test',
|
|
395
|
+
])
|
|
396
|
+
expect(result.options).toEqual({ branch: 'feature-test' })
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('should handle single dash as positional', () => {
|
|
400
|
+
const result = parser.parse(['node', 'script.js', 'test', '-'])
|
|
401
|
+
expect(result.positionals).toEqual(['-'])
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
})
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { ZodObject, ZodType } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { Injectable } from '@navios/di'
|
|
4
|
+
|
|
5
|
+
export interface ParsedCliArgs {
|
|
6
|
+
command: string
|
|
7
|
+
options: Record<string, any>
|
|
8
|
+
positionals: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class CliParserService {
|
|
13
|
+
/**
|
|
14
|
+
* Parses command-line arguments from process.argv
|
|
15
|
+
* Commands can be multi-word (e.g., 'db migrate', 'cache clear')
|
|
16
|
+
* Expected format: node script.js command [subcommand...] --flag value --boolean-flag positional1 positional2
|
|
17
|
+
*
|
|
18
|
+
* @param argv - Array of command-line arguments (typically process.argv)
|
|
19
|
+
* @param optionsSchema - Optional Zod schema to determine boolean flags and option types
|
|
20
|
+
* @returns Parsed command (space-separated if multi-word), options, and positional arguments
|
|
21
|
+
*/
|
|
22
|
+
parse(argv: string[], optionsSchema?: ZodObject): ParsedCliArgs {
|
|
23
|
+
// Skip first two args (node and script path)
|
|
24
|
+
const args = argv.slice(2)
|
|
25
|
+
|
|
26
|
+
if (args.length === 0) {
|
|
27
|
+
throw new Error('[Navios Commander] No command provided')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract boolean field names from schema for accurate parsing
|
|
31
|
+
const booleanFields = optionsSchema
|
|
32
|
+
? this.extractBooleanFields(optionsSchema)
|
|
33
|
+
: new Set<string>()
|
|
34
|
+
|
|
35
|
+
// Collect command words until we hit an argument that starts with '-' or '--'
|
|
36
|
+
const commandParts: string[] = []
|
|
37
|
+
let i = 0
|
|
38
|
+
while (i < args.length && !args[i].startsWith('-')) {
|
|
39
|
+
commandParts.push(args[i])
|
|
40
|
+
i++
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (commandParts.length === 0) {
|
|
44
|
+
throw new Error('[Navios Commander] No command provided')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const command = commandParts.join(' ')
|
|
48
|
+
const options: Record<string, any> = {}
|
|
49
|
+
const positionals: string[] = []
|
|
50
|
+
while (i < args.length) {
|
|
51
|
+
const arg = args[i]
|
|
52
|
+
|
|
53
|
+
if (arg.startsWith('--')) {
|
|
54
|
+
// Long option format: --key=value or --key value
|
|
55
|
+
const key = arg.slice(2)
|
|
56
|
+
const equalIndex = key.indexOf('=')
|
|
57
|
+
|
|
58
|
+
if (equalIndex !== -1) {
|
|
59
|
+
// Format: --key=value
|
|
60
|
+
const optionName = key.slice(0, equalIndex)
|
|
61
|
+
const optionValue = key.slice(equalIndex + 1)
|
|
62
|
+
options[this.camelCase(optionName)] = this.parseValue(optionValue)
|
|
63
|
+
i++
|
|
64
|
+
} else {
|
|
65
|
+
// Format: --key value or --boolean-flag
|
|
66
|
+
const camelCaseKey = this.camelCase(key)
|
|
67
|
+
const isBoolean =
|
|
68
|
+
booleanFields.has(camelCaseKey) || booleanFields.has(key)
|
|
69
|
+
const nextArg = args[i + 1]
|
|
70
|
+
|
|
71
|
+
if (isBoolean) {
|
|
72
|
+
// Known boolean flag from schema
|
|
73
|
+
options[camelCaseKey] = true
|
|
74
|
+
i++
|
|
75
|
+
} else if (nextArg && !nextArg.startsWith('-')) {
|
|
76
|
+
// Has a value
|
|
77
|
+
options[camelCaseKey] = this.parseValue(nextArg)
|
|
78
|
+
i += 2
|
|
79
|
+
} else {
|
|
80
|
+
// Assume boolean flag
|
|
81
|
+
options[camelCaseKey] = true
|
|
82
|
+
i++
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else if (arg.startsWith('-') && arg.length > 1 && arg !== '-') {
|
|
86
|
+
// Short option format: -k value or -abc (multiple flags)
|
|
87
|
+
const flags = arg.slice(1)
|
|
88
|
+
|
|
89
|
+
if (flags.length === 1) {
|
|
90
|
+
// Single short flag: -k value or -k
|
|
91
|
+
const isBoolean = booleanFields.has(flags)
|
|
92
|
+
const nextArg = args[i + 1]
|
|
93
|
+
|
|
94
|
+
if (isBoolean) {
|
|
95
|
+
// Known boolean flag from schema
|
|
96
|
+
options[flags] = true
|
|
97
|
+
i++
|
|
98
|
+
} else if (nextArg && !nextArg.startsWith('-')) {
|
|
99
|
+
options[flags] = this.parseValue(nextArg)
|
|
100
|
+
i += 2
|
|
101
|
+
} else {
|
|
102
|
+
options[flags] = true
|
|
103
|
+
i++
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// Multiple short flags: -abc -> {a: true, b: true, c: true}
|
|
107
|
+
for (const flag of flags) {
|
|
108
|
+
options[flag] = true
|
|
109
|
+
}
|
|
110
|
+
i++
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Positional argument
|
|
114
|
+
positionals.push(arg)
|
|
115
|
+
i++
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
command,
|
|
121
|
+
options,
|
|
122
|
+
positionals,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Converts kebab-case to camelCase
|
|
128
|
+
*/
|
|
129
|
+
private camelCase(str: string): string {
|
|
130
|
+
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Attempts to parse string values into appropriate types
|
|
135
|
+
*/
|
|
136
|
+
private parseValue(value: string): any {
|
|
137
|
+
// Check for boolean
|
|
138
|
+
if (value === 'true') return true
|
|
139
|
+
if (value === 'false') return false
|
|
140
|
+
|
|
141
|
+
// Check for null/undefined
|
|
142
|
+
if (value === 'null') return null
|
|
143
|
+
if (value === 'undefined') return undefined
|
|
144
|
+
|
|
145
|
+
// Check for number
|
|
146
|
+
if (/^-?\d+$/.test(value)) {
|
|
147
|
+
return parseInt(value, 10)
|
|
148
|
+
}
|
|
149
|
+
if (/^-?\d+\.\d+$/.test(value)) {
|
|
150
|
+
return parseFloat(value)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check for JSON
|
|
154
|
+
if (
|
|
155
|
+
(value.startsWith('{') && value.endsWith('}')) ||
|
|
156
|
+
(value.startsWith('[') && value.endsWith(']'))
|
|
157
|
+
) {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(value)
|
|
160
|
+
} catch {
|
|
161
|
+
// If parsing fails, return as string
|
|
162
|
+
return value
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Return as string
|
|
167
|
+
return value
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extracts boolean field names from a Zod schema
|
|
172
|
+
* Handles ZodObject, ZodOptional, and ZodDefault wrappers
|
|
173
|
+
*/
|
|
174
|
+
private extractBooleanFields(schema: ZodObject): Set<string> {
|
|
175
|
+
const booleanFields = new Set<string>()
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Check if schema has _def.typeName (Zod schema structure)
|
|
179
|
+
const typeName = schema.def.type
|
|
180
|
+
|
|
181
|
+
if (typeName === 'object') {
|
|
182
|
+
// Extract shape from ZodObject
|
|
183
|
+
const shape = schema.def.shape
|
|
184
|
+
|
|
185
|
+
if (shape && typeof shape === 'object') {
|
|
186
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
187
|
+
if (this.isSchemaBoolean(fieldSchema as any)) {
|
|
188
|
+
booleanFields.add(key)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// Silently fail if schema introspection fails
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return booleanFields
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Checks if a Zod schema represents a boolean type
|
|
202
|
+
* Unwraps ZodOptional and ZodDefault
|
|
203
|
+
*/
|
|
204
|
+
private isSchemaBoolean(schema: ZodType): boolean {
|
|
205
|
+
try {
|
|
206
|
+
let currentSchema = schema
|
|
207
|
+
const typeName = currentSchema.def.type
|
|
208
|
+
|
|
209
|
+
// Unwrap ZodOptional and ZodDefault
|
|
210
|
+
if (typeName === 'optional' || typeName === 'default') {
|
|
211
|
+
currentSchema = (currentSchema as any)?._def?.innerType || currentSchema
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const innerTypeName = currentSchema.def.type
|
|
215
|
+
return innerTypeName === 'boolean'
|
|
216
|
+
} catch {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Formats help text for available commands
|
|
223
|
+
*/
|
|
224
|
+
formatCommandList(commands: Array<{ path: string; class: any }>): string {
|
|
225
|
+
const lines = ['Available commands:', '']
|
|
226
|
+
for (const { path } of commands) {
|
|
227
|
+
lines.push(` ${path}`)
|
|
228
|
+
}
|
|
229
|
+
return lines.join('\n')
|
|
230
|
+
}
|
|
231
|
+
}
|