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,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.
|
|
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": "
|
|
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"
|