icore 1.0.19 → 2.0.1

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 CHANGED
@@ -5,713 +5,243 @@
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 and terminal presentation mechanics for [Node.js®](https://nodejs.org) applications.
8
+ Small dependency-free command line interface and terminal presentation
9
+ mechanics for [Node.js®](https://nodejs.org) applications.
9
10
 
10
- Supports a practical GNU-style option syntax:
11
+ `icore` owns the path from `process.argv` to terminal output:
11
12
 
12
- - long options: `--name value`, `--name=value`;
13
- - boolean flags: `--flag`, `--no-flag`;
14
- - short aliases: `-f`, `-n value`;
15
- - option terminator: `--`.
13
+ - declarative, type-inferred option schemas;
14
+ - multi-segment command resolution;
15
+ - separate prepare and execute phases;
16
+ - JSON, CSV, and text-table presentation;
17
+ - stdout/stderr writers and reusable terminal error policy.
16
18
 
17
- ## Installation
19
+ Application-specific API calls, configuration, domain behavior, and help text
20
+ remain in the consuming application.
18
21
 
19
- To use `icore` in your project, run:
22
+ ## Requirements And Installation
23
+
24
+ `icore` requires Node.js `>=16.9.0` and includes TypeScript declarations.
20
25
 
21
26
  ```sh
22
27
  npm install icore
23
28
  ```
24
29
 
25
- ## Table of Contents
26
-
27
- - [API Reference](#api-reference)
28
- - [`createTerminalApp()`](#createterminalapp)
29
- - [`createCommand()`](#createcommand)
30
- - [`command.define(command)`](#commanddefinecommand)
31
- - [`command.registry(commands)`](#commandregistrycommands)
32
- - [`command.run(command, args, context)`](#commandruncommand-args-context)
33
- - [`createCommands(commands)`](#createcommandscommands)
34
- - [`commands.prepare(args, options?)`](#commandsprepareargs-options)
35
- - [`commands.run(prepared, context)`](#commandsrunprepared-context)
36
- - [`commands.runFromArgs(args, context, options?)`](#commandsrunfromargsargs-context-options)
37
- - [`createPresentation()`](#createpresentation)
38
- - [`presentation.empty()`](#presentationempty)
39
- - [`presentation.text(value)`](#presentationtextvalue)
40
- - [`presentation.record(value)`](#presentationrecordvalue)
41
- - [`presentation.records(values)`](#presentationrecordsvalues)
42
- - [`presentation.table(rows)`](#presentationtablerows)
43
- - [`presentation.csv(rows)`](#presentationcsvrows)
44
- - [`presentation.render(result, format?)`](#presentationrenderresult-format)
45
- - [`presentation.renderers.*`](#presentationrenderers)
46
- - [`createOutput()`](#createoutput)
47
- - [`output.write(chunk)`](#outputwritechunk)
48
- - [`output.error(chunk)`](#outputerrorchunk)
49
- - [`output.stdout.write(chunk)`](#outputstdoutwritechunk)
50
- - [`output.stderr.write(chunk)`](#outputstderrwritechunk)
51
- - [Lower-Level Mechanics](#lower-level-mechanics)
52
- - [`parseArgv(args, schema?)`](#parseargv)
53
- - [`parseOptionsDetailed(schema, values)`](#parseoptionsdetailed)
54
- - [`resolveCommand(registry, positionals)`](#resolvecommand)
55
- - [`resolveCommandFromArgs(registry, args)`](#resolvecommandfromargs)
56
- - [`prepareCommandFromArgs(registry, args, options?)`](#preparecommandfromargs)
57
- - [`runPreparedCommand(prepared, context)`](#runpreparedcommand)
58
- - [`runCommandFromRegistry(registry, args, context, options?)`](#runcommandfromregistry)
59
- - [`runCommand(command, args, context, options?)`](#runcommand)
60
- - [Internal Source Layout](#internal-source-layout)
61
- - [How It Works](#how-it-works)
62
- - [Example](#example)
63
- - [Option Schemas](#option-schemas)
64
- - [Type Inference](#type-inference)
65
- - [Facade of arguments](#facade-of-arguments)
66
- - [Error Messages](#error-messages)
67
- - [Project Boundary](#project-boundary)
68
-
69
- ## API Reference
70
-
71
- Start at `createTerminalApp()` for regular terminal applications, then move to
72
- the construction methods only when the app needs explicit command,
73
- presentation, or output wiring.
74
-
75
- ### `createTerminalApp()`
76
-
77
- `createTerminalApp()` is the top-level terminal application composition point.
78
- It wires command execution, presentation rendering, and stdout/stderr delivery
79
- without taking ownership of application behavior.
80
-
81
- The checked TypeScript contract lives in [`src/terminal/app.ts`](src/terminal/app.ts).
30
+ Import public APIs from `icore`. Deep imports into `dist` or `src` are not part
31
+ of the package contract.
82
32
 
83
- The method returns an application object with `prepare(args)`,
84
- `writePreparedOutput(prepared, output)`, `runPrepared(prepared, context)`, and
85
- `run(args, context)`.
33
+ ## Contents
86
34
 
87
- Construction inputs:
88
-
89
- - `commands` is required; build it with [`createCommand()`](#createcommand) and
90
- [`command.registry(commands)`](#commandregistrycommands), or directly with
91
- [`createCommands(commands)`](#createcommandscommands);
92
- - `presentation` is optional; omit it to use [`createPresentation()`](#createpresentation);
93
- - `output` is optional; omit it to use [`createOutput()`](#createoutput);
94
- - `resolveFormat` is optional and customizes how a prepared command selects a
95
- presentation format.
96
-
97
- Returned app methods:
35
+ - [Quick Start](#quick-start)
36
+ - [Choose An API Level](#choose-an-api-level)
37
+ - [Supported Argument Syntax](#supported-argument-syntax)
38
+ - [Error Handling](#error-handling)
39
+ - [Guides](#guides)
40
+ - [Project Boundary](#project-boundary)
98
41
 
99
- - `app.prepare(args, options?)` delegates to
100
- [`commands.prepare(args, options?)`](#commandsprepareargs-options);
101
- - `app.writePreparedOutput(prepared, output)` renders and writes already
102
- obtained terminal output without running the command;
103
- - `app.runPrepared(prepared, context)` runs an already prepared command, then
104
- renders and writes terminal output;
105
- - `app.run(args, context, options?)` prepares the command, delegates command
106
- execution to [`commands.run(prepared, context)`](#commandsrunprepared-context),
107
- then renders and writes terminal output.
42
+ ## Quick Start
108
43
 
109
- Minimal shape:
44
+ Create commands, put them in a registry, and give that registry to
45
+ `createTerminalApp()`:
110
46
 
111
47
  ```ts
112
- const app = createTerminalApp({
113
- commands,
114
- presentation,
115
- output
116
- });
48
+ import {
49
+ createCommand,
50
+ createTerminalApp
51
+ } from 'icore';
117
52
 
118
- const args = process.argv.slice(2);
119
- const exitCode = await app.run(args, context, {
120
- strict: true
121
- });
122
- ```
123
-
124
- Command handlers keep ownership of application work. Terminal output methods
125
- accept terminal-ready results: text, streaming text, presentation results, or no
126
- output. If a command returns an application lifecycle object, run it with
127
- `app.commands.run(...)`, inspect or map the result, then pass only terminal
128
- output to `app.writePreparedOutput(...)`. Application DTO mapping, config
129
- loading, network clients, and domain behavior stay in the consuming application.
130
-
131
- String output is written exactly as provided. Return `\n` from the command when
132
- line output is desired.
133
-
134
- ### `createCommand()`
135
-
136
- `createCommand()` returns the command mechanics entrypoint used to build
137
- `commands` for `createTerminalApp()`.
138
-
139
- Simplified shape:
140
-
141
- ```ts
142
53
  const command = createCommand();
143
54
 
144
- command.define(commandDefinition);
145
- command.registry(commandDefinitions);
146
- command.run(commandDefinition, args, context);
147
- ```
148
-
149
- Build `commands` before creating the terminal app:
150
-
151
- - `createCommand()` creates the command mechanics entrypoint;
152
- - `command.define(...)` declares one command;
153
- - `command.registry([...])` returns the `commands` object required by
154
- `createTerminalApp()`.
155
-
156
- The command definition keeps the application-specific parts: command path,
157
- option schema, and handler behavior. The terminal app only needs the resulting
158
- registry object.
159
-
160
- #### `command.define(command)`
161
-
162
- Declares one command and preserves its literal path and option schema types.
163
- Use it when defining commands inline before adding them to a registry.
164
-
165
- #### `command.registry(commands)`
166
-
167
- Builds the `commands` object required by `createTerminalApp()`. It keeps the
168
- registered command definitions, derived command names, and command flow methods
169
- together.
170
-
171
- #### `command.run(command, args, context)`
172
-
173
- Runs a single command without a registry. Use it for focused command execution,
174
- small tests, or custom flows where command path resolution is not needed.
175
-
176
- #### `createCommands(commands)`
177
-
178
- Use `createCommands(commands)` when the application already has command
179
- definitions and does not need the `createCommand()` object form.
180
-
181
- The returned object is the same `commands` contract consumed by
182
- `createTerminalApp()`: it can resolve, prepare, and run registered commands.
183
-
184
- Simplified shape:
185
-
186
- ```ts
187
- const commands = createCommands(commandDefinitions);
188
-
189
- commands.names;
190
- commands.registry;
191
- commands.resolve(positionals);
192
- commands.resolveFromArgs(args);
193
- commands.prepare(args, options);
194
- commands.run(prepared, context);
195
- commands.runFromArgs(args, context, options);
196
- ```
197
-
198
- Use `commands.prepare(...)` and `commands.run(...)` when application code needs
199
- the same two-phase flow used by `createTerminalApp()`. Use
200
- `commands.runFromArgs(...)` for custom terminal boundaries that still want the
201
- registry-level command flow.
202
-
203
- #### `commands.prepare(args, options?)`
204
-
205
- Resolves and validates a registered command without runtime context. This is
206
- the same preparation step used by `app.prepare(...)` and `app.run(...)`.
207
-
208
- #### `commands.run(prepared, context)`
209
-
210
- Runs an already prepared command with application context. Use it after
211
- `commands.prepare(...)` when the application needs a custom two-phase flow.
212
-
213
- #### `commands.runFromArgs(args, context, options?)`
214
-
215
- Resolves, prepares, and runs a registered command without the terminal
216
- application boundary. Use it when a custom boundary owns presentation or output.
217
-
218
- ### `createPresentation()`
219
-
220
- `createPresentation()` creates the presentation object used by
221
- `createTerminalApp()` to render terminal-ready results.
222
-
223
- Source of truth:
224
-
225
- - [src/presentation/facade.ts](src/presentation/facade.ts) for the presentation
226
- object;
227
- - [src/presentation/view.ts](src/presentation/view.ts) for presentation view
228
- factories;
229
- - [src/presentation/result-renderer.ts](src/presentation/result-renderer.ts)
230
- for rendering presentation results.
231
-
232
- Simplified shape:
233
-
234
- ```ts
235
- const presentation = createPresentation();
236
-
237
- presentation.empty();
238
- presentation.text(value);
239
- presentation.record(value);
240
- presentation.records(values);
241
- presentation.table(rows);
242
- presentation.csv(rows);
243
- presentation.render(result, format);
244
- ```
245
-
246
- The presentation object owns generic JSON, CSV, and table rendering mechanics.
247
- Application code still maps domain objects to presentation-ready values.
248
-
249
- #### `presentation.empty()`
250
-
251
- Creates a presentation result with no terminal output.
252
-
253
- #### `presentation.text(value)`
254
-
255
- Wraps ready terminal text. Use it when the command already owns the final text.
256
-
257
- #### `presentation.record(value)`
258
-
259
- Creates a generic one-record view. The renderer can print it as table, CSV, or
260
- JSON depending on the selected format.
261
-
262
- #### `presentation.records(values)`
263
-
264
- Creates a generic multi-record view. Application code still decides which fields
265
- belong in each record.
266
-
267
- #### `presentation.table(rows)`
268
-
269
- Creates an explicit table view from prepared text rows.
270
-
271
- #### `presentation.csv(rows)`
272
-
273
- Creates an explicit CSV view from scalar rows.
274
-
275
- #### `presentation.render(result, format?)`
276
-
277
- Renders a presentation result to terminal text. The default format is `table`.
278
-
279
- #### `presentation.renderers.*`
280
-
281
- Exposes lower-level JSON, table, and CSV renderers for custom presentation
282
- composition. Prefer `presentation.render(...)` for regular terminal commands.
283
-
284
- ### `createOutput()`
285
-
286
- `createOutput()` creates the output object used by `createTerminalApp()` to
287
- write rendered text.
288
-
289
- Source of truth:
290
-
291
- - [src/output/facade.ts](src/output/facade.ts) for the output object;
292
- - [src/output/node-writer.ts](src/output/node-writer.ts) for stdout and stderr
293
- writers;
294
- - [src/output/text-writer.ts](src/output/text-writer.ts) for backpressure-aware
295
- text writing.
296
-
297
- Simplified shape:
298
-
299
- ```ts
300
- const output = createOutput();
301
-
302
- await output.write(chunk);
303
- await output.error(chunk);
304
- await output.stdout.write(chunk);
305
- await output.stderr.write(chunk);
306
- ```
307
-
308
- The default output writes regular text to `stdout` and diagnostic text to
309
- `stderr`. Pass custom sinks when tests or applications need controlled output.
310
-
311
- #### `output.write(chunk)`
312
-
313
- Writes regular terminal output through stdout.
314
-
315
- #### `output.error(chunk)`
316
-
317
- Writes diagnostic terminal output through stderr.
318
-
319
- #### `output.stdout.write(chunk)`
320
-
321
- Writes directly to the stdout channel. Use this only when a specific channel
322
- must be passed around.
323
-
324
- #### `output.stderr.write(chunk)`
325
-
326
- Writes directly to the stderr channel. Use this only when a specific channel
327
- must be passed around.
328
-
329
- ### Lower-Level Mechanics
330
-
331
- Lower-level mechanics sit behind the construction methods. They are kept out of
332
- the main navigation to keep the application construction path first.
333
-
334
- For application code, prefer `createTerminalApp()` first. Drop to this level
335
- only when the terminal application flow or construction methods are too coarse.
336
-
337
- Source of truth:
338
-
339
- - [src/argv/parser.ts](src/argv/parser.ts) for raw argv parsing;
340
- - [src/options/parser.ts](src/options/parser.ts) for option value validation;
341
- - [src/command/mechanics.ts](src/command/mechanics.ts) for command resolution,
342
- preparation, and execution.
343
-
344
- #### Parsing And Resolution
345
-
346
- This group turns raw arguments or positionals into a selected command.
347
-
348
- - <a id="parseargv"></a>`parseArgv(args, schema?)` parses raw CLI arguments into positionals and raw
349
- option values;
350
- - <a id="resolvecommand"></a>`resolveCommand(registry, positionals)` resolves from already parsed
351
- positionals;
352
- - <a id="resolvecommandfromargs"></a>`resolveCommandFromArgs(registry, args)` resolves from raw CLI arguments by
353
- using registered command schemas.
354
-
355
- Use these methods when command selection is needed without validation or
356
- execution.
357
-
358
- #### Preparation
359
-
360
- This group validates command input without runtime context.
361
-
362
- - <a id="parseoptionsdetailed"></a>`parseOptionsDetailed(schema, values)` validates raw option values and keeps
363
- option presence metadata;
364
- - <a id="preparecommandfromargs"></a>`prepareCommandFromArgs(registry, args, options?)` resolves and validates a
365
- registered command;
366
- - `options.strict` rejects options before the command path.
367
-
368
- Use preparation when runtime resources should be created only after command
369
- selection is known.
370
-
371
- #### Execution
372
-
373
- This group calls command handlers after command input is prepared.
374
-
375
- - <a id="runpreparedcommand"></a>`runPreparedCommand(prepared, context)` runs an already prepared command;
376
- - <a id="runcommandfromregistry"></a>`runCommandFromRegistry(registry, args, context, options?)` prepares and runs
377
- a command from a registry;
378
- - <a id="runcommand"></a>`runCommand(command, args, context, options?)` runs a single command without a
379
- registry.
380
-
381
- Use execution primitives for custom boundaries. Regular CLI applications should
382
- usually stay at `createTerminalApp()`.
383
-
384
- ## Internal Source Layout
385
-
386
- The public package entrypoint remains `src/index.ts`. Internal source files are
387
- grouped by CLI framework responsibility:
388
-
389
- ```text
390
- src/
391
- argv/ raw argv token parsing
392
- options/ option schemas and option value validation
393
- command/ command resolution and execution mechanics
394
- presentation/ terminal view models and JSON/CSV/table renderers
395
- output/ stdout/stderr writer boundaries
396
- terminal/ command + presentation + output app composition
397
- errors/ machine-readable icore errors
398
- ```
399
-
400
- Consumers should continue importing from `icore`; deep imports are an internal
401
- source layout detail.
402
-
403
- ## How It Works
404
-
405
- ![yuml diagram](http://yuml.me/diagram/scruffy;dir:LR;/class/[*argv*%20{bg:gray}|External;hello%20--name%20Alice%20--uppercase]->[*matches*%20{bg:lavender}|System;parse,%20resolve,%20validate,%20infer]->[*typed%20result*%20{bg:honeydew}|Container;command=hello;%20name=Alice;%20uppercase=true]->[*your%20app*%20{bg:cornsilk}|System;business%20logic%20and%20output])
406
-
407
- ## Example
408
-
409
- More examples live in [examples/readme.md](examples/readme.md), including
410
- option schemas, CLI argument syntax, practical CLI patterns, and lower-level
411
- primitives.
412
-
413
- ```ts
414
- import { defineCommand, runCommand } from 'icore';
415
-
416
- const exampleCommand = defineCommand({
417
- path: ['hello'],
418
- options: {
419
- name: {
420
- type: 'string',
421
- default: 'world'
55
+ const commands = command.registry([
56
+ command.define({
57
+ path: ['hello'],
58
+ options: {
59
+ name: {
60
+ type: 'string',
61
+ default: 'world'
62
+ },
63
+ uppercase: {
64
+ type: 'boolean'
65
+ }
422
66
  },
423
- uppercase: {
424
- type: 'boolean'
67
+ handle({ options }) {
68
+ const greeting = `Hello, ${options.name}!`;
69
+
70
+ return `${options.uppercase ? greeting.toUpperCase() : greeting}\n`;
425
71
  }
426
- },
427
- async handle({ options }) {
428
- const message = `Hello, ${options.name}!`;
72
+ })
73
+ ] as const);
429
74
 
430
- return options.uppercase ? message.toUpperCase() : message;
431
- }
432
- });
75
+ const app = createTerminalApp({ commands });
433
76
 
434
- const output = await runCommand(
435
- exampleCommand,
436
- ['hello', '--name', 'Alice', '--uppercase'],
437
- {}
438
- );
77
+ async function main(args: readonly string[]): Promise<void> {
78
+ process.exitCode = await app.run(args, undefined, {
79
+ strict: true
80
+ });
81
+ }
439
82
 
440
- console.log(output);
83
+ void main(process.argv.slice(2));
441
84
  ```
442
85
 
443
- Terminal output:
86
+ After compiling the application:
444
87
 
445
88
  ```console
446
- $ node cli.js hello --name Alice --uppercase
447
- HELLO, ALICE!
448
- ```
449
-
450
- The command handler receives parsed options, user-provided option metadata,
451
- remaining positionals, prepared payload, and caller provided context.
452
-
453
- ## Option Schemas
454
-
455
- Options are described as plain objects.
456
-
457
- **Option names are exact.** `icore` does not normalize `camelCase` to
458
- `kebab-case`. Use quoted object keys when your public CLI option contains
459
- `-`.
460
-
461
- Each option can define an optional short alias:
462
-
463
- ```ts
464
- const schema = {
465
- name: {
466
- type: 'string',
467
- alias: 'n'
468
- },
469
- uppercase: {
470
- type: 'boolean',
471
- alias: 'u'
472
- }
473
- } as const;
474
- ```
475
-
476
- Aliases must be a single ASCII letter and unique within the schema. Parsed
477
- values are always returned by long option name.
478
-
479
- ### `type: 'string'`
480
-
481
- ```ts
482
- const schema = {
483
- name: {
484
- type: 'string',
485
- required: true
486
- },
487
- style: {
488
- type: 'string',
489
- choices: ['short', 'long'],
490
- default: 'short'
491
- }
492
- } as const;
493
- ```
494
-
495
- String options reject missing required values, blank strings such as `--name=`,
496
- boolean flag form, and values outside `choices`.
497
-
498
- ### `type: 'boolean'`
499
-
500
- ```ts
501
- const schema = {
502
- uppercase: {
503
- type: 'boolean'
504
- }
505
- } as const;
506
- ```
89
+ $ node dist/cli.js hello
90
+ Hello, world!
507
91
 
508
- Boolean options accept **flag form** and schema-known negation:
509
-
510
- ```sh
511
- --uppercase
512
- --no-uppercase
513
- ```
514
-
515
- Explicit values are rejected:
516
-
517
- ```sh
518
- --uppercase=true
519
- --uppercase=false
520
- --uppercase=yes
521
- --uppercase=
522
- ```
523
-
524
- `--uppercase false` keeps `--uppercase` as `true` and leaves `false` as a positional
525
- argument.
526
-
527
- Use `syntax: 'flag'` when a boolean option should accept only flag form:
528
-
529
- ```ts
530
- const schema = {
531
- uppercase: {
532
- type: 'boolean',
533
- default: false,
534
- syntax: 'flag'
535
- }
536
- } as const;
92
+ $ node dist/cli.js hello --name Alice --uppercase
93
+ HELLO, ALICE!
537
94
  ```
538
95
 
539
- With `syntax: 'flag'`, `--uppercase` is accepted, while `--uppercase=value`
540
- and `--no-uppercase` are rejected.
96
+ The schema determines the handler's option types. Required options and options
97
+ with defaults are always present; optional options are returned as
98
+ `T | undefined`.
541
99
 
542
- ### `type: 'number'`
100
+ `strict: true` rejects options placed before the command path. String output is
101
+ written exactly as returned, so include `\n` when line output is intended.
543
102
 
544
- ```ts
545
- const schema = {
546
- limit: {
547
- type: 'number',
548
- integer: true,
549
- min: 1,
550
- max: 1000,
551
- default: 100
552
- }
553
- } as const;
554
- ```
103
+ ## Choose An API Level
555
104
 
556
- Number options parse decimal numeric values and can validate integer and range
557
- constraints. Defaults are validated with the same rules as user-provided values.
105
+ Start with the highest-level API that fits the application:
558
106
 
559
- ## Type Inference
107
+ | Need | Start with | Detailed guide |
108
+ | --- | --- | --- |
109
+ | Complete terminal application | `createTerminalApp()` | [Terminal App](examples/terminal-app.md) |
110
+ | Commands and option schemas | `createCommand()` or `createCommands()` | [Option Schemas](examples/option-schemas.md) |
111
+ | Custom prepare/execute lifecycle | `commands.prepare()` and `commands.run()` | [Custom Command Flow](examples/custom-command-flow.md) |
112
+ | Presentation without command execution | `createPresentation()` | [Presentation And Output](examples/presentation-output.md) |
113
+ | Explicit stdout/stderr writing | `createOutput()` | [Output Writers](examples/output-writers.md) |
114
+ | Parser and resolver primitives | `parseArgv()`, `resolveCommand()`, and related exports | [Primitive Mechanics](examples/readme.md#primitive-mechanics) |
560
115
 
561
- Use `InferOptions` when you need the parsed option type explicitly.
116
+ The terminal application exposes the main lifecycle operations:
562
117
 
563
- ```ts
564
- import type { InferOptions } from 'icore';
565
-
566
- const schema = {
567
- name: {
568
- type: 'string',
569
- default: 'world'
570
- },
571
- uppercase: {
572
- type: 'boolean'
573
- }
574
- } as const;
118
+ - `app.run(args, context, options?)` prepares, executes, renders, and writes;
119
+ - `app.prepare(args, options?)` validates input without runtime context;
120
+ - `app.runPrepared(prepared, context)` executes an already prepared command;
121
+ - `app.writePreparedOutput(prepared, output)` writes caller-obtained output;
122
+ - `app.reportError(error, context?)` applies the configured terminal error
123
+ policy and returns a process-style exit code.
575
124
 
576
- type Options = InferOptions<typeof schema>;
577
- ```
125
+ Command handlers receive parsed `options`, option-presence metadata in
126
+ `provided`, remaining `positionals`, caller-owned `context`, and an optional
127
+ prepared `payload`.
578
128
 
579
- `Options` is equivalent to:
129
+ Supported terminal results are strings, async string streams, presentation
130
+ results, and `undefined`:
580
131
 
581
- ```ts
582
- type Options = {
583
- name: string;
584
- uppercase: boolean | undefined;
585
- };
132
+ ```text
133
+ argv resolve → validate/prepare → execute → render → stdout/stderr
586
134
  ```
587
135
 
588
- **Required options and options with defaults are always present.** Optional
589
- options without defaults are returned as `T | undefined`.
590
-
591
- Use `InferProvidedOptions` when you need the option presence type explicitly.
592
-
593
- ```ts
594
- import type { InferProvidedOptions } from 'icore';
136
+ Exact public exports live in [`src/index.ts`](src/index.ts) and in the bundled
137
+ TypeScript declarations.
595
138
 
596
- type Provided = InferProvidedOptions<typeof schema>;
597
- ```
139
+ ## Supported Argument Syntax
598
140
 
599
- `Provided` maps every schema option to `boolean`. `true` means the user
600
- specified that option explicitly; defaults keep the flag `false`.
141
+ The supported syntax is intentionally small and predictable:
601
142
 
602
- Use `MergeOptionsSchemas` when you need the merged schema type explicitly.
143
+ | Form | Example | Notes |
144
+ | --- | --- | --- |
145
+ | Long option with separate value | `--name Alice` | String and number options consume a value |
146
+ | Long option with attached value | `--name=Alice` | Equivalent to the separate form |
147
+ | Boolean flag | `--verbose` | Produces `true` |
148
+ | Boolean negation | `--no-cache` | Supported for known boolean options unless `syntax: 'flag'` |
149
+ | Short alias | `-v`, `-n Alice` | Must be declared in the schema as one ASCII letter |
150
+ | Option terminator | `--` | Every following token becomes positional |
603
151
 
604
- ```ts
605
- import type { MergeOptionsSchemas } from 'icore';
152
+ Option names are exact: `icore` does not convert `camelCase` to `kebab-case`.
153
+ Explicit boolean values such as `--verbose=true`, attached short values such as
154
+ `-nAlice`, and grouped aliases such as `-abc` are not supported.
606
155
 
607
- type Schema = MergeOptionsSchemas<[typeof nameOptions, typeof greetingOptions]>;
608
- ```
156
+ See [CLI Argument Syntax](examples/cli-argument-syntax.md) for parsing examples,
157
+ edge cases, duplicate handling, and the option terminator contract.
609
158
 
610
- Use `CommandName` when you need the inferred command name type explicitly.
159
+ ## Error Handling
611
160
 
612
- ```ts
613
- import type { CommandName } from 'icore';
161
+ CLI parsing, validation, resolution, and definition failures are reported as
162
+ `IcoreError`. It extends `Error` with:
614
163
 
615
- type Name = CommandName<typeof helloFormalCommand>;
616
- ```
164
+ - a stable machine-readable `code`;
165
+ - a `usage` or `definition` category;
166
+ - required, code-specific `details`.
617
167
 
618
- `Name` is equivalent to:
168
+ Use `isIcoreError(...)` instead of manually casting details. Passing a code to
169
+ the guard narrows the corresponding details shape:
619
170
 
620
171
  ```ts
621
- type Name = 'hello formal';
622
- ```
172
+ import {
173
+ createTerminalApp,
174
+ isIcoreError
175
+ } from 'icore';
623
176
 
624
- ## Facade of arguments
177
+ const help = 'Usage: cli hello [--name value]';
625
178
 
626
- Use `--` to stop option parsing. The terminator itself is not included in
627
- positionals; every following token is treated as positional, even when it starts
628
- with `-`.
629
-
630
- Short syntax is supported only for aliases declared in the option schema.
631
- Boolean aliases use flag form, such as `-f`; string and number aliases use a
632
- separate value, such as `-n value`.
633
-
634
- Attached short values such as `-nvalue` and grouped short booleans such as
635
- `-abc` are not supported yet. Unknown short tokens remain positional for
636
- compatibility.
637
-
638
- Negated syntax such as `--no-cache` is interpreted as `cache: false` when
639
- `cache` is a known boolean option without `syntax: 'flag'`. Unknown negated
640
- options, negation for string or number options, and negation for flag-only
641
- boolean options are rejected.
642
-
643
- ## Error Messages
644
-
645
- `icore` throws `IcoreError` objects for CLI parsing, option validation, and
646
- command resolution failures. `IcoreError` extends the regular `Error` class and
647
- adds a stable machine-readable `code` plus structured `details`.
179
+ const app = createTerminalApp({
180
+ commands,
181
+ errorPolicy: {
182
+ renderError(error) {
183
+ const message = error instanceof Error
184
+ ? error.message
185
+ : String(error);
648
186
 
649
- Applications should treat `error.message` as **display text**. Use `error.code`
650
- for machine-readable handling:
187
+ if (isIcoreError(error, 'UNKNOWN_COMMAND')) {
188
+ return `${message}\n\n${help}\n`;
189
+ }
651
190
 
652
- ```ts
653
- import { IcoreError } from 'icore';
654
-
655
- try {
656
- await main(args);
657
- } catch (error) {
658
- if (error instanceof IcoreError && error.code === 'UNKNOWN_COMMAND') {
659
- printHelp();
660
- process.exitCode = 2;
661
- return;
191
+ return `${message}\n`;
192
+ },
193
+ resolveExitCode(error) {
194
+ return isIcoreError(error) && error.category === 'usage'
195
+ ? 2
196
+ : 1;
197
+ }
662
198
  }
663
-
664
- throw error;
665
- }
199
+ });
666
200
  ```
667
201
 
668
- Supported error codes:
202
+ Inside the `UNKNOWN_COMMAND` branch, fields such as `error.details.command` and
203
+ `error.details.positionals` are strongly typed. `IcoreErrorDetailsMap` is the
204
+ public source of truth for every code. Direct `new IcoreError(...)` calls must
205
+ provide a third argument matching the selected code.
669
206
 
670
- ```ts
671
- type IcoreErrorCode =
672
- | 'UNKNOWN_COMMAND'
673
- | 'UNEXPECTED_ARGUMENT'
674
- | 'DUPLICATE_ARGUMENT'
675
- | 'EXPECTED_REQUIRED_ARGUMENT'
676
- | 'INVALID_OPTION_TYPE'
677
- | 'INVALID_OPTION_CHOICE'
678
- | 'UNEXPECTED_POSITIONAL'
679
- | 'INVALID_OPTION_ALIAS'
680
- | 'DUPLICATE_ALIAS'
681
- | 'INVALID_OPTION_DEFAULT'
682
- | 'DUPLICATE_COMMAND';
683
- ```
207
+ Without a custom policy, terminal apps write `Error.message + "\n"` (or
208
+ `String(error) + "\n"` for other thrown values) and return exit code `1`.
209
+ Application-specific help remains application policy.
684
210
 
685
- Applications can catch these errors and decide how to print them. For example,
686
- after printing `error.message`, terminal output can look like this:
211
+ Custom lifecycles can call `app.reportError(...)` to reuse the same rendering
212
+ and exit-code policy. The complete prepare, execute, write, and external phase
213
+ flow is shown in the
214
+ [Terminal App guide](examples/terminal-app.md#reuse-error-reporting-in-a-custom-lifecycle).
687
215
 
688
- ```console
689
- $ node cli.js hello --unknown
690
- Unexpected argument '--unknown'
216
+ ## Guides
691
217
 
692
- $ node cli.js hello --uppercase=yes
693
- Expected '--uppercase' as boolean flag
218
+ The [examples index](examples/readme.md) routes from regular terminal apps to
219
+ lower-level mechanics. Useful starting points:
694
220
 
695
- $ node cli.js hello --name=
696
- Expected '--name' as string
697
- ```
221
+ - [Terminal App](examples/terminal-app.md) regular application composition,
222
+ format resolution, and reusable error reporting;
223
+ - [Option Schemas](examples/option-schemas.md) — strings, booleans, numbers,
224
+ choices, defaults, aliases, and inferred types;
225
+ - [Practical CLI Patterns](examples/practical-cli-patterns.md) — help/version
226
+ shortcuts, shared options, and compatibility aliases;
227
+ - [Command Resolution](examples/command-resolution.md) — registries, explicit
228
+ resolution, and command-name guards;
229
+ - [Two-Phase Primitives](examples/two-phase-primitives.md) — preparation,
230
+ payloads, execution, and provided-option metadata;
231
+ - [Presentation Primitives](examples/presentation-primitives.md) — text, record,
232
+ table, CSV, JSON, and direct renderers.
698
233
 
699
- Errors thrown by command `prepare` and `handle` functions are application errors
700
- and pass through unchanged.
234
+ Release history is recorded in [CHANGELOG.md](CHANGELOG.md). Directional
235
+ decisions live in [docs/roadmap.md](docs/roadmap.md).
701
236
 
702
237
  ## Project Boundary
703
238
 
704
- `icore` is intended to be a **small CLI mechanics module**. It should **not**
705
- grow into a domain-specific framework for a particular SDK or API.
706
-
707
- Good responsibilities for `icore`:
239
+ `icore` is a small terminal mechanics module. It owns generic behavior such as
240
+ option validation, command resolution, typed handler input, presentation
241
+ rendering, error contracts, and stdout/stderr delivery.
708
242
 
709
- - option schema evaluation;
710
- - command path checking;
711
- - common argument errors;
712
- - typed command handler input;
713
- - generic JSON, CSV, and text table rendering;
714
- - stdout/stderr text writer mechanics.
243
+ It does not own application DTO mapping, API calls, configuration loading,
244
+ domain-specific validation, lifecycle resources, or help content. Keep those
245
+ responsibilities in the consuming application.
715
246
 
716
- Application-specific report mapping, scalar formatting, API calls, config
717
- loading, and domain behavior stay outside `icore`.
247
+ Licensed under the [MIT License](LICENSE).