@xyd-js/opencli 0.1.0-build.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.
@@ -0,0 +1,588 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "opencli-spec.json",
4
+ "type": "object",
5
+ "properties": {
6
+ "opencli": {
7
+ "type": "string",
8
+ "description": "The OpenCLI version number"
9
+ },
10
+ "info": {
11
+ "$ref": "#/$defs/CliInfo",
12
+ "description": "Information about the CLI"
13
+ },
14
+ "conventions": {
15
+ "$ref": "#/$defs/Conventions",
16
+ "description": "The conventions used by the CLI"
17
+ },
18
+ "arguments": {
19
+ "type": "array",
20
+ "items": {
21
+ "$ref": "#/$defs/Argument"
22
+ },
23
+ "description": "Root command arguments"
24
+ },
25
+ "options": {
26
+ "type": "array",
27
+ "items": {
28
+ "$ref": "#/$defs/Option"
29
+ },
30
+ "description": "Root command options"
31
+ },
32
+ "commands": {
33
+ "type": "array",
34
+ "items": {
35
+ "$ref": "#/$defs/Command"
36
+ },
37
+ "description": "Root command sub commands"
38
+ },
39
+ "exitCodes": {
40
+ "type": "array",
41
+ "items": {
42
+ "$ref": "#/$defs/ExitCode"
43
+ },
44
+ "description": "Root command exit codes"
45
+ },
46
+ "examples": {
47
+ "type": "array",
48
+ "items": {
49
+ "type": "string"
50
+ },
51
+ "description": "Examples of how to use the CLI"
52
+ },
53
+ "interactive": {
54
+ "type": "boolean",
55
+ "description": "Indicates whether or not the command requires interactive input"
56
+ },
57
+ "metadata": {
58
+ "type": "array",
59
+ "items": {
60
+ "$ref": "#/$defs/Metadata"
61
+ },
62
+ "description": "Custom metadata"
63
+ },
64
+ "x-openapi": {
65
+ "$ref": "#/$defs/XOpenApiRoot",
66
+ "description": "xyd extension: OpenAPI/HTTP binding for the CLI as a whole (servers + security). Emitted by @xyd-js/openapi2opencli and consumed by the opencli2* code generators to build real API requests."
67
+ }
68
+ },
69
+ "required": [
70
+ "opencli",
71
+ "info"
72
+ ],
73
+ "description": "The OpenCLI description",
74
+ "$defs": {
75
+ "CliInfo": {
76
+ "type": "object",
77
+ "properties": {
78
+ "title": {
79
+ "type": "string",
80
+ "description": "The application title"
81
+ },
82
+ "summary": {
83
+ "type": "string",
84
+ "description": "A short summary of the application"
85
+ },
86
+ "description": {
87
+ "type": "string",
88
+ "description": "A description of the application"
89
+ },
90
+ "contact": {
91
+ "$ref": "#/$defs/Contact",
92
+ "description": "The contact information"
93
+ },
94
+ "license": {
95
+ "$ref": "#/$defs/License",
96
+ "description": "The application license"
97
+ },
98
+ "version": {
99
+ "type": "string",
100
+ "description": "The application version"
101
+ }
102
+ },
103
+ "required": [
104
+ "title",
105
+ "version"
106
+ ]
107
+ },
108
+ "Conventions": {
109
+ "type": "object",
110
+ "properties": {
111
+ "groupOptions": {
112
+ "type": "boolean",
113
+ "default": true,
114
+ "description": "Whether or not grouping of short options are allowed"
115
+ },
116
+ "optionSeparator": {
117
+ "type": "string",
118
+ "default": " ",
119
+ "description": "The option argument separator"
120
+ }
121
+ }
122
+ },
123
+ "Argument": {
124
+ "type": "object",
125
+ "properties": {
126
+ "name": {
127
+ "type": "string",
128
+ "description": "The argument name"
129
+ },
130
+ "required": {
131
+ "type": "boolean",
132
+ "description": "Whether or not the argument is required"
133
+ },
134
+ "arity": {
135
+ "$ref": "#/$defs/Arity",
136
+ "description": "The argument arity. Arity defines the minimum and maximum number of argument values"
137
+ },
138
+ "acceptedValues": {
139
+ "type": "array",
140
+ "items": {
141
+ "type": "string"
142
+ },
143
+ "description": "A list of accepted values"
144
+ },
145
+ "group": {
146
+ "type": "string",
147
+ "description": "The argument group"
148
+ },
149
+ "description": {
150
+ "type": "string",
151
+ "description": "The argument description"
152
+ },
153
+ "hidden": {
154
+ "type": "boolean",
155
+ "default": false,
156
+ "description": "Whether or not the argument is hidden"
157
+ },
158
+ "metadata": {
159
+ "type": "array",
160
+ "items": {
161
+ "$ref": "#/$defs/Metadata"
162
+ },
163
+ "description": "Custom metadata"
164
+ }
165
+ },
166
+ "required": [
167
+ "name"
168
+ ]
169
+ },
170
+ "Option": {
171
+ "type": "object",
172
+ "properties": {
173
+ "name": {
174
+ "type": "string",
175
+ "description": "The option name"
176
+ },
177
+ "required": {
178
+ "type": "boolean",
179
+ "description": "Whether or not the option is required"
180
+ },
181
+ "aliases": {
182
+ "type": "array",
183
+ "items": {
184
+ "type": "string"
185
+ },
186
+ "uniqueItems": true,
187
+ "description": "The option's aliases"
188
+ },
189
+ "arguments": {
190
+ "type": "array",
191
+ "items": {
192
+ "$ref": "#/$defs/Argument"
193
+ },
194
+ "description": "The option's arguments"
195
+ },
196
+ "group": {
197
+ "type": "string",
198
+ "description": "The option group"
199
+ },
200
+ "description": {
201
+ "type": "string",
202
+ "description": "The option description"
203
+ },
204
+ "recursive": {
205
+ "type": "boolean",
206
+ "default": false,
207
+ "description": "Specifies whether the option is accessible from the immediate parent command and, recursively, from its subcommands"
208
+ },
209
+ "hidden": {
210
+ "type": "boolean",
211
+ "default": false,
212
+ "description": "Whether or not the option is hidden"
213
+ },
214
+ "metadata": {
215
+ "type": "array",
216
+ "items": {
217
+ "$ref": "#/$defs/Metadata"
218
+ },
219
+ "description": "Custom metadata"
220
+ }
221
+ },
222
+ "required": [
223
+ "name"
224
+ ]
225
+ },
226
+ "Command": {
227
+ "type": "object",
228
+ "properties": {
229
+ "name": {
230
+ "type": "string",
231
+ "description": "The command name"
232
+ },
233
+ "aliases": {
234
+ "type": "array",
235
+ "items": {
236
+ "type": "string"
237
+ },
238
+ "uniqueItems": true,
239
+ "description": "The command aliases"
240
+ },
241
+ "options": {
242
+ "type": "array",
243
+ "items": {
244
+ "$ref": "#/$defs/Option"
245
+ },
246
+ "description": "The command options"
247
+ },
248
+ "arguments": {
249
+ "type": "array",
250
+ "items": {
251
+ "$ref": "#/$defs/Argument"
252
+ },
253
+ "description": "The command arguments"
254
+ },
255
+ "commands": {
256
+ "type": "array",
257
+ "items": {
258
+ "$ref": "#/$defs/Command"
259
+ },
260
+ "description": "The command's sub commands"
261
+ },
262
+ "exitCodes": {
263
+ "type": "array",
264
+ "items": {
265
+ "$ref": "#/$defs/ExitCode"
266
+ },
267
+ "description": "The command's exit codes"
268
+ },
269
+ "description": {
270
+ "type": "string",
271
+ "description": "The command description"
272
+ },
273
+ "hidden": {
274
+ "type": "boolean",
275
+ "default": false,
276
+ "description": "Whether or not the command is hidden"
277
+ },
278
+ "examples": {
279
+ "type": "array",
280
+ "items": {
281
+ "type": "string"
282
+ },
283
+ "description": "Examples of how to use the command"
284
+ },
285
+ "interactive": {
286
+ "type": "boolean",
287
+ "description": "Indicate whether or not the command requires interactive input"
288
+ },
289
+ "metadata": {
290
+ "type": "array",
291
+ "items": {
292
+ "$ref": "#/$defs/Metadata"
293
+ },
294
+ "description": "Custom metadata"
295
+ },
296
+ "x-openapi": {
297
+ "$ref": "#/$defs/XOpenApiCommand",
298
+ "description": "xyd extension: OpenAPI/HTTP binding for this command's operation (method, path, params, body). Consumed by the opencli2* code generators to build real API requests."
299
+ }
300
+ },
301
+ "required": [
302
+ "name"
303
+ ]
304
+ },
305
+ "ExitCode": {
306
+ "type": "object",
307
+ "properties": {
308
+ "code": {
309
+ "type": "integer",
310
+ "description": "The exit code"
311
+ },
312
+ "description": {
313
+ "type": "string",
314
+ "description": "The exit code description"
315
+ }
316
+ },
317
+ "required": [
318
+ "code"
319
+ ]
320
+ },
321
+ "Metadata": {
322
+ "type": "object",
323
+ "properties": {
324
+ "name": {
325
+ "type": "string"
326
+ },
327
+ "value": {}
328
+ },
329
+ "required": [
330
+ "name"
331
+ ]
332
+ },
333
+ "Contact": {
334
+ "type": "object",
335
+ "properties": {
336
+ "name": {
337
+ "type": "string",
338
+ "description": "The identifying name of the contact person/organization"
339
+ },
340
+ "url": {
341
+ "type": "string",
342
+ "format": "uri",
343
+ "description": "The URI for the contact information. This MUST be in the form of a URI."
344
+ },
345
+ "email": {
346
+ "$ref": "#/$defs/email",
347
+ "description": "The email address of the contact person/organization. This MUST be in the form of an email address."
348
+ }
349
+ },
350
+ "description": "Contact information"
351
+ },
352
+ "License": {
353
+ "type": "object",
354
+ "properties": {
355
+ "name": {
356
+ "type": "string",
357
+ "description": "The license name"
358
+ },
359
+ "identifier": {
360
+ "type": "string",
361
+ "description": "The SPDX license identifier"
362
+ },
363
+ "url": {
364
+ "type": "string",
365
+ "description": "The URI for the license. This MUST be in the form of a URI"
366
+ }
367
+ }
368
+ },
369
+ "Arity": {
370
+ "type": "object",
371
+ "properties": {
372
+ "minimum": {
373
+ "type": "integer",
374
+ "minimum": 0,
375
+ "description": "The minimum number of values allowed"
376
+ },
377
+ "maximum": {
378
+ "type": "integer",
379
+ "minimum": 0,
380
+ "description": "The maximum number of values allowed"
381
+ }
382
+ },
383
+ "description": "Arity defines the minimum and maximum number of argument values"
384
+ },
385
+ "email": {
386
+ "type": "string",
387
+ "pattern": ".+\\@.+\\..+"
388
+ },
389
+ "XOpenApiRoot": {
390
+ "type": "object",
391
+ "description": "xyd extension: OpenAPI/HTTP binding for the CLI as a whole.",
392
+ "properties": {
393
+ "servers": {
394
+ "type": "array",
395
+ "items": {
396
+ "type": "string"
397
+ },
398
+ "description": "Available API base URLs (from the OpenAPI servers list)"
399
+ },
400
+ "security": {
401
+ "type": "array",
402
+ "items": {
403
+ "$ref": "#/$defs/XOpenApiSecurity"
404
+ },
405
+ "description": "Default security requirements for the CLI"
406
+ }
407
+ }
408
+ },
409
+ "XOpenApiSecurity": {
410
+ "type": "object",
411
+ "description": "A single security scheme mapped from the OpenAPI securitySchemes.",
412
+ "properties": {
413
+ "kind": {
414
+ "type": "string",
415
+ "description": "Normalized scheme a generator branches on: bearer | apiKey-header | apiKey-query | apiKey-cookie | basic | other"
416
+ },
417
+ "type": {
418
+ "type": "string",
419
+ "description": "The OpenAPI security scheme type (e.g. http, apiKey, oauth2)"
420
+ },
421
+ "scheme": {
422
+ "type": "string",
423
+ "description": "The HTTP auth scheme (e.g. bearer, basic) when type is http"
424
+ },
425
+ "in": {
426
+ "type": "string",
427
+ "description": "Location for an apiKey scheme (header, query or cookie)"
428
+ },
429
+ "name": {
430
+ "type": "string",
431
+ "description": "Header/query/cookie name for an apiKey scheme"
432
+ },
433
+ "bearerFormat": {
434
+ "type": "string",
435
+ "description": "The bearer token format hint (e.g. JWT)"
436
+ },
437
+ "envVar": {
438
+ "type": "string",
439
+ "description": "Environment variable a generated CLI reads the credential from (e.g. OPENAI_API_KEY)"
440
+ }
441
+ },
442
+ "required": [
443
+ "type"
444
+ ]
445
+ },
446
+ "XOpenApiCommand": {
447
+ "type": "object",
448
+ "description": "xyd extension: OpenAPI/HTTP binding for a single command's operation.",
449
+ "properties": {
450
+ "operationId": {
451
+ "type": "string",
452
+ "description": "The source OpenAPI operationId, when present"
453
+ },
454
+ "method": {
455
+ "type": "string",
456
+ "description": "HTTP method (lowercase)"
457
+ },
458
+ "path": {
459
+ "type": "string",
460
+ "description": "HTTP path template (e.g. /chat/completions/{completion_id})"
461
+ },
462
+ "server": {
463
+ "type": "string",
464
+ "description": "Override base URL for this operation, if any"
465
+ },
466
+ "contentType": {
467
+ "type": "string",
468
+ "description": "Request body content type"
469
+ },
470
+ "security": {
471
+ "type": "array",
472
+ "items": {
473
+ "$ref": "#/$defs/XOpenApiSecurity"
474
+ },
475
+ "description": "Per-operation security requirements (overrides root)"
476
+ },
477
+ "params": {
478
+ "type": "array",
479
+ "items": {
480
+ "$ref": "#/$defs/XOpenApiParam"
481
+ },
482
+ "description": "How CLI arguments/options map to HTTP parameters"
483
+ },
484
+ "body": {
485
+ "$ref": "#/$defs/XOpenApiBody",
486
+ "description": "How CLI options map to the request body"
487
+ }
488
+ },
489
+ "required": [
490
+ "method",
491
+ "path"
492
+ ]
493
+ },
494
+ "XOpenApiParam": {
495
+ "type": "object",
496
+ "description": "Maps one CLI argument/option onto an HTTP parameter.",
497
+ "properties": {
498
+ "in": {
499
+ "type": "string",
500
+ "description": "Parameter location (path, query, header or cookie)"
501
+ },
502
+ "name": {
503
+ "type": "string",
504
+ "description": "The wire name of the HTTP parameter"
505
+ },
506
+ "from": {
507
+ "type": "string",
508
+ "description": "Source CLI token: 'argument:<name>' or 'option:<name>'"
509
+ },
510
+ "required": {
511
+ "type": "boolean",
512
+ "description": "Whether the HTTP parameter is required"
513
+ },
514
+ "explode": {
515
+ "type": "boolean",
516
+ "description": "OpenAPI explode flag for array/object serialization"
517
+ },
518
+ "style": {
519
+ "type": "string",
520
+ "description": "OpenAPI serialization style"
521
+ }
522
+ },
523
+ "required": [
524
+ "in",
525
+ "name",
526
+ "from"
527
+ ]
528
+ },
529
+ "XOpenApiBody": {
530
+ "type": "object",
531
+ "description": "Maps CLI options onto the HTTP request body.",
532
+ "properties": {
533
+ "style": {
534
+ "type": "string",
535
+ "description": "How the body is assembled: 'flatten' (per-property flags), 'json' (single JSON flag) or 'multipart'"
536
+ },
537
+ "contentType": {
538
+ "type": "string",
539
+ "description": "Body content type"
540
+ },
541
+ "from": {
542
+ "type": "string",
543
+ "description": "Source CLI token when the whole body comes from one option (style=json)"
544
+ },
545
+ "properties": {
546
+ "type": "array",
547
+ "items": {
548
+ "$ref": "#/$defs/XOpenApiBodyProp"
549
+ },
550
+ "description": "Per-property body field mappings (style=flatten/multipart)"
551
+ }
552
+ },
553
+ "required": [
554
+ "style"
555
+ ]
556
+ },
557
+ "XOpenApiBodyProp": {
558
+ "type": "object",
559
+ "description": "Maps one CLI option onto a request body field.",
560
+ "properties": {
561
+ "name": {
562
+ "type": "string",
563
+ "description": "The wire name of the body field"
564
+ },
565
+ "from": {
566
+ "type": "string",
567
+ "description": "Source CLI token: 'option:<name>'"
568
+ },
569
+ "jsonPath": {
570
+ "type": "string",
571
+ "description": "Dot path of the field within the JSON body"
572
+ },
573
+ "encoding": {
574
+ "type": "string",
575
+ "description": "How the CLI value is encoded into the field: 'string', 'number', 'integer', 'boolean', 'json' (parse as JSON), 'array' or 'file'"
576
+ },
577
+ "required": {
578
+ "type": "boolean",
579
+ "description": "Whether the body field is required"
580
+ }
581
+ },
582
+ "required": [
583
+ "name",
584
+ "from"
585
+ ]
586
+ }
587
+ }
588
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@xyd-js/opencli",
3
+ "version": "0.1.0-build.0",
4
+ "description": "OpenCLI core model and helpers (shared by opencli-remark and the openapi2opencli / opencli2* generators)",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "dependencies": {},
8
+ "devDependencies": {
9
+ "@types/node": "^20.9.0",
10
+ "json-schema-to-typescript": "^14.0.1",
11
+ "rimraf": "^3.0.2",
12
+ "tsup": "^8.3.0",
13
+ "typescript": "^5.6.2",
14
+ "vitest": "^2.1.1"
15
+ },
16
+ "scripts": {
17
+ "clean": "rimraf dist",
18
+ "prebuild": "pnpm clean",
19
+ "build": "tsup",
20
+ "test": "vitest",
21
+ "ci:test": "vitest run",
22
+ "generate:types": "json2ts -i opencli-spec.json -o src/types.ts",
23
+ "format": "bunx biome check --write src"
24
+ }
25
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { findCommand, generateUsage, generateOptions, generateArguments } from '../index';
4
+ import type { OpencliSpecJson } from '../index';
5
+
6
+ const spec: OpencliSpecJson = {
7
+ opencli: '1.0.0',
8
+ info: { title: 'spice', version: '1.0.0', description: 'Spice CLI' },
9
+ commands: [
10
+ {
11
+ name: 'install',
12
+ aliases: ['i'],
13
+ description: 'Install a package',
14
+ options: [{ name: 'global', aliases: ['g'], description: 'Install globally' }],
15
+ arguments: [{ name: 'package', required: true, description: 'Package name' }],
16
+ commands: [{ name: 'dev', description: 'Install as dev dependency' }],
17
+ },
18
+ ],
19
+ };
20
+
21
+ describe('@xyd-js/opencli core', () => {
22
+ it('findCommand resolves a top-level command', () => {
23
+ const cmd = findCommand(spec, 'install');
24
+ expect(cmd?.name).toBe('install');
25
+ });
26
+
27
+ it('findCommand resolves via alias', () => {
28
+ const cmd = findCommand(spec, 'i');
29
+ expect(cmd?.name).toBe('install');
30
+ });
31
+
32
+ it('findCommand resolves a nested command', () => {
33
+ const cmd = findCommand(spec, 'install dev');
34
+ expect(cmd?.name).toBe('dev');
35
+ });
36
+
37
+ it('findCommand with empty path returns synthetic root', () => {
38
+ const cmd = findCommand(spec, '');
39
+ expect(cmd?.name).toBe('spice');
40
+ expect(cmd?.commands?.[0]?.name).toBe('install');
41
+ });
42
+
43
+ it('generateUsage renders options + required arg', () => {
44
+ const cmd = findCommand(spec, 'install')!;
45
+ expect(generateUsage(spec, cmd, 'spice install')).toBe('spice install [options] <package>');
46
+ });
47
+
48
+ it('generateOptions / generateArguments render tab-indented code style', () => {
49
+ const cmd = findCommand(spec, 'install')!;
50
+ expect(generateOptions(cmd, 'code')).toContain('-g, --global');
51
+ expect(generateArguments(cmd, 'code')).toContain('package');
52
+ });
53
+
54
+ it('x-openapi binding survives on the model (typed extension)', () => {
55
+ const withBinding: OpencliSpecJson = {
56
+ ...spec,
57
+ 'x-openapi': { servers: ['https://api.example.com/v1'] },
58
+ };
59
+ expect(withBinding['x-openapi']?.servers?.[0]).toBe('https://api.example.com/v1');
60
+ });
61
+ });