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,128 @@
1
+ # Output Writers
2
+
3
+ `createOutput()` is the preferred output entrypoint. It exposes semantic
4
+ `write(...)` and `error(...)` methods while keeping raw stdout/stderr channels
5
+ available for lower-level integrations.
6
+
7
+ Prefer semantic methods first. Direct writer primitives are useful in tests,
8
+ streaming adapters, and custom terminal boundaries.
9
+
10
+ ## Use Semantic Output
11
+
12
+ ```ts
13
+ import { createOutput } from 'icore';
14
+
15
+ const output = createOutput();
16
+
17
+ await output.write('regular command output\n');
18
+ await output.error('warning: using cached data\n');
19
+ ```
20
+
21
+ The convention is simple: command results go to stdout, diagnostics go to
22
+ stderr. Keeping that split makes shell pipelines usable.
23
+
24
+ ## Use Explicit Channels
25
+
26
+ Use `output.stdout.write(...)` and `output.stderr.write(...)` when a specific
27
+ channel must be passed into another component.
28
+
29
+ ```ts
30
+ await output.stdout.write('regular command output\n');
31
+ await output.stderr.write('warning: using cached data\n');
32
+ ```
33
+
34
+ This is lower-level than `output.write(...)` and `output.error(...)`. Prefer it
35
+ only when the channel identity matters.
36
+
37
+ ## Inject Test Sinks
38
+
39
+ Pass custom sinks to `createOutput(...)` when tests or applications need to
40
+ capture text.
41
+
42
+ ```ts
43
+ let stdout = '';
44
+ let stderr = '';
45
+
46
+ const output = createOutput({
47
+ stdout: {
48
+ write(chunk) {
49
+ stdout += chunk;
50
+ }
51
+ },
52
+ stderr: {
53
+ write(chunk) {
54
+ stderr += chunk;
55
+ }
56
+ }
57
+ });
58
+
59
+ await output.write('ok\n');
60
+ await output.error('warning\n');
61
+ ```
62
+
63
+ This is better than mocking `process.stdout` globally. The output dependency is
64
+ explicit and local to the terminal boundary.
65
+
66
+ ## Create Node Writers Directly
67
+
68
+ `createStdoutWriter(...)` and `createStderrWriter(...)` create individual text
69
+ writers.
70
+
71
+ ```ts
72
+ import {
73
+ createStderrWriter,
74
+ createStdoutWriter
75
+ } from 'icore';
76
+
77
+ const stdout = createStdoutWriter();
78
+ const stderr = createStderrWriter();
79
+
80
+ await stdout.write('ok\n');
81
+ await stderr.write('warning\n');
82
+ ```
83
+
84
+ Use these when a custom facade owns channel composition. For normal CLI code,
85
+ `createOutput()` is clearer because it preserves stdout/stderr semantics in one
86
+ object.
87
+
88
+ ## Use A Backpressure-Aware Writer
89
+
90
+ `createBackpressureTextWriter(...)` adapts a text sink and waits when the sink
91
+ reports backpressure.
92
+
93
+ ```ts
94
+ import { createBackpressureTextWriter } from 'icore';
95
+
96
+ const writer = createBackpressureTextWriter({
97
+ write(chunk) {
98
+ return process.stdout.write(chunk);
99
+ },
100
+ once(event, listener) {
101
+ process.stdout.once(event, listener);
102
+ }
103
+ });
104
+
105
+ await writer.write('large chunk\n');
106
+ ```
107
+
108
+ This is useful for streaming commands. It is more code than direct
109
+ `process.stdout.write(...)`, but it prevents a fast producer from ignoring a
110
+ slow consumer.
111
+
112
+ ## Use Promise-Returning Sinks
113
+
114
+ Sinks can also return a promise from `write(...)`.
115
+
116
+ ```ts
117
+ const writer = createBackpressureTextWriter({
118
+ async write(chunk) {
119
+ await sendToRemoteConsole(chunk);
120
+ }
121
+ });
122
+
123
+ await writer.write('forwarded output\n');
124
+ ```
125
+
126
+ This shape is useful when output is not a Node stream. The tradeoff is that the
127
+ application owns the reliability and ordering guarantees of that sink.
128
+
@@ -0,0 +1,385 @@
1
+ # Practical CLI Patterns
2
+
3
+ These examples show application-level patterns built on top of icore. They use
4
+ neutral command names, but the shapes are meant for real CLI applications with
5
+ many commands, shared options, utility commands, and compatibility behavior.
6
+
7
+ ## Global Help And Version Shortcuts
8
+
9
+ In a larger CLI, `--help`, `-h`, `--version`, and `-v` often work before command
10
+ execution. They should not require command-specific required options or runtime
11
+ context.
12
+
13
+ This behavior is not automatic in icore because it is application policy, not
14
+ command mechanics. Keeping it in the bootstrap runner is the most explicit
15
+ choice: utility shortcuts can bypass command-specific validation, while regular
16
+ commands still use the registry and their own schemas.
17
+
18
+ Put this logic in the bootstrap runner, before the command registry runs a
19
+ business command:
20
+
21
+ ```ts
22
+ import {
23
+ parseArgv,
24
+ parseOptions,
25
+ type OptionsSchema,
26
+ type RawOptionValue
27
+ } from 'icore';
28
+
29
+ const bootstrapOptionsSchema = {
30
+ help: {
31
+ type: 'boolean'
32
+ },
33
+ h: {
34
+ type: 'boolean'
35
+ },
36
+ version: {
37
+ type: 'boolean'
38
+ },
39
+ v: {
40
+ type: 'boolean'
41
+ },
42
+ offline: {
43
+ type: 'boolean'
44
+ }
45
+ } as const satisfies OptionsSchema;
46
+
47
+ const bootstrapOptionNames = Object.keys(bootstrapOptionsSchema);
48
+
49
+ function normalizeGlobalAliases(args: readonly string[]): string[] {
50
+ return args.map((arg) => {
51
+ if (arg === '-h') {
52
+ return '--h';
53
+ }
54
+
55
+ if (arg === '-v') {
56
+ return '--v';
57
+ }
58
+
59
+ return arg;
60
+ });
61
+ }
62
+
63
+ function parseBootstrapInput(args: readonly string[]) {
64
+ const parsed = parseArgv(normalizeGlobalAliases(args), bootstrapOptionsSchema);
65
+ const bootstrapOptions: Record<string, RawOptionValue> = {};
66
+
67
+ for (const name of bootstrapOptionNames) {
68
+ if (Object.hasOwn(parsed.options, name)) {
69
+ bootstrapOptions[name] = parsed.options[name] as RawOptionValue;
70
+ }
71
+ }
72
+
73
+ parseOptions(bootstrapOptionsSchema, bootstrapOptions);
74
+
75
+ return parsed;
76
+ }
77
+ ```
78
+
79
+ This example normalizes `-h` and `-v` before parsing. It is a small compatibility
80
+ layer, but it should stay close to bootstrap code because it changes the public
81
+ CLI surface.
82
+
83
+ Only bootstrap-owned options are validated here. Command-specific options are
84
+ left for the selected command schema. That avoids a common mistake where global
85
+ parsing rejects valid command options before the command is even resolved.
86
+
87
+ The user can ask for top-level help:
88
+
89
+ ```bash
90
+ workspace-cli --help
91
+ workspace-cli -h
92
+ workspace-cli help
93
+ ```
94
+
95
+ The application prints its command list:
96
+
97
+ ```text
98
+ Usage:
99
+ workspace-cli <command> [options]
100
+
101
+ Commands:
102
+ help [command]
103
+ version
104
+ jobs list
105
+ jobs run
106
+ ```
107
+
108
+ The user can ask for command help without providing required command options:
109
+
110
+ ```bash
111
+ workspace-cli jobs run --help
112
+ workspace-cli help jobs run
113
+ ```
114
+
115
+ The application prints only that command:
116
+
117
+ ```text
118
+ jobs run - Run a job
119
+
120
+ Usage:
121
+ workspace-cli jobs run --job-id=ID [--dry-run]
122
+
123
+ Options:
124
+ --job-id=value Required job identifier
125
+ --dry-run Validate input without running the job
126
+ ```
127
+
128
+ Version can also be a command and a global shortcut:
129
+
130
+ ```bash
131
+ workspace-cli version
132
+ workspace-cli --version
133
+ workspace-cli -v
134
+ ```
135
+
136
+ The application prints runtime information:
137
+
138
+ ```text
139
+ workspace-cli 1.4.0
140
+ node v24.13.1
141
+ platform linux x64
142
+ ```
143
+
144
+ Invalid boolean assignments are still rejected before command execution:
145
+
146
+ ```bash
147
+ workspace-cli --help=false
148
+ ```
149
+
150
+ The terminal shows:
151
+
152
+ ```text
153
+ Expected '--help' as boolean flag
154
+ ```
155
+
156
+ ## Utility Commands As Regular Commands
157
+
158
+ Utility commands can use the same command registry as the rest of the
159
+ application. The command below accepts extra positionals so `help jobs run` can
160
+ target another command:
161
+
162
+ This approach is usually better than hard-coding utility commands outside the
163
+ registry because `help` and `version` remain visible public commands. The cost
164
+ is that bootstrap still needs shortcut handling for `--help`, `-h`, `--version`,
165
+ and `-v` if those forms are part of the public interface.
166
+
167
+ ```ts
168
+ import { createCommand } from 'icore';
169
+
170
+ const command = createCommand();
171
+
172
+ const helpCommand = command.define({
173
+ path: ['help'],
174
+ options: {},
175
+ allowExtraPositionals: true,
176
+ handle({ positionals }) {
177
+ if (positionals.length === 0) {
178
+ return renderTopLevelHelp();
179
+ }
180
+
181
+ return renderCommandHelp(positionals.map(String).join(' '));
182
+ }
183
+ });
184
+
185
+ const versionCommand = command.define({
186
+ path: ['version'],
187
+ options: {},
188
+ allowExtraPositionals: true,
189
+ handle() {
190
+ return renderVersionInfo();
191
+ }
192
+ });
193
+ ```
194
+
195
+ This keeps utility command routing explicit. The application still owns the
196
+ actual help text and version text.
197
+
198
+ ## Shared Application Options
199
+
200
+ Real commands often reuse connection, output, or runtime switches. Keep those
201
+ schemas small and compose them with command-specific schemas:
202
+
203
+ Composition is useful when the same options appear across many commands. It is
204
+ not a reason to create generic schemas for one-off options. If only one command
205
+ uses an option, define it directly on that command.
206
+
207
+ ```ts
208
+ import {
209
+ mergeOptionsSchema,
210
+ type OptionsSchema
211
+ } from 'icore';
212
+
213
+ const runtimeOptions = {
214
+ config: {
215
+ type: 'string'
216
+ },
217
+ profile: {
218
+ type: 'string'
219
+ },
220
+ offline: {
221
+ type: 'boolean'
222
+ }
223
+ } as const satisfies OptionsSchema;
224
+
225
+ const outputOptions = {
226
+ format: {
227
+ type: 'string',
228
+ choices: ['table', 'json', 'csv'],
229
+ default: 'table'
230
+ }
231
+ } as const satisfies OptionsSchema;
232
+
233
+ const listJobsOptions = mergeOptionsSchema(runtimeOptions, outputOptions, {
234
+ status: {
235
+ type: 'string',
236
+ choices: ['queued', 'running', 'done', 'failed']
237
+ },
238
+ limit: {
239
+ type: 'number',
240
+ integer: true,
241
+ min: 1,
242
+ max: 100,
243
+ default: 20
244
+ }
245
+ } as const);
246
+ ```
247
+
248
+ The user can combine shared and command-specific options:
249
+
250
+ ```bash
251
+ workspace-cli jobs list --profile staging --format json --status failed --limit 10
252
+ ```
253
+
254
+ The command handler receives:
255
+
256
+ ```ts
257
+ {
258
+ config: undefined,
259
+ profile: 'staging',
260
+ offline: undefined,
261
+ format: 'json',
262
+ status: 'failed',
263
+ limit: 10
264
+ }
265
+ ```
266
+
267
+ ## Deprecated Option Aliases
268
+
269
+ When a public CLI option is renamed, keep the compatibility option close to the
270
+ canonical option and resolve the conflict explicitly:
271
+
272
+ This is not the cleanest contract, but it is useful when existing users may
273
+ already depend on the old name. Keep the deprecated option visible in code,
274
+ warn when it is used, and reject ambiguous calls that pass both names.
275
+
276
+ ```ts
277
+ const itemIdOptions = {
278
+ 'item-id': {
279
+ type: 'string'
280
+ },
281
+ 'legacy-id': {
282
+ type: 'string'
283
+ }
284
+ } as const;
285
+
286
+ type ItemIdOptions = {
287
+ 'item-id'?: string;
288
+ 'legacy-id'?: string;
289
+ };
290
+
291
+ function resolveItemId(options: ItemIdOptions): string {
292
+ if (options['item-id'] !== undefined && options['legacy-id'] !== undefined) {
293
+ throw new Error("Use either '--item-id' or deprecated '--legacy-id', not both");
294
+ }
295
+
296
+ if (options['item-id'] !== undefined) {
297
+ return options['item-id'];
298
+ }
299
+
300
+ if (options['legacy-id'] !== undefined) {
301
+ return options['legacy-id'];
302
+ }
303
+
304
+ throw new Error("Expected '--item-id'");
305
+ }
306
+ ```
307
+
308
+ The canonical option is quiet:
309
+
310
+ ```bash
311
+ workspace-cli catalog get --item-id=item-42
312
+ ```
313
+
314
+ The deprecated option can still work while the application prints a warning to
315
+ stderr:
316
+
317
+ ```bash
318
+ workspace-cli catalog get --legacy-id=item-42
319
+ ```
320
+
321
+ The terminal can show:
322
+
323
+ ```text
324
+ Warning: '--legacy-id' is deprecated; use '--item-id' instead.
325
+ ```
326
+
327
+ Using both options is rejected:
328
+
329
+ ```bash
330
+ workspace-cli catalog get --item-id=item-42 --legacy-id=item-42
331
+ ```
332
+
333
+ The terminal shows:
334
+
335
+ ```text
336
+ Use either '--item-id' or deprecated '--legacy-id', not both
337
+ ```
338
+
339
+ ## Edge Cases Worth Testing
340
+
341
+ Boolean flags do not consume the next token as a value:
342
+
343
+ This behavior is intentional. It prevents `--offline false` from silently
344
+ meaning something different than the supported boolean syntax.
345
+
346
+ ```bash
347
+ workspace-cli jobs list --offline false
348
+ ```
349
+
350
+ The parser sees `offline: true` and leaves `false` as a positional token. If the
351
+ command does not allow extra positionals, command validation rejects it.
352
+
353
+ Schema-known string and number options can consume dash-prefixed values:
354
+
355
+ This is why parsing receives the command schema. Without the schema, `-draft`
356
+ or `-1` could be mistaken for another option-like token.
357
+
358
+ ```bash
359
+ workspace-cli search --query -draft --limit -1
360
+ ```
361
+
362
+ This is useful for search text and signed numbers. Schema validation can still
363
+ reject the parsed value later, for example when `limit` has `min: 1`.
364
+
365
+ The option terminator turns everything after it into positional input:
366
+
367
+ This is useful when the user needs to pass text that looks like an option to the
368
+ application itself.
369
+
370
+ ```bash
371
+ workspace-cli search -- --query -draft --offline
372
+ ```
373
+
374
+ The tokens `--query`, `-draft`, and `--offline` are no longer parsed as options.
375
+
376
+ Duplicate long and short forms are rejected as the same argument:
377
+
378
+ Rejecting duplicates is stricter than "last value wins", but it prevents hidden
379
+ precedence rules in command contracts.
380
+
381
+ ```bash
382
+ workspace-cli jobs list --offline -o
383
+ ```
384
+
385
+ That is a duplicate when `offline` declares `alias: 'o'`.
@@ -0,0 +1,66 @@
1
+ # Presentation And Output
2
+
3
+ Use this shape when command execution is handled elsewhere, but the application
4
+ still wants shared terminal rendering and stdout/stderr writing.
5
+
6
+ This split is useful when application code already has its own command runner
7
+ or framework. icore can still provide a consistent terminal view layer without
8
+ taking ownership of command execution.
9
+
10
+ Create presentation and output once near the terminal boundary:
11
+
12
+ ```ts
13
+ import {
14
+ createOutput,
15
+ createPresentation
16
+ } from 'icore';
17
+
18
+ const presentation = createPresentation();
19
+ const output = createOutput();
20
+ ```
21
+
22
+ Keeping these objects near the boundary avoids leaking stdout/stderr decisions
23
+ into application services. The tradeoff is that domain objects must be mapped to
24
+ generic presentation records before they are rendered.
25
+
26
+ Build a view from application data:
27
+
28
+ ```ts
29
+ const result = presentation.records([
30
+ {
31
+ id: 'job-1',
32
+ active: true
33
+ },
34
+ {
35
+ id: 'job-2',
36
+ active: false
37
+ }
38
+ ]);
39
+ ```
40
+
41
+ The view contains already selected fields. This is deliberate: presentation
42
+ views should not discover domain meaning by themselves.
43
+
44
+ Render the view and write it to stdout. Write operational messages to stderr:
45
+
46
+ ```ts
47
+ await output.write(presentation.render(result, 'table'));
48
+ await output.error('Rendered job table\n');
49
+ ```
50
+
51
+ Write machine-consumable command output to stdout and operational status to
52
+ stderr. That convention keeps shell pipelines usable.
53
+
54
+ The user sees the table in stdout:
55
+
56
+ ```text
57
+ id active
58
+ job-1 true
59
+ job-2 false
60
+ ```
61
+
62
+ The status line goes to stderr:
63
+
64
+ ```text
65
+ Rendered job table
66
+ ```