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.
- package/CHANGELOG.md +17 -0
- package/dist/{argv.d.ts → argv/parser.d.ts} +3 -1
- package/dist/{argv.js → argv/parser.js} +5 -5
- package/dist/{commands.d.ts → command/mechanics.d.ts} +79 -6
- package/dist/{commands.js → command/mechanics.js} +60 -15
- package/dist/{errors.d.ts → errors/icore-error.d.ts} +2 -0
- package/dist/{errors.js → errors/icore-error.js} +2 -0
- package/dist/index.d.ts +28 -1
- package/dist/index.js +61 -16
- package/dist/options/parser.d.ts +43 -0
- package/dist/{options.js → options/parser.js} +27 -17
- package/dist/{options.d.ts → options/schema.d.ts} +16 -23
- package/dist/options/schema.js +23 -0
- package/dist/output/facade.d.ts +28 -0
- package/dist/output/facade.js +32 -0
- package/dist/output/node-writer.d.ts +17 -0
- package/dist/output/node-writer.js +25 -0
- package/dist/output/text-writer.d.ts +29 -0
- package/dist/output/text-writer.js +49 -0
- package/dist/presentation/facade.d.ts +39 -0
- package/dist/presentation/facade.js +47 -0
- package/dist/presentation/format-options.d.ts +31 -0
- package/dist/presentation/format-options.js +37 -0
- package/dist/presentation/renderers/csv.d.ts +18 -0
- package/dist/presentation/renderers/csv.js +35 -0
- package/dist/presentation/renderers/json.d.ts +13 -0
- package/dist/presentation/renderers/json.js +18 -0
- package/dist/presentation/renderers/table.d.ts +14 -0
- package/dist/presentation/renderers/table.js +29 -0
- package/dist/presentation/result-renderer.d.ts +25 -0
- package/dist/presentation/result-renderer.js +184 -0
- package/dist/presentation/view.d.ts +58 -0
- package/dist/presentation/view.js +56 -0
- package/dist/terminal/app.d.ts +51 -0
- package/dist/terminal/app.js +100 -0
- package/examples/cli-argument-syntax.md +218 -0
- package/examples/command-resolution.md +174 -0
- package/examples/custom-command-flow.md +109 -0
- package/examples/option-schemas.md +206 -0
- package/examples/output-writers.md +128 -0
- package/examples/practical-cli-patterns.md +385 -0
- package/examples/presentation-output.md +66 -0
- package/examples/presentation-primitives.md +190 -0
- package/examples/readme.md +48 -0
- package/examples/terminal-app.md +118 -0
- package/examples/two-phase-primitives.md +282 -0
- package/package.json +9 -3
- package/readme.md +281 -290
- package/dist/cli.d.ts +0 -14
- 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
|
+
```
|