icore 1.0.4 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icore",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Declarative command line interface mechanics for Node.js",
5
5
  "keywords": [
6
6
  "command-line",
package/readme.md CHANGED
@@ -5,33 +5,335 @@
5
5
  [![types](https://img.shields.io/npm/types/icore.svg)](https://www.npmjs.com/package/icore)
6
6
  [![license](https://img.shields.io/npm/l/icore.svg)](LICENSE)
7
7
 
8
- Small dependency-free command line interface mechanics for Node.js applications.
8
+ Small dependency-free command line interface mechanics for [Node.js®](https://nodejs.org) applications.
9
9
 
10
- `icore` describes CLI commands with typed option schemas, resolves command
11
- registries, validates primitive options, and passes typed input to handlers. It
12
- stops at CLI mechanics: your application still owns business rules, SDK calls,
13
- process lifecycle, and output formatting.
10
+ ### Installation
14
11
 
15
- ## How It Works
12
+ To use `icore` in your project, run:
16
13
 
17
- ![yuml diagram](http://yuml.me/diagram/scruffy;dir:LR;/class/[*argv*%20{bg:gray}|External;users%20get%20--limit%2010%20--json]->[*matches*%20{bg:lavender}|System;parse,%20resolve,%20validate,%20infer]->[*typed%20result*%20{bg:honeydew}|Container;command=users/get;%20limit=10;%20json=true]->[*your%20app*%20{bg:cornsilk}|System;business%20logic%20and%20output])
14
+ ```sh
15
+ npm install icore
16
+ ```
18
17
 
19
- [API Reference](docs/api.md) | [Examples](docs/examples.md)
18
+ ### Table of Contents
20
19
 
21
- ## Install
20
+ * [API](#api)
21
+ * [`parseArgv(args, schema?)`](#parseargvargs-schema)
22
+ * [`parseOptions(schema, values)`](#parseoptionsschema-values)
23
+ * [`parseOptionsDetailed(schema, values)`](#parseoptionsdetailedschema-values)
24
+ * [`defineCommand(command)`](#definecommandcommand)
25
+ * [`defineCommandRegistry(commands)`](#definecommandregistrycommands)
26
+ * [`isCommandName(registry, value)`](#iscommandnameregistry-value)
27
+ * [`resolveCommand(registry, positionals)`](#resolvecommandregistry-positionals)
28
+ * [`resolveCommandFromArgs(registry, args)`](#resolvecommandfromargsregistry-args)
29
+ * [`runCommandFromRegistry(registry, args, context)`](#runcommandfromregistryregistry-args-context)
30
+ * [`mergeOptionsSchema(...schemas)`](#mergeoptionsschemaschemas)
31
+ * [`runCommand(command, args, context)`](#runcommandcommand-args-context)
32
+ * [Example](#example)
33
+ * [Option Schemas](#option-schemas)
34
+ * [Type Inference](#type-inference)
35
+ * [Facade of arguments](#facade-of-arguments)
36
+ * [Error Messages](#error-messages)
22
37
 
23
- To use `icore` in your project, run:
38
+ ### API
24
39
 
25
- ```sh
26
- npm install icore
40
+ #### `parseArgv(args, schema?)`
41
+
42
+ Parses raw CLI arguments into positionals and raw option values.
43
+
44
+ ```ts
45
+ import { parseArgv } from 'icore';
46
+
47
+ const argv = parseArgv([
48
+ 'hello',
49
+ '--name',
50
+ 'User',
51
+ '--upper'
52
+ ]);
53
+ ```
54
+
55
+ Result:
56
+
57
+ ```ts
58
+ {
59
+ positionals: ['hello'],
60
+ options: {
61
+ name: 'User',
62
+ upper: true
63
+ }
64
+ }
65
+ ```
66
+
67
+ When an option schema is provided, boolean options are parsed as flag-only
68
+ options without consuming the following positional argument:
69
+
70
+ ```ts
71
+ const argv = parseArgv([
72
+ 'hello',
73
+ '--upper',
74
+ 'User'
75
+ ], {
76
+ upper: {
77
+ type: 'boolean'
78
+ }
79
+ });
27
80
  ```
28
81
 
29
- ## Quick Start
82
+ Result:
83
+
84
+ ```ts
85
+ {
86
+ positionals: ['hello', 'User'],
87
+ options: {
88
+ upper: true
89
+ }
90
+ }
91
+ ```
92
+
93
+ #### `parseOptions(schema, values)`
94
+
95
+ Validates raw option values using an option schema and returns typed options.
96
+
97
+ ```ts
98
+ import { parseOptions } from 'icore';
99
+
100
+ const options = parseOptions({
101
+ name: {
102
+ type: 'string',
103
+ default: 'world'
104
+ },
105
+ upper: {
106
+ type: 'boolean'
107
+ }
108
+ } as const, {
109
+ name: 'User',
110
+ upper: true
111
+ });
112
+ ```
113
+
114
+ Result:
115
+
116
+ ```ts
117
+ {
118
+ name: 'User',
119
+ upper: true
120
+ }
121
+ ```
122
+
123
+ #### `parseOptionsDetailed(schema, values)`
124
+
125
+ Validates raw option values and returns parsed options together with
126
+ user-provided metadata.
127
+
128
+ ```ts
129
+ import { parseOptionsDetailed } from 'icore';
130
+
131
+ const result = parseOptionsDetailed({
132
+ name: {
133
+ type: 'string',
134
+ default: 'world'
135
+ },
136
+ upper: {
137
+ type: 'boolean'
138
+ }
139
+ } as const, {
140
+ upper: true
141
+ });
142
+ ```
143
+
144
+ Result:
145
+
146
+ ```ts
147
+ {
148
+ options: {
149
+ name: 'world',
150
+ upper: true
151
+ },
152
+ provided: {
153
+ name: false,
154
+ upper: true
155
+ }
156
+ }
157
+ ```
158
+
159
+ `provided` is useful when a default value and an omitted option have different
160
+ application-level meaning.
161
+
162
+ #### `defineCommand(command)`
163
+
164
+ Defines a command while preserving its option schema types.
165
+
166
+ ```ts
167
+ import { defineCommand } from 'icore';
168
+
169
+ const command = defineCommand({
170
+ path: ['hello'],
171
+ options: {
172
+ name: {
173
+ type: 'string',
174
+ default: 'world'
175
+ },
176
+ upper: {
177
+ type: 'boolean'
178
+ }
179
+ },
180
+ async handle({ options }) {
181
+ const message = `Hello, ${options.name}!`;
182
+
183
+ return options.upper ? message.toUpperCase() : message;
184
+ }
185
+ });
186
+ ```
187
+
188
+ #### `defineCommandRegistry(commands)`
189
+
190
+ Defines a command registry while preserving literal command path types.
191
+
192
+ ```ts
193
+ import { defineCommandRegistry } from 'icore';
194
+
195
+ const registry = defineCommandRegistry([
196
+ helloCommand,
197
+ helloFormalCommand
198
+ ] as const);
199
+ ```
200
+
201
+ The registry exposes ordered commands and derived command names:
202
+
203
+ ```ts
204
+ registry.commandNames;
205
+ ```
206
+
207
+ Result:
208
+
209
+ ```ts
210
+ [
211
+ 'hello',
212
+ 'hello formal'
213
+ ]
214
+ ```
215
+
216
+ Duplicate command paths are rejected.
217
+
218
+ #### `isCommandName(registry, value)`
219
+
220
+ Checks whether an unknown value is a command name registered in the registry.
221
+
222
+ ```ts
223
+ if (isCommandName(registry, value)) {
224
+ // value is narrowed to the registry command name union.
225
+ }
226
+ ```
227
+
228
+ #### `resolveCommand(registry, positionals)`
229
+
230
+ Resolves a command from already parsed positional arguments.
231
+
232
+ ```ts
233
+ import { resolveCommand } from 'icore';
234
+
235
+ const resolved = resolveCommand(registry, [
236
+ 'hello',
237
+ 'formal',
238
+ 'User'
239
+ ]);
240
+ ```
241
+
242
+ Result:
243
+
244
+ ```ts
245
+ {
246
+ name: 'hello formal',
247
+ path: ['hello', 'formal'],
248
+ command: helloFormalCommand,
249
+ positionals: ['User']
250
+ }
251
+ ```
252
+
253
+ When several command paths match, the most specific command wins. For example,
254
+ `hello formal` is preferred over `hello`.
255
+
256
+ #### `resolveCommandFromArgs(registry, args)`
257
+
258
+ Resolves a command from raw CLI arguments. Each candidate command is parsed with
259
+ its own option schema, so boolean flags do not accidentally consume command path
260
+ segments.
261
+
262
+ ```ts
263
+ const resolved = resolveCommandFromArgs(registry, [
264
+ 'hello',
265
+ '--name',
266
+ 'User',
267
+ '--upper'
268
+ ]);
269
+ ```
270
+
271
+ #### `runCommandFromRegistry(registry, args, context)`
272
+
273
+ Resolves a command from a registry and runs its handler.
274
+
275
+ ```ts
276
+ const output = await runCommandFromRegistry(
277
+ registry,
278
+ ['hello', '--name', 'User', '--upper'],
279
+ context
280
+ );
281
+ ```
282
+
283
+ This is **registry-level mechanics only**. Application-specific setup, side
284
+ effects, and output formatting still belong outside `icore`.
285
+
286
+ #### `mergeOptionsSchema(...schemas)`
287
+
288
+ Merges multiple option schemas while preserving literal option definition types.
289
+ Later schemas override earlier schemas with the same option name.
290
+
291
+ ```ts
292
+ import { mergeOptionsSchema } from 'icore';
293
+
294
+ const nameOptions = {
295
+ name: {
296
+ type: 'string',
297
+ default: 'world'
298
+ }
299
+ } as const;
300
+
301
+ const greetingOptions = {
302
+ upper: {
303
+ type: 'boolean'
304
+ }
305
+ } as const;
306
+
307
+ const options = mergeOptionsSchema(nameOptions, greetingOptions);
308
+ ```
309
+
310
+ #### `runCommand(command, args, context)`
311
+
312
+ Parses arguments, validates options, checks command positionals, and runs the
313
+ handler.
314
+
315
+ ```ts
316
+ const output = await runCommand(
317
+ command,
318
+ ['hello', '--name', 'User', '--upper'],
319
+ context
320
+ );
321
+ ```
322
+
323
+ **By default**, extra positionals are rejected. A command can opt in to extra
324
+ positionals with `allowExtraPositionals: true`.
325
+
326
+ ### How It Works
327
+
328
+ ![yuml diagram](http://yuml.me/diagram/scruffy;dir:LR;/class/[*argv*%20{bg:gray}|External;hello%20--name%20User%20--upper]->[*matches*%20{bg:lavender}|System;parse,%20resolve,%20validate,%20infer]->[*typed%20result*%20{bg:honeydew}|Container;command=hello;%20name=User;%20upper=true]->[*your%20app*%20{bg:cornsilk}|System;business%20logic%20and%20output])
329
+
330
+
331
+ ### Example
30
332
 
31
333
  ```ts
32
334
  import { defineCommand, runCommand } from 'icore';
33
335
 
34
- const helloCommand = defineCommand({
336
+ const exampleCommand = defineCommand({
35
337
  path: ['hello'],
36
338
  options: {
37
339
  name: {
@@ -50,53 +352,194 @@ const helloCommand = defineCommand({
50
352
  });
51
353
 
52
354
  const output = await runCommand(
53
- helloCommand,
54
- ['hello', '--name', 'Stanislav', '--upper'],
355
+ exampleCommand,
356
+ ['hello', '--name', 'User', '--upper'],
55
357
  {}
56
358
  );
57
359
 
58
360
  console.log(output);
59
361
  ```
60
362
 
61
- ## Why icore?
363
+ Terminal output:
62
364
 
63
- Use `icore` when you need more than raw argument parsing:
365
+ ```console
366
+ $ node cli.js hello --name User --upper
367
+ HELLO, USER!
368
+ ```
64
369
 
65
- - typed command definitions;
66
- - command registry resolution;
67
- - required options;
68
- - string choices;
69
- - number parsing with integer, minimum, and maximum constraints;
70
- - option presence metadata;
71
- - typed command handler input.
370
+ The command handler receives parsed options, user-provided option metadata,
371
+ remaining positionals, and caller provided context.
372
+
373
+ ### Option Schemas
374
+
375
+ Options are described as plain objects.
376
+
377
+ **Option names are exact.** `icore` does not normalize `camelCase` to
378
+ `kebab-case`. Use quoted object keys when your public CLI option contains
379
+ `-`.
380
+
381
+ #### `type: 'string'`
382
+
383
+ ```ts
384
+ const schema = {
385
+ name: {
386
+ type: 'string',
387
+ required: true
388
+ },
389
+ style: {
390
+ type: 'string',
391
+ choices: ['short', 'long'],
392
+ default: 'short'
393
+ }
394
+ } as const;
395
+ ```
396
+
397
+ String options reject missing required values, blank strings, boolean flag form,
398
+ and values outside `choices`.
399
+
400
+ #### `type: 'boolean'`
401
+
402
+ ```ts
403
+ const schema = {
404
+ upper: {
405
+ type: 'boolean'
406
+ }
407
+ } as const;
408
+ ```
409
+
410
+ Boolean options accept **flag form only**:
411
+
412
+ ```sh
413
+ --upper
414
+ ```
415
+
416
+ Explicit values are rejected:
417
+
418
+ ```sh
419
+ --upper true
420
+ --upper=true
421
+ ```
422
+
423
+ #### `type: 'number'`
424
+
425
+ ```ts
426
+ const schema = {
427
+ limit: {
428
+ type: 'number',
429
+ integer: true,
430
+ min: 1,
431
+ max: 1000,
432
+ default: 100
433
+ }
434
+ } as const;
435
+ ```
436
+
437
+ Number options parse decimal numeric values and can validate integer and range
438
+ constraints. Defaults are validated with the same rules as user-provided values.
439
+
440
+ ### Type Inference
441
+
442
+ Use `InferOptions` when you need the parsed option type explicitly.
443
+
444
+ ```ts
445
+ import type { InferOptions } from 'icore';
446
+
447
+ const schema = {
448
+ name: {
449
+ type: 'string',
450
+ default: 'world'
451
+ },
452
+ upper: {
453
+ type: 'boolean'
454
+ }
455
+ } as const;
456
+
457
+ type Options = InferOptions<typeof schema>;
458
+ ```
459
+
460
+ `Options` is equivalent to:
461
+
462
+ ```ts
463
+ type Options = {
464
+ name: string;
465
+ upper: boolean | undefined;
466
+ };
467
+ ```
468
+
469
+ **Required options and options with defaults are always present.** Optional
470
+ options without defaults are returned as `T | undefined`.
471
+
472
+ Use `InferProvidedOptions` when you need the option presence type explicitly.
473
+
474
+ ```ts
475
+ import type { InferProvidedOptions } from 'icore';
476
+
477
+ type Provided = InferProvidedOptions<typeof schema>;
478
+ ```
72
479
 
73
- Use [`node:util.parseArgs`](https://nodejs.org/api/util.html#utilparseargsconfig)
74
- directly when you only need low-level argument parsing.
480
+ `Provided` maps every schema option to `boolean`. `true` means the user
481
+ specified that option explicitly; defaults keep the flag `false`.
75
482
 
76
- ## Supported Syntax
483
+ Use `MergeOptionsSchemas` when you need the merged schema type explicitly.
77
484
 
78
- | Syntax | Supported | Notes |
79
- |---|---:|---|
80
- | `--name value` | yes | string and number options |
81
- | `--name=value` | yes | string and number options |
82
- | `--flag` | yes | boolean options |
83
- | `--flag=true` | no | boolean options are flag-only |
84
- | `-f` | no | short aliases are not supported |
85
- | `--no-cache` | no | negative boolean flags are not supported |
86
- | repeated options | no | duplicates are rejected |
87
- | multiple values | no | arrays are not supported |
485
+ ```ts
486
+ import type { MergeOptionsSchemas } from 'icore';
88
487
 
89
- ## Documentation
488
+ type Schema = MergeOptionsSchemas<[typeof nameOptions, typeof greetingOptions]>;
489
+ ```
90
490
 
91
- - [Design notes](docs/design.md)
92
- - [Project policies](docs/policy/index.md)
491
+ Use `CommandName` when you need the inferred command name type explicitly.
93
492
 
94
- ## Requirements
493
+ ```ts
494
+ import type { CommandName } from 'icore';
95
495
 
96
- - Node.js `>=16.9.0`
97
- - TypeScript declarations are included.
98
- - Runtime dependencies: none.
496
+ type Name = CommandName<typeof helloFormalCommand>;
497
+ ```
99
498
 
100
- ## License
499
+ `Name` is equivalent to:
500
+
501
+ ```ts
502
+ type Name = 'hello formal';
503
+ ```
101
504
 
102
- [MIT](LICENSE)
505
+ ### Facade of arguments
506
+
507
+ The table below provides examples of how to specify the syntax.
508
+
509
+ | Syntax | Supported |
510
+ |---|---:|
511
+ | `--name value` | yes |
512
+ | `--name=value` | yes |
513
+ | `--flag` | yes |
514
+ | `--flag=true` | no |
515
+ | `-f` | no |
516
+ | `--no-cache` | no |
517
+
518
+ ### Error Messages
519
+
520
+ `icore` throws regular `Error` objects with predictable user-facing messages.
521
+ Applications should treat these messages as **display text**, not as a
522
+ **machine-readable API**.
523
+
524
+ Applications can catch these errors and decide how to print them. For example,
525
+ after printing `error.message`, terminal output can look like this:
526
+
527
+ ```console
528
+ $ node cli.js hello --unknown
529
+ Unexpected argument '--unknown'
530
+
531
+ $ node cli.js hello --upper=true
532
+ Expected '--upper' as boolean flag
533
+ ```
534
+
535
+ ### Project Boundary
536
+
537
+ `icore` is intended to be a **small CLI mechanics module**. It should **not**
538
+ grow into a domain-specific framework for a particular SDK or API.
539
+
540
+ Good responsibilities for `icore`:
541
+
542
+ - option schema evaluation;
543
+ - command path checking;
544
+ - common argument errors;
545
+ - typed command handler input.