icore 1.0.14 → 1.0.16

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/{argv.d.ts → argv/parser.d.ts} +3 -1
  3. package/dist/{argv.js → argv/parser.js} +5 -5
  4. package/dist/{commands.d.ts → command/mechanics.d.ts} +79 -6
  5. package/dist/{commands.js → command/mechanics.js} +60 -15
  6. package/dist/{errors.d.ts → errors/icore-error.d.ts} +2 -0
  7. package/dist/{errors.js → errors/icore-error.js} +2 -0
  8. package/dist/index.d.ts +28 -1
  9. package/dist/index.js +61 -16
  10. package/dist/options/parser.d.ts +43 -0
  11. package/dist/{options.js → options/parser.js} +27 -17
  12. package/dist/{options.d.ts → options/schema.d.ts} +16 -23
  13. package/dist/options/schema.js +23 -0
  14. package/dist/output/facade.d.ts +28 -0
  15. package/dist/output/facade.js +32 -0
  16. package/dist/output/node-writer.d.ts +17 -0
  17. package/dist/output/node-writer.js +25 -0
  18. package/dist/output/text-writer.d.ts +29 -0
  19. package/dist/output/text-writer.js +49 -0
  20. package/dist/presentation/facade.d.ts +39 -0
  21. package/dist/presentation/facade.js +47 -0
  22. package/dist/presentation/format-options.d.ts +31 -0
  23. package/dist/presentation/format-options.js +37 -0
  24. package/dist/presentation/renderers/csv.d.ts +18 -0
  25. package/dist/presentation/renderers/csv.js +35 -0
  26. package/dist/presentation/renderers/json.d.ts +13 -0
  27. package/dist/presentation/renderers/json.js +18 -0
  28. package/dist/presentation/renderers/table.d.ts +14 -0
  29. package/dist/presentation/renderers/table.js +29 -0
  30. package/dist/presentation/result-renderer.d.ts +25 -0
  31. package/dist/presentation/result-renderer.js +184 -0
  32. package/dist/presentation/view.d.ts +58 -0
  33. package/dist/presentation/view.js +56 -0
  34. package/dist/terminal/app.d.ts +51 -0
  35. package/dist/terminal/app.js +100 -0
  36. package/examples/cli-argument-syntax.md +218 -0
  37. package/examples/command-resolution.md +174 -0
  38. package/examples/custom-command-flow.md +109 -0
  39. package/examples/option-schemas.md +206 -0
  40. package/examples/output-writers.md +128 -0
  41. package/examples/practical-cli-patterns.md +385 -0
  42. package/examples/presentation-output.md +66 -0
  43. package/examples/presentation-primitives.md +190 -0
  44. package/examples/readme.md +48 -0
  45. package/examples/terminal-app.md +118 -0
  46. package/examples/two-phase-primitives.md +282 -0
  47. package/package.json +9 -3
  48. package/readme.md +281 -290
  49. package/dist/cli.d.ts +0 -14
  50. package/dist/cli.js +0 -31
@@ -0,0 +1,190 @@
1
+ # Presentation Primitives
2
+
3
+ `createPresentation()` is the preferred entrypoint for terminal rendering. The
4
+ primitives below are useful when a command needs explicit view types or when an
5
+ adapter wants to use a renderer directly.
6
+
7
+ Prefer `presentation.render(...)` first. Direct renderers are lower-level and
8
+ make the caller responsible for choosing the right input shape.
9
+
10
+ ## Empty And Text Views
11
+
12
+ Use `presentation.empty()` when a command succeeds without terminal output.
13
+ Use `presentation.text(...)` when the command already owns final text.
14
+
15
+ ```ts
16
+ import { createPresentation } from 'icore';
17
+
18
+ const presentation = createPresentation();
19
+
20
+ const noOutput = presentation.empty();
21
+ const readyText = presentation.text('cache cleared\n');
22
+ ```
23
+
24
+ These views are format-independent. Rendering them as `table`, `json`, or `csv`
25
+ does not invent structure.
26
+
27
+ ## Record And Records Views
28
+
29
+ Use `record(...)` and `records(...)` when application data can be represented as
30
+ generic key/value objects.
31
+
32
+ ```ts
33
+ const oneJob = presentation.record({
34
+ id: 'job-42',
35
+ status: 'running'
36
+ });
37
+
38
+ const manyJobs = presentation.records([
39
+ {
40
+ id: 'job-41',
41
+ status: 'done'
42
+ },
43
+ {
44
+ id: 'job-42',
45
+ status: 'running'
46
+ }
47
+ ]);
48
+
49
+ presentation.render(oneJob, 'table');
50
+ presentation.render(manyJobs, 'json');
51
+ ```
52
+
53
+ This is the most convenient view for regular reports. The tradeoff is that
54
+ field selection has to happen before the view is created; generic presentation
55
+ code should not inspect domain objects.
56
+
57
+ ## Explicit Table And CSV Views
58
+
59
+ Use `table(...)` when the application already owns prepared text cells.
60
+ Use `csv(...)` when the application already owns CSV scalar rows.
61
+
62
+ ```ts
63
+ const table = presentation.table([
64
+ ['id', 'status'],
65
+ ['job-41', 'done'],
66
+ ['job-42', 'running']
67
+ ]);
68
+
69
+ const csv = presentation.csv([
70
+ ['id', 'status'],
71
+ ['job-41', 'done'],
72
+ ['job-42', 'running']
73
+ ]);
74
+ ```
75
+
76
+ These forms are less convenient than `records(...)`, but they are useful when
77
+ the application needs full control over column sequence and cell values.
78
+
79
+ ## Supported Formats
80
+
81
+ Use `presentation.formats`, `presentationFormats`, and
82
+ `isPresentationFormat(...)` when a custom boundary accepts a format option.
83
+
84
+ ```ts
85
+ import {
86
+ isPresentationFormat,
87
+ presentationFormats
88
+ } from 'icore';
89
+
90
+ function normalizeFormat(value: unknown) {
91
+ return isPresentationFormat(value) ? value : 'table';
92
+ }
93
+
94
+ presentation.formats;
95
+ presentationFormats;
96
+ ```
97
+
98
+ This keeps format checks aligned with the renderer. Avoid copying the string
99
+ union into application code.
100
+
101
+ ## Render Through The Facade
102
+
103
+ `presentation.render(...)` delegates to `renderPresentationResult(...)`.
104
+
105
+ ```ts
106
+ import { renderPresentationResult } from 'icore';
107
+
108
+ const view = presentation.records([
109
+ {
110
+ id: 'job-42',
111
+ status: 'running'
112
+ }
113
+ ]);
114
+
115
+ const fromFacade = presentation.render(view, 'csv');
116
+ const fromPrimitive = renderPresentationResult(view, 'csv');
117
+ ```
118
+
119
+ Prefer the facade in application code. The standalone function is useful for
120
+ tests or custom presentation objects.
121
+
122
+ ## Check Presentation Results
123
+
124
+ Use `isPresentationResult(...)` before rendering unknown command output.
125
+
126
+ ```ts
127
+ import { isPresentationResult } from 'icore';
128
+
129
+ function renderUnknown(value: unknown): string {
130
+ if (!isPresentationResult(value)) {
131
+ throw new Error('Expected presentation result');
132
+ }
133
+
134
+ return presentation.render(value, 'table');
135
+ }
136
+ ```
137
+
138
+ This is useful at generic boundaries. Inside a command handler, returning a
139
+ known presentation view is usually clearer.
140
+
141
+ ## Use Direct Renderers
142
+
143
+ The facade exposes renderers for code that already has renderer-specific input.
144
+
145
+ ```ts
146
+ const jsonText = presentation.renderers.json.render({
147
+ ok: true
148
+ });
149
+
150
+ const tableText = presentation.renderers.table.render([
151
+ ['field', 'value'],
152
+ ['ok', 'true']
153
+ ]);
154
+
155
+ const csvRow = presentation.renderers.csv.renderRow([
156
+ 'job-42',
157
+ 'running'
158
+ ]);
159
+
160
+ const csvText = presentation.renderers.csv.render([
161
+ ['id', 'status'],
162
+ ['job-42', 'running']
163
+ ]);
164
+ ```
165
+
166
+ The standalone functions provide the same lower-level behavior:
167
+
168
+ ```ts
169
+ import {
170
+ renderCsv,
171
+ renderCsvRow,
172
+ renderJson,
173
+ renderTextTable
174
+ } from 'icore';
175
+
176
+ renderJson({ ok: true });
177
+ renderTextTable([
178
+ ['field', 'value'],
179
+ ['ok', 'true']
180
+ ]);
181
+ renderCsvRow(['job-42', 'running']);
182
+ renderCsv([
183
+ ['id', 'status'],
184
+ ['job-42', 'running']
185
+ ]);
186
+ ```
187
+
188
+ Direct renderers are sharp tools. They are good for adapters and tests, but
189
+ regular commands should usually return presentation views and let
190
+ `presentation.render(...)` choose the renderer.
@@ -0,0 +1,48 @@
1
+ # Examples
2
+
3
+ These examples are guide-style notes for application code. They explain what to
4
+ put into a consuming project, how to run it from a terminal, what output to
5
+ expect, and why a particular shape is useful or intentionally limited.
6
+
7
+ ## Terminal Application
8
+
9
+ - [terminal-app.md](terminal-app.md) shows the regular `createTerminalApp()`
10
+ path with commands, presentation, and output.
11
+ - [practical-cli-patterns.md](practical-cli-patterns.md) shows application-level
12
+ patterns for global help/version shortcuts, command help, deprecated options,
13
+ and edge-case argument handling.
14
+
15
+ ## Layer Toolkit
16
+
17
+ - [option-schemas.md](option-schemas.md) shows how to describe string,
18
+ boolean, and number options with defaults, choices, aliases, and inferred
19
+ TypeScript types.
20
+ - [cli-argument-syntax.md](cli-argument-syntax.md) documents supported CLI
21
+ argument syntax with terminal input examples.
22
+ - [custom-command-flow.md](custom-command-flow.md) shows explicit
23
+ `commands.prepare(...)`, `commands.run(...)`, and `commands.runFromArgs(...)`
24
+ usage without the terminal app boundary.
25
+ - [presentation-output.md](presentation-output.md) shows presentation rendering
26
+ and output writing without command execution.
27
+
28
+ ## Primitive Mechanics
29
+
30
+ - [command-resolution.md](command-resolution.md) shows command registries,
31
+ explicit resolution, command-name guards, and standalone command definitions.
32
+ - [two-phase-primitives.md](two-phase-primitives.md) shows lower-level
33
+ preparation and execution primitives used behind command facades.
34
+ - [presentation-primitives.md](presentation-primitives.md) shows explicit
35
+ presentation views, renderers, and format guards.
36
+ - [output-writers.md](output-writers.md) shows stdout/stderr writer primitives
37
+ and custom sink wiring.
38
+
39
+ ## Compatibility And Rework Candidates
40
+
41
+ Some examples intentionally show public APIs that are useful but not preferred
42
+ as the first choice for new application code. Those examples call out the
43
+ tradeoff near the method usage.
44
+
45
+ - [command-resolution.md](command-resolution.md) covers
46
+ `resolveCommandFromArgs(...)`.
47
+ - [two-phase-primitives.md](two-phase-primitives.md) covers
48
+ `parseOptionsDetailed(...)` and `parseOptionsSubsetDetailed(...)`.
@@ -0,0 +1,118 @@
1
+ # Terminal App
2
+
3
+ Use this shape when your package owns a command-line entrypoint and wants icore
4
+ to connect command parsing, presentation rendering, and stdout/stderr writing.
5
+
6
+ This is the default shape for a regular terminal application because it keeps
7
+ the CLI boundary in one place: commands return text or presentation views, while
8
+ `createTerminalApp()` owns rendering, stdout/stderr delivery, and exit codes.
9
+ The tradeoff is that command handlers must return terminal-supported values,
10
+ not arbitrary application objects.
11
+
12
+ Create the file that becomes your CLI entrypoint, for example `src/cli.ts`, and
13
+ put the terminal app composition there:
14
+
15
+ ```ts
16
+ import {
17
+ createCommand,
18
+ createOutput,
19
+ createPresentation,
20
+ createTerminalApp,
21
+ presentationFormatOptions
22
+ } from 'icore';
23
+
24
+ type AppContext = {
25
+ currentUser: string;
26
+ };
27
+
28
+ const command = createCommand();
29
+ const presentation = createPresentation();
30
+
31
+ const commands = command.registry([
32
+ command.define({
33
+ path: ['users', 'current'],
34
+ options: presentationFormatOptions,
35
+ handle({ context }: {
36
+ context: AppContext;
37
+ }) {
38
+ return presentation.record({
39
+ user: context.currentUser
40
+ });
41
+ }
42
+ })
43
+ ] as const);
44
+
45
+ const app = createTerminalApp({
46
+ commands,
47
+ presentation,
48
+ output: createOutput()
49
+ });
50
+
51
+ async function main(args: readonly string[]): Promise<void> {
52
+ process.exitCode = await app.run(args, {
53
+ currentUser: 'Alice'
54
+ }, {
55
+ strict: true
56
+ });
57
+ }
58
+
59
+ void main(process.argv.slice(2));
60
+ ```
61
+
62
+ Create `command`, `presentation`, and `output` once near the entrypoint. That is
63
+ slightly more explicit than hiding them behind defaults, but it keeps stdout,
64
+ stderr, and format behavior visible at the terminal boundary. `strict: true`
65
+ keeps the public command form predictable by rejecting option placement that the
66
+ application does not intentionally support.
67
+
68
+ After compiling the consuming project, run the generated entrypoint:
69
+
70
+ ```bash
71
+ node dist/cli.js users current
72
+ ```
73
+
74
+ The terminal prints the default table view:
75
+
76
+ ```text
77
+ field value
78
+ user Alice
79
+ ```
80
+
81
+ Ask for JSON when the command should be consumed by another process:
82
+
83
+ ```bash
84
+ node dist/cli.js users current --format json
85
+ ```
86
+
87
+ The terminal prints:
88
+
89
+ ```json
90
+ {
91
+ "user": "Alice"
92
+ }
93
+ ```
94
+
95
+ ## Override Format Resolution
96
+
97
+ By default, `createTerminalApp()` looks for a parsed `format` option and uses it
98
+ when it is one of the supported presentation formats. Override `resolveFormat`
99
+ when format policy is application-specific.
100
+
101
+ ```ts
102
+ const appWithFormatPolicy = createTerminalApp({
103
+ commands,
104
+ presentation,
105
+ output: createOutput(),
106
+ resolveFormat(prepared) {
107
+ if (prepared.name === 'users export') {
108
+ return 'csv';
109
+ }
110
+
111
+ return undefined;
112
+ }
113
+ });
114
+ ```
115
+
116
+ This is useful when one command has a different default output contract. It is
117
+ also a point where the application can make a bad abstraction: avoid hiding
118
+ format decisions here when a normal `--format` option would be clearer.
@@ -0,0 +1,282 @@
1
+ # Two-Phase Primitives
2
+
3
+ The high-level flow is `commands.prepare(...)` followed by
4
+ `commands.run(...)`. The primitives below are useful when a custom terminal
5
+ boundary needs the same pieces without adopting the full facade.
6
+
7
+ Prefer the facade first. These primitives are intentionally lower-level and
8
+ become worthwhile only when the application needs a custom lifecycle.
9
+
10
+ The snippets below use this setup:
11
+
12
+ ```ts
13
+ import {
14
+ createCommand,
15
+ createTerminalApp
16
+ } from 'icore';
17
+
18
+ type AppContext = {
19
+ workspace: string;
20
+ };
21
+
22
+ const command = createCommand();
23
+
24
+ const runJobCommand = command.define({
25
+ path: ['jobs', 'run'],
26
+ options: {
27
+ 'job-id': {
28
+ type: 'string',
29
+ required: true
30
+ }
31
+ } as const,
32
+ prepare({ options }) {
33
+ return {
34
+ jobId: options['job-id'].trim()
35
+ };
36
+ },
37
+ handle({ payload, context }: {
38
+ payload: { jobId: string };
39
+ context: AppContext;
40
+ }) {
41
+ return `${context.workspace}:${payload.jobId}\n`;
42
+ }
43
+ });
44
+
45
+ const commands = command.registry([
46
+ runJobCommand
47
+ ] as const);
48
+
49
+ const app = createTerminalApp({
50
+ commands
51
+ });
52
+ ```
53
+
54
+ ## Prepare From A Terminal App
55
+
56
+ `app.prepare(...)` lets an application validate command input before runtime
57
+ context exists.
58
+
59
+ ```ts
60
+ const prepared = await app.prepare([
61
+ 'jobs',
62
+ 'run',
63
+ '--job-id',
64
+ 'job-42'
65
+ ], {
66
+ strict: true
67
+ });
68
+
69
+ prepared.name;
70
+ prepared.options;
71
+ prepared.provided;
72
+ ```
73
+
74
+ This is useful when the selected command decides which runtime resources to
75
+ create. If preparation fails, the application can print an argument error
76
+ without opening connections or starting background work.
77
+
78
+ ## Prepare From A Registry
79
+
80
+ `prepareCommandFromArgs(...)` is the standalone primitive behind
81
+ `commands.prepare(...)`.
82
+
83
+ ```ts
84
+ import {
85
+ prepareCommandFromArgs,
86
+ type CommandPayload
87
+ } from 'icore';
88
+
89
+ const prepared = await prepareCommandFromArgs(commands.registry, [
90
+ 'jobs',
91
+ 'run',
92
+ '--job-id',
93
+ 'job-42'
94
+ ], {
95
+ strict: true
96
+ });
97
+
98
+ type Payload = CommandPayload<typeof runJobCommand>;
99
+ ```
100
+
101
+ Use this when code owns a registry but does not use the `commands` object. The
102
+ tradeoff is weaker readability: readers have to know which primitive maps to
103
+ which facade method.
104
+
105
+ ## Run A Prepared Command
106
+
107
+ `runPreparedCommand(...)` executes a command that was already resolved and
108
+ validated.
109
+
110
+ ```ts
111
+ import {
112
+ runPreparedCommand,
113
+ type CommandContext,
114
+ type CommandResult
115
+ } from 'icore';
116
+
117
+ type Context = CommandContext<typeof runJobCommand>;
118
+ type Result = CommandResult<typeof runJobCommand>;
119
+
120
+ const context: Context = {
121
+ workspace: 'local'
122
+ };
123
+
124
+ const result: Result = await runPreparedCommand(prepared, context);
125
+ ```
126
+
127
+ This is the safest primitive when context creation has side effects. Prepare
128
+ first, then create the exact context the selected command needs.
129
+
130
+ ## Narrow Prepared Commands
131
+
132
+ `isPreparedCommandName(...)` narrows a prepared-command union by command name.
133
+
134
+ ```ts
135
+ import { isPreparedCommandName } from 'icore';
136
+
137
+ if (isPreparedCommandName(prepared, 'jobs run')) {
138
+ prepared.options['job-id'];
139
+ }
140
+ ```
141
+
142
+ Use this when several commands reuse one preparation flow but need different
143
+ runtime setup. The guard is intentionally narrow: it checks the prepared
144
+ command name, not arbitrary metadata.
145
+
146
+ ## Run From A Registry
147
+
148
+ `runCommandFromRegistry(...)` is the standalone primitive behind
149
+ `commands.runFromArgs(...)`.
150
+
151
+ ```ts
152
+ import { runCommandFromRegistry } from 'icore';
153
+
154
+ const output = await runCommandFromRegistry(
155
+ commands.registry,
156
+ [
157
+ 'jobs',
158
+ 'run',
159
+ '--job-id',
160
+ 'job-42'
161
+ ],
162
+ {
163
+ workspace: 'local'
164
+ },
165
+ {
166
+ strict: true
167
+ }
168
+ );
169
+ ```
170
+
171
+ This is compact, but it removes the explicit gap between validation and runtime
172
+ context creation. Use it when that gap does not matter.
173
+
174
+ ## Run One Command Without A Registry
175
+
176
+ Use `command.run(...)` or standalone `runCommand(...)` for focused execution of
177
+ one command.
178
+
179
+ ```ts
180
+ import { runCommand } from 'icore';
181
+
182
+ const outputFromFacade = await command.run(runJobCommand, [
183
+ 'jobs',
184
+ 'run',
185
+ '--job-id',
186
+ 'job-42'
187
+ ], {
188
+ workspace: 'local'
189
+ }, {
190
+ strict: true
191
+ });
192
+
193
+ const outputFromPrimitive = await runCommand(runJobCommand, [
194
+ 'jobs',
195
+ 'run',
196
+ '--job-id',
197
+ 'job-42'
198
+ ], {
199
+ workspace: 'local'
200
+ }, {
201
+ strict: true
202
+ });
203
+ ```
204
+
205
+ This is useful for tests and tiny tools. It is a poor fit for large CLIs because
206
+ it skips registry-level command selection.
207
+
208
+ ## Read Provided Metadata
209
+
210
+ `parseOptionsDetailed(...)` returns parsed values plus a presence map.
211
+
212
+ ```ts
213
+ import {
214
+ parseOptionsDetailed,
215
+ type InferProvidedOptions
216
+ } from 'icore';
217
+
218
+ const schema = {
219
+ format: {
220
+ type: 'string',
221
+ choices: ['table', 'json'],
222
+ default: 'table'
223
+ },
224
+ verbose: {
225
+ type: 'boolean'
226
+ }
227
+ } as const;
228
+
229
+ const parsed = parseOptionsDetailed(schema, {
230
+ verbose: true
231
+ });
232
+
233
+ type Provided = InferProvidedOptions<typeof schema>;
234
+ ```
235
+
236
+ The parsed options are:
237
+
238
+ ```ts
239
+ {
240
+ format: 'table',
241
+ verbose: true
242
+ }
243
+ ```
244
+
245
+ The provided map is:
246
+
247
+ ```ts
248
+ {
249
+ format: false,
250
+ verbose: true
251
+ }
252
+ ```
253
+
254
+ This distinction matters when defaults exist. The command can know whether the
255
+ user explicitly typed an option or received the default.
256
+
257
+ ## Validate A Subset
258
+
259
+ `parseOptionsSubsetDetailed(...)` validates only options known by one schema and
260
+ leaves the rest untouched.
261
+
262
+ ```ts
263
+ import { parseOptionsSubsetDetailed } from 'icore';
264
+
265
+ const parsed = parseOptionsSubsetDetailed({
266
+ format: {
267
+ type: 'string',
268
+ choices: ['table', 'json']
269
+ }
270
+ } as const, {
271
+ format: 'json',
272
+ token: 'secret'
273
+ });
274
+
275
+ parsed.options;
276
+ parsed.rest;
277
+ ```
278
+
279
+ This is useful for staged parsing where bootstrap options and command options
280
+ are validated by different layers. It should be used carefully: accepting
281
+ unknown rest values is a deliberate boundary decision, not a shortcut around
282
+ validation.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "icore",
3
- "version": "1.0.14",
4
- "description": "Declarative command line interface mechanics for Node.js",
3
+ "version": "1.0.16",
4
+ "description": "Declarative command line interface and terminal presentation mechanics for Node.js",
5
5
  "keywords": [
6
6
  "command-line",
7
7
  "argv",
@@ -9,6 +9,11 @@
9
9
  "option-parser",
10
10
  "command-registry",
11
11
  "command-router",
12
+ "terminal-app",
13
+ "terminal-output",
14
+ "presentation",
15
+ "table-renderer",
16
+ "csv-renderer",
12
17
  "typed-cli",
13
18
  "declarative-cli"
14
19
  ],
@@ -28,13 +33,14 @@
28
33
  "types": "dist/index.d.ts",
29
34
  "scripts": {
30
35
  "build": "tsc",
31
- "test": "node --test dist/*.test.js",
36
+ "test": "fwa --prune",
32
37
  "lint": "eslint src"
33
38
  },
34
39
  "devDependencies": {
35
40
  "@eslint/js": "^9.39.4",
36
41
  "@types/node": "^26.0.1",
37
42
  "eslint": "^9.39.4",
43
+ "fwa": "^2.0.6",
38
44
  "globals": "^17.7.0",
39
45
  "typescript": "^6.0.3",
40
46
  "typescript-eslint": "^8.62.0"