icore 1.0.2 → 1.0.4

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/docs/api.md ADDED
@@ -0,0 +1,466 @@
1
+ # API Reference
2
+
3
+ ## Option Schemas
4
+
5
+ Options are described as plain objects.
6
+
7
+ Option names are exact. `icore` does not normalize camelCase to kebab-case or
8
+ kebab-case to camelCase. Use quoted object keys when your public CLI option
9
+ contains `-`.
10
+
11
+ ### String Options
12
+
13
+ ```ts
14
+ const schema = {
15
+ token: {
16
+ type: 'string',
17
+ required: true
18
+ },
19
+ format: {
20
+ type: 'string',
21
+ choices: ['json', 'table'],
22
+ default: 'table'
23
+ }
24
+ } as const;
25
+ ```
26
+
27
+ String options reject missing required values, blank strings, boolean flag form,
28
+ and values outside `choices`.
29
+
30
+ ### Boolean Options
31
+
32
+ ```ts
33
+ const schema = {
34
+ insecure: {
35
+ type: 'boolean'
36
+ }
37
+ } as const;
38
+ ```
39
+
40
+ Boolean options accept flag form only:
41
+
42
+ ```sh
43
+ --insecure
44
+ ```
45
+
46
+ Explicit values are rejected:
47
+
48
+ ```sh
49
+ --insecure true
50
+ --insecure=true
51
+ ```
52
+
53
+ ### Number Options
54
+
55
+ ```ts
56
+ const schema = {
57
+ limit: {
58
+ type: 'number',
59
+ integer: true,
60
+ min: 1,
61
+ max: 1000,
62
+ default: 100
63
+ }
64
+ } as const;
65
+ ```
66
+
67
+ Number options parse decimal numeric values and can validate integer and range
68
+ constraints. Defaults are validated with the same rules as user-provided values.
69
+
70
+ ## `parseArgv(args, schema?)`
71
+
72
+ Parses raw CLI arguments into positionals and raw option values.
73
+
74
+ ```ts
75
+ import { parseArgv } from 'icore';
76
+
77
+ const argv = parseArgv([
78
+ 'users',
79
+ 'get-accounts',
80
+ '--format=json',
81
+ '--insecure'
82
+ ]);
83
+ ```
84
+
85
+ Result:
86
+
87
+ ```ts
88
+ {
89
+ positionals: ['users', 'get-accounts'],
90
+ options: {
91
+ format: 'json',
92
+ insecure: true
93
+ }
94
+ }
95
+ ```
96
+
97
+ When an option schema is provided, boolean options are parsed as flag-only
98
+ options without consuming the following positional argument:
99
+
100
+ ```ts
101
+ const argv = parseArgv([
102
+ 'users',
103
+ 'get-accounts',
104
+ '--insecure',
105
+ 'extra'
106
+ ], {
107
+ insecure: {
108
+ type: 'boolean'
109
+ }
110
+ });
111
+ ```
112
+
113
+ Result:
114
+
115
+ ```ts
116
+ {
117
+ positionals: ['users', 'get-accounts', 'extra'],
118
+ options: {
119
+ insecure: true
120
+ }
121
+ }
122
+ ```
123
+
124
+ ## `parseOptions(schema, values)`
125
+
126
+ Validates raw option values using an option schema and returns typed options.
127
+
128
+ ```ts
129
+ import { parseOptions } from 'icore';
130
+
131
+ const options = parseOptions({
132
+ format: {
133
+ type: 'string',
134
+ choices: ['json', 'table'],
135
+ default: 'table'
136
+ },
137
+ depth: {
138
+ type: 'number',
139
+ integer: true,
140
+ min: 1,
141
+ required: true
142
+ }
143
+ } as const, {
144
+ depth: '10'
145
+ });
146
+ ```
147
+
148
+ Result:
149
+
150
+ ```ts
151
+ {
152
+ format: 'table',
153
+ depth: 10
154
+ }
155
+ ```
156
+
157
+ ## `parseOptionsDetailed(schema, values)`
158
+
159
+ Validates raw option values and returns parsed options together with
160
+ user-provided metadata.
161
+
162
+ ```ts
163
+ import { parseOptionsDetailed } from 'icore';
164
+
165
+ const result = parseOptionsDetailed({
166
+ token: {
167
+ type: 'string',
168
+ required: true
169
+ },
170
+ format: {
171
+ type: 'string',
172
+ choices: ['json', 'table'],
173
+ default: 'table'
174
+ }
175
+ } as const, {
176
+ token: 'secret'
177
+ });
178
+ ```
179
+
180
+ Result:
181
+
182
+ ```ts
183
+ {
184
+ options: {
185
+ token: 'secret',
186
+ format: 'table'
187
+ },
188
+ provided: {
189
+ token: true,
190
+ format: false
191
+ }
192
+ }
193
+ ```
194
+
195
+ `provided` is useful when a default value and an omitted option have different
196
+ application-level meaning.
197
+
198
+ ## `defineCommand(command)`
199
+
200
+ Defines a command while preserving its option schema types.
201
+
202
+ ```ts
203
+ import { defineCommand } from 'icore';
204
+
205
+ const command = defineCommand({
206
+ path: ['marketdata', 'get-order-book'],
207
+ options: {
208
+ 'instrument-id': {
209
+ type: 'string',
210
+ required: true
211
+ },
212
+ depth: {
213
+ type: 'number',
214
+ integer: true,
215
+ min: 1,
216
+ required: true
217
+ },
218
+ format: {
219
+ type: 'string',
220
+ choices: ['json', 'table'],
221
+ default: 'table'
222
+ }
223
+ },
224
+ async handle({ options, context }) {
225
+ const response = await context.sdk.marketdata.getOrderBook({
226
+ instrumentId: options['instrument-id'],
227
+ depth: options.depth
228
+ });
229
+
230
+ return context.formatOrderBook(response, options.format);
231
+ }
232
+ });
233
+ ```
234
+
235
+ ## `defineCommandRegistry(commands)`
236
+
237
+ Defines a command registry while preserving literal command path types.
238
+
239
+ ```ts
240
+ import { defineCommandRegistry } from 'icore';
241
+
242
+ const registry = defineCommandRegistry([
243
+ usersGetAccountsCommand,
244
+ marketdataGetOrderBookCommand
245
+ ] as const);
246
+ ```
247
+
248
+ The registry exposes ordered commands and derived command names:
249
+
250
+ ```ts
251
+ registry.commandNames;
252
+ ```
253
+
254
+ Result:
255
+
256
+ ```ts
257
+ [
258
+ 'users get-accounts',
259
+ 'marketdata get-order-book'
260
+ ]
261
+ ```
262
+
263
+ Duplicate command paths are rejected.
264
+
265
+ ## `isCommandName(registry, value)`
266
+
267
+ Checks whether an unknown value is a command name registered in the registry.
268
+
269
+ ```ts
270
+ if (isCommandName(registry, value)) {
271
+ // value is narrowed to the registry command name union.
272
+ }
273
+ ```
274
+
275
+ ## `resolveCommand(registry, positionals)`
276
+
277
+ Resolves a command from already parsed positional arguments.
278
+
279
+ ```ts
280
+ import { resolveCommand } from 'icore';
281
+
282
+ const resolved = resolveCommand(registry, [
283
+ 'users',
284
+ 'get-accounts',
285
+ 'extra'
286
+ ]);
287
+ ```
288
+
289
+ Result:
290
+
291
+ ```ts
292
+ {
293
+ name: 'users get-accounts',
294
+ path: ['users', 'get-accounts'],
295
+ command: usersGetAccountsCommand,
296
+ positionals: ['extra']
297
+ }
298
+ ```
299
+
300
+ When several command paths match, the most specific command wins. For example,
301
+ `users get-accounts` is preferred over `users`.
302
+
303
+ ## `resolveCommandFromArgs(registry, args)`
304
+
305
+ Resolves a command from raw CLI arguments. Each candidate command is parsed with
306
+ its own option schema, so boolean flags do not accidentally consume command path
307
+ segments.
308
+
309
+ ```ts
310
+ const resolved = resolveCommandFromArgs(registry, [
311
+ '--verbose',
312
+ 'users',
313
+ 'get-accounts'
314
+ ]);
315
+ ```
316
+
317
+ ## `runCommandFromRegistry(registry, args, context)`
318
+
319
+ Resolves a command from a registry and runs its handler.
320
+
321
+ ```ts
322
+ const output = await runCommandFromRegistry(
323
+ registry,
324
+ ['users', 'get-accounts', '--format', 'json'],
325
+ context
326
+ );
327
+ ```
328
+
329
+ This is registry-level mechanics only. Application-specific setup, API request
330
+ building, and output formatting still belong outside `icore`.
331
+
332
+ ## `mergeOptionsSchema(...schemas)`
333
+
334
+ Merges multiple option schemas while preserving literal option definition types.
335
+ Later schemas override earlier schemas with the same option name.
336
+
337
+ ```ts
338
+ import { mergeOptionsSchema } from 'icore';
339
+
340
+ const sdkOptions = {
341
+ token: {
342
+ type: 'string',
343
+ required: true
344
+ }
345
+ } as const;
346
+
347
+ const formatOptions = {
348
+ format: {
349
+ type: 'string',
350
+ choices: ['json', 'table'],
351
+ default: 'table'
352
+ }
353
+ } as const;
354
+
355
+ const options = mergeOptionsSchema(sdkOptions, formatOptions);
356
+ ```
357
+
358
+ ## `runCommand(command, args, context)`
359
+
360
+ Parses arguments, validates options, checks command positionals, and runs the
361
+ handler.
362
+
363
+ ```ts
364
+ const output = await runCommand(
365
+ command,
366
+ [
367
+ 'marketdata',
368
+ 'get-order-book',
369
+ '--instrument-id',
370
+ 'instrument-id',
371
+ '--depth',
372
+ '10'
373
+ ],
374
+ context
375
+ );
376
+ ```
377
+
378
+ By default, extra positionals are rejected. A command can opt in to extra
379
+ positionals with `allowExtraPositionals: true`.
380
+
381
+ ## Type Inference
382
+
383
+ Use `InferOptions` when you need the parsed option type explicitly.
384
+
385
+ ```ts
386
+ import type { InferOptions } from 'icore';
387
+
388
+ const schema = {
389
+ format: {
390
+ type: 'string',
391
+ choices: ['json', 'table'],
392
+ default: 'table'
393
+ },
394
+ dryRun: {
395
+ type: 'boolean'
396
+ }
397
+ } as const;
398
+
399
+ type Options = InferOptions<typeof schema>;
400
+ ```
401
+
402
+ `Options` is equivalent to:
403
+
404
+ ```ts
405
+ type Options = {
406
+ format: 'json' | 'table';
407
+ dryRun: boolean | undefined;
408
+ };
409
+ ```
410
+
411
+ Required options and options with defaults are always present. Optional options
412
+ without defaults are returned as `T | undefined`.
413
+
414
+ Use `InferProvidedOptions` when you need the option presence type explicitly.
415
+
416
+ ```ts
417
+ import type { InferProvidedOptions } from 'icore';
418
+
419
+ type Provided = InferProvidedOptions<typeof schema>;
420
+ ```
421
+
422
+ `Provided` maps every schema option to `boolean`. `true` means the user
423
+ specified that option explicitly; defaults keep the flag `false`.
424
+
425
+ Use `MergeOptionsSchemas` when you need the merged schema type explicitly.
426
+
427
+ ```ts
428
+ import type { MergeOptionsSchemas } from 'icore';
429
+
430
+ type Schema = MergeOptionsSchemas<[typeof sdkOptions, typeof formatOptions]>;
431
+ ```
432
+
433
+ Use `CommandName` when you need the inferred command name type explicitly.
434
+
435
+ ```ts
436
+ import type { CommandName } from 'icore';
437
+
438
+ type Name = CommandName<typeof usersGetAccountsCommand>;
439
+ ```
440
+
441
+ `Name` is equivalent to:
442
+
443
+ ```ts
444
+ type Name = 'users get-accounts';
445
+ ```
446
+
447
+ ## Error Messages
448
+
449
+ `icore` throws regular `Error` objects with predictable user-facing messages.
450
+ Applications should treat these messages as display text, not as a
451
+ machine-readable API.
452
+
453
+ Examples:
454
+
455
+ ```txt
456
+ Unexpected argument '--unknown'
457
+ Unexpected duplicate argument '--format'
458
+ Unexpected duplicate command 'users get-accounts'
459
+ Unknown command: users get-unknown
460
+ Expected required argument '--token'
461
+ Expected '--format' as one of: json, table
462
+ Expected '--depth' as integer
463
+ Expected '--depth' to be greater than or equal to 1
464
+ ```
465
+
466
+ Applications can catch these errors and decide how to print them.
package/docs/design.md ADDED
@@ -0,0 +1,80 @@
1
+ # Design Notes
2
+
3
+ ## Purpose
4
+
5
+ `icore` is a small CLI mechanics module. It standardizes command definitions,
6
+ argument parsing, primitive option validation, command registry resolution, and
7
+ typed command handler input.
8
+
9
+ It intentionally stops before application behavior. Your application still
10
+ owns business validation, API request mapping, SDK calls, process lifecycle,
11
+ and output formatting.
12
+
13
+ ## Why icore?
14
+
15
+ Use `icore` when you need more than raw argument parsing:
16
+
17
+ - typed command definitions;
18
+ - command registry resolution;
19
+ - required options;
20
+ - string choices;
21
+ - number parsing with integer, minimum, and maximum constraints;
22
+ - option presence metadata;
23
+ - typed command handler input.
24
+
25
+ Use [`node:util.parseArgs`](https://nodejs.org/api/util.html#utilparseargsconfig)
26
+ directly when you only need low-level argument parsing.
27
+
28
+ ## Non-goals
29
+
30
+ `icore` stops at CLI mechanics. It does not build API DTOs, call SDKs, format
31
+ output, manage process lifecycle, or validate business rules.
32
+
33
+ It also does not handle:
34
+
35
+ - provider-specific request modes;
36
+ - generated contract mapping;
37
+ - mutually exclusive application modes;
38
+ - presentation formatting as JSON, tables, CSV, or another application format;
39
+ - database, HTTP, gRPC, or SDK lifecycle management.
40
+
41
+ Keep those decisions near the command handler or in your application layer.
42
+
43
+ ## Supported Syntax
44
+
45
+ | Syntax | Supported | Notes |
46
+ |---|---:|---|
47
+ | `--name value` | yes | string and number options |
48
+ | `--name=value` | yes | string and number options |
49
+ | `--flag` | yes | boolean options |
50
+ | `--flag=true` | no | boolean options are flag-only |
51
+ | `-f` | no | short aliases are not supported |
52
+ | `--no-cache` | no | negative boolean flags are not supported |
53
+ | repeated options | no | duplicates are rejected |
54
+ | multiple values | no | arrays are not supported |
55
+
56
+ ## Design Goals
57
+
58
+ - describe CLI mechanics declaratively;
59
+ - use literal option types: `type: 'string' | 'boolean' | 'number'`;
60
+ - keep handlers free from repetitive option parsing;
61
+ - keep domain and API semantics outside the framework;
62
+ - provide predictable user-facing errors;
63
+ - avoid runtime dependencies.
64
+
65
+ ## Project Boundary
66
+
67
+ Good responsibilities for `icore`:
68
+
69
+ - option schema evaluation;
70
+ - command path checking;
71
+ - common argument errors;
72
+ - typed command handler input.
73
+
74
+ Responsibilities that should stay outside `icore`:
75
+
76
+ - business validation;
77
+ - SDK lifecycle management;
78
+ - provider-specific request modes;
79
+ - generated contract mapping;
80
+ - presentation formatting.
@@ -0,0 +1,100 @@
1
+ # Examples
2
+
3
+ ## Basic Command
4
+
5
+ ```ts
6
+ import { defineCommand, runCommand } from 'icore';
7
+
8
+ const helloCommand = defineCommand({
9
+ path: ['hello'],
10
+ options: {
11
+ name: {
12
+ type: 'string',
13
+ default: 'world'
14
+ },
15
+ upper: {
16
+ type: 'boolean'
17
+ }
18
+ },
19
+ async handle({ options }) {
20
+ const message = `Hello, ${options.name}!`;
21
+
22
+ return options.upper ? message.toUpperCase() : message;
23
+ }
24
+ });
25
+
26
+ const output = await runCommand(
27
+ helloCommand,
28
+ ['hello', '--name', 'Stanislav', '--upper'],
29
+ {}
30
+ );
31
+
32
+ console.log(output);
33
+ ```
34
+
35
+ ## Command Registry
36
+
37
+ ```ts
38
+ import {
39
+ defineCommand,
40
+ defineCommandRegistry,
41
+ runCommandFromRegistry
42
+ } from 'icore';
43
+
44
+ const usersListCommand = defineCommand({
45
+ path: ['users', 'list'],
46
+ options: {
47
+ limit: {
48
+ type: 'number',
49
+ integer: true,
50
+ min: 1,
51
+ max: 100,
52
+ default: 20
53
+ }
54
+ },
55
+ async handle({ options }) {
56
+ return `List ${options.limit} users`;
57
+ }
58
+ });
59
+
60
+ const registry = defineCommandRegistry([
61
+ usersListCommand
62
+ ] as const);
63
+
64
+ const output = await runCommandFromRegistry(
65
+ registry,
66
+ ['users', 'list', '--limit', '10'],
67
+ {}
68
+ );
69
+ ```
70
+
71
+ ## Option Presence Metadata
72
+
73
+ Use `parseOptionsDetailed` when defaults and explicitly provided values have
74
+ different application-level meaning.
75
+
76
+ ```ts
77
+ import { parseOptionsDetailed } from 'icore';
78
+
79
+ const result = parseOptionsDetailed({
80
+ format: {
81
+ type: 'string',
82
+ choices: ['json', 'table'],
83
+ default: 'table'
84
+ }
85
+ } as const, {});
86
+
87
+ result.options.format;
88
+ result.provided.format;
89
+ ```
90
+
91
+ `result.options.format` is `table`, and `result.provided.format` is `false`.
92
+
93
+ ## CommonJS
94
+
95
+ ```js
96
+ const {
97
+ defineCommand,
98
+ runCommand
99
+ } = require('icore');
100
+ ```