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,218 @@
|
|
|
1
|
+
# CLI Argument Syntax
|
|
2
|
+
|
|
3
|
+
icore supports a practical GNU-style option syntax. The parser receives
|
|
4
|
+
`process.argv.slice(2)`, splits command path tokens from option tokens, and then
|
|
5
|
+
uses the command schema to validate option values.
|
|
6
|
+
|
|
7
|
+
Supported forms:
|
|
8
|
+
|
|
9
|
+
- long options: `--name value`, `--name=value`;
|
|
10
|
+
- boolean flags: `--flag`, `--no-flag`;
|
|
11
|
+
- short aliases declared in the schema: `-f`, `-n value`;
|
|
12
|
+
- option terminator: `--`.
|
|
13
|
+
|
|
14
|
+
The supported set is intentionally small. It covers common terminal usage while
|
|
15
|
+
avoiding shell-specific surprises and hidden precedence rules.
|
|
16
|
+
|
|
17
|
+
## Long options
|
|
18
|
+
|
|
19
|
+
Use a separate value:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node dist/cli.js users get --name Alice
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The raw option value is:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"name": "Alice"
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use an equals sign when it reads better:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node dist/cli.js users get --name=Alice
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The handler receives the same parsed value.
|
|
40
|
+
|
|
41
|
+
Supporting both forms keeps the CLI comfortable for humans and scripts. Both
|
|
42
|
+
forms map to the same option value, so command handlers do not need to care
|
|
43
|
+
which spelling the user chose.
|
|
44
|
+
|
|
45
|
+
Schema-known string and number options can consume values that start with `-`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
node dist/cli.js search --label -draft --limit -1
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That works when the command schema declares `label` as `type: 'string'` and
|
|
52
|
+
`limit` as `type: 'number'`.
|
|
53
|
+
|
|
54
|
+
This is one reason argv parsing accepts a schema hint. Without it, values like
|
|
55
|
+
`-draft` and `-1` are hard to distinguish from option-looking tokens.
|
|
56
|
+
|
|
57
|
+
## Boolean flags
|
|
58
|
+
|
|
59
|
+
Use the option name to set a boolean to `true`:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node dist/cli.js reports list --archived
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use `--no-<name>` to set a known boolean option to `false`:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node dist/cli.js reports list --no-archived
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Do not pass explicit boolean values:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
node dist/cli.js reports list --archived=true
|
|
75
|
+
node dist/cli.js reports list --archived=false
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Those forms are rejected. A boolean option is either present as a flag or, when
|
|
79
|
+
the schema allows it, negated with `--no-<name>`.
|
|
80
|
+
|
|
81
|
+
Rejecting explicit boolean values is stricter, but it avoids a long list of
|
|
82
|
+
quasi-boolean strings such as `yes`, `no`, `1`, and `0`.
|
|
83
|
+
|
|
84
|
+
This command:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
node dist/cli.js reports list --archived false
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
sets `archived` to `true` and leaves `false` as a positional token. It does not
|
|
91
|
+
mean `archived: false`.
|
|
92
|
+
|
|
93
|
+
## Short aliases
|
|
94
|
+
|
|
95
|
+
Short aliases work only when the option schema declares them.
|
|
96
|
+
|
|
97
|
+
For a boolean alias:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const options = {
|
|
101
|
+
verbose: {
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
alias: 'v'
|
|
104
|
+
}
|
|
105
|
+
} as const;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The user can type:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
node dist/cli.js users get -v
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The handler receives:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"verbose": true
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For a string or number alias, put the value in the next token:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const options = {
|
|
126
|
+
name: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
alias: 'n'
|
|
129
|
+
},
|
|
130
|
+
limit: {
|
|
131
|
+
type: 'number',
|
|
132
|
+
alias: 'l'
|
|
133
|
+
}
|
|
134
|
+
} as const;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The user can type:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
node dist/cli.js users get -n Alice -l 10
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The handler receives:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"name": "Alice",
|
|
148
|
+
"limit": 10
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Attached short values are not supported:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
node dist/cli.js users get -nAlice
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Grouped short booleans are not supported:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
node dist/cli.js users get -abc
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use one declared alias per token.
|
|
165
|
+
|
|
166
|
+
This limitation is deliberate. Compact short syntax can be convenient, but it
|
|
167
|
+
creates more parser edge cases and makes command examples harder to read. icore
|
|
168
|
+
chooses clarity over maximum GNU compatibility here.
|
|
169
|
+
|
|
170
|
+
## Option terminator
|
|
171
|
+
|
|
172
|
+
Use `--` to stop option parsing. The terminator itself is removed; every token
|
|
173
|
+
after it becomes positional, even when it starts with `-`.
|
|
174
|
+
|
|
175
|
+
This is useful when the application needs to receive raw user text, patterns, or
|
|
176
|
+
values that look like options.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
node dist/cli.js search -- --name Alice -v
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The parser treats `--name`, `Alice`, and `-v` as positional tokens after the
|
|
183
|
+
command path.
|
|
184
|
+
|
|
185
|
+
## Duplicate options
|
|
186
|
+
|
|
187
|
+
Do not pass the same option twice:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
node dist/cli.js users get --format json --format table
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Do not pass the same option through both its long name and its short alias:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
node dist/cli.js users get --verbose -v
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Both forms are rejected as duplicate arguments.
|
|
200
|
+
|
|
201
|
+
Rejecting duplicates is usually safer than choosing the first or last value. It
|
|
202
|
+
forces the caller to send one clear value and keeps command behavior
|
|
203
|
+
deterministic.
|
|
204
|
+
|
|
205
|
+
## Unknown options
|
|
206
|
+
|
|
207
|
+
Unknown long options are rejected when command options are validated:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
node dist/cli.js users get --unknown value
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Unknown short tokens are not expanded automatically. If the active command does
|
|
214
|
+
not allow them as positionals, command validation rejects them as unexpected
|
|
215
|
+
positional arguments.
|
|
216
|
+
|
|
217
|
+
For application-level patterns built from this syntax, see
|
|
218
|
+
[practical-cli-patterns.md](practical-cli-patterns.md).
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Command Resolution
|
|
2
|
+
|
|
3
|
+
Use these primitives when the application needs command selection without
|
|
4
|
+
immediately running a command. This is common in help systems, command previews,
|
|
5
|
+
audit logs, and custom routers.
|
|
6
|
+
|
|
7
|
+
For regular terminal applications, prefer `createTerminalApp()` or
|
|
8
|
+
`createCommand().registry(...)`. The lower-level resolution API is more verbose,
|
|
9
|
+
but it makes command selection explicit and testable.
|
|
10
|
+
|
|
11
|
+
## Define Commands Without The Facade
|
|
12
|
+
|
|
13
|
+
`defineCommand(...)` is the low-level form behind `command.define(...)`.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
defineCommand,
|
|
18
|
+
defineCommandRegistry,
|
|
19
|
+
type CommandName
|
|
20
|
+
} from 'icore';
|
|
21
|
+
|
|
22
|
+
const listJobsCommand = defineCommand({
|
|
23
|
+
path: ['jobs', 'list'],
|
|
24
|
+
options: {
|
|
25
|
+
status: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
choices: ['queued', 'running', 'done', 'failed']
|
|
28
|
+
}
|
|
29
|
+
} as const,
|
|
30
|
+
handle({ options }) {
|
|
31
|
+
return `status=${options.status ?? 'any'}\n`;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const runJobCommand = defineCommand({
|
|
36
|
+
path: ['jobs', 'run'],
|
|
37
|
+
options: {
|
|
38
|
+
'job-id': {
|
|
39
|
+
type: 'string',
|
|
40
|
+
required: true
|
|
41
|
+
}
|
|
42
|
+
} as const,
|
|
43
|
+
handle({ options }) {
|
|
44
|
+
return `run ${options['job-id']}\n`;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const registry = defineCommandRegistry([
|
|
49
|
+
listJobsCommand,
|
|
50
|
+
runJobCommand
|
|
51
|
+
] as const);
|
|
52
|
+
|
|
53
|
+
type PublicCommandName = CommandName<typeof runJobCommand>;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use this form when the registry itself is the important object. The tradeoff is
|
|
57
|
+
that you do not get the convenient `commands.prepare(...)` and
|
|
58
|
+
`commands.run(...)` methods until you call `createCommands(...)`.
|
|
59
|
+
|
|
60
|
+
The inferred `PublicCommandName` is:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
type PublicCommandName = 'jobs run';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Create A Commands Object Directly
|
|
67
|
+
|
|
68
|
+
`createCommands(...)` is the direct alternative to
|
|
69
|
+
`createCommand().registry(...)`.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createCommands } from 'icore';
|
|
73
|
+
|
|
74
|
+
const commands = createCommands([
|
|
75
|
+
listJobsCommand,
|
|
76
|
+
runJobCommand
|
|
77
|
+
] as const);
|
|
78
|
+
|
|
79
|
+
commands.names;
|
|
80
|
+
commands.registry;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This is useful when command definitions are already created elsewhere and the
|
|
84
|
+
application does not need the `createCommand()` object. It is not better for
|
|
85
|
+
normal code; it is just a more direct construction form.
|
|
86
|
+
|
|
87
|
+
## Resolve From Positionals
|
|
88
|
+
|
|
89
|
+
Use `commands.resolve(...)` when arguments are already split into command path
|
|
90
|
+
tokens.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const resolved = commands.resolve([
|
|
94
|
+
'jobs',
|
|
95
|
+
'run'
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
resolved.name;
|
|
99
|
+
resolved.positionals;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The resolved command name is:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
jobs run
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
No options are parsed in this form. That is intentional: it is useful for help
|
|
109
|
+
renderers that already have command tokens, but it is not enough to validate a
|
|
110
|
+
terminal call.
|
|
111
|
+
|
|
112
|
+
## Resolve From Raw Arguments
|
|
113
|
+
|
|
114
|
+
Use `commands.resolveFromArgs(...)` or standalone
|
|
115
|
+
`resolveCommandFromArgs(...)` when raw terminal arguments may contain options.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { resolveCommandFromArgs } from 'icore';
|
|
119
|
+
|
|
120
|
+
const resolved = commands.resolveFromArgs([
|
|
121
|
+
'jobs',
|
|
122
|
+
'run',
|
|
123
|
+
'--job-id',
|
|
124
|
+
'job-42'
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const sameResolved = resolveCommandFromArgs(commands.registry, [
|
|
128
|
+
'jobs',
|
|
129
|
+
'run',
|
|
130
|
+
'--job-id',
|
|
131
|
+
'job-42'
|
|
132
|
+
]);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This form asks each command schema how to split options from command tokens. It
|
|
136
|
+
is a better fit for real argv input than `resolve(...)`.
|
|
137
|
+
|
|
138
|
+
## Use The Standalone Resolver
|
|
139
|
+
|
|
140
|
+
`resolveCommand(...)` is the primitive behind `commands.resolve(...)`.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { resolveCommand } from 'icore';
|
|
144
|
+
|
|
145
|
+
const resolved = resolveCommand(registry, [
|
|
146
|
+
'jobs',
|
|
147
|
+
'list'
|
|
148
|
+
]);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Prefer `commands.resolve(...)` when you already have a `commands` object. Use
|
|
152
|
+
the standalone function when a custom registry object is passed around.
|
|
153
|
+
|
|
154
|
+
## Guard Command Names
|
|
155
|
+
|
|
156
|
+
`isCommandName(...)` narrows unknown input to the command names registered in a
|
|
157
|
+
registry.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { isCommandName } from 'icore';
|
|
161
|
+
|
|
162
|
+
function renderHelpPage(name: unknown): string {
|
|
163
|
+
if (!isCommandName(commands.registry, name)) {
|
|
164
|
+
return renderTopLevelHelp();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return renderCommandHelp(name);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This is safer than comparing against string literals in several places. The
|
|
172
|
+
cost is that the guard only checks registered command names; it does not
|
|
173
|
+
validate options or execute anything.
|
|
174
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Custom Command Flow
|
|
2
|
+
|
|
3
|
+
Use this shape when the application wants command mechanics without the terminal
|
|
4
|
+
application boundary. The application prepares a command, creates its runtime
|
|
5
|
+
context, and then runs the prepared command explicitly.
|
|
6
|
+
|
|
7
|
+
This is useful when command parsing happens before runtime dependencies are
|
|
8
|
+
available. For example, the application can validate arguments, select a
|
|
9
|
+
command, and only then open a database connection or create an API client. The
|
|
10
|
+
tradeoff is that the application must own output rendering and error handling
|
|
11
|
+
itself.
|
|
12
|
+
|
|
13
|
+
Put the command registry close to the code that owns the command contract:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
createCommand,
|
|
18
|
+
type PreparedCommand
|
|
19
|
+
} from 'icore';
|
|
20
|
+
|
|
21
|
+
type AppContext = {
|
|
22
|
+
greeting: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const command = createCommand();
|
|
26
|
+
|
|
27
|
+
const greetCommand = command.define({
|
|
28
|
+
path: ['greet'],
|
|
29
|
+
options: {
|
|
30
|
+
name: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
required: true
|
|
33
|
+
}
|
|
34
|
+
} as const,
|
|
35
|
+
prepare({ options }) {
|
|
36
|
+
return {
|
|
37
|
+
normalizedName: options.name.trim()
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
handle({ payload, context }: {
|
|
41
|
+
payload: { normalizedName: string };
|
|
42
|
+
context: AppContext;
|
|
43
|
+
}) {
|
|
44
|
+
return `${context.greeting}, ${payload.normalizedName}!\n`;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const commands = command.registry([
|
|
49
|
+
greetCommand
|
|
50
|
+
] as const);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `prepare` hook is intentionally small. It normalizes command input before
|
|
54
|
+
runtime context exists, but it should not perform application work. Keeping that
|
|
55
|
+
split makes failed argument parsing cheap and deterministic.
|
|
56
|
+
|
|
57
|
+
When application startup needs a separate preparation step, prepare first and
|
|
58
|
+
run later:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
async function runTwoPhase(args: readonly string[]): Promise<string> {
|
|
62
|
+
const prepared: PreparedCommand<typeof greetCommand> = await commands.prepare(args, {
|
|
63
|
+
strict: true
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return commands.run(prepared, {
|
|
67
|
+
greeting: 'Hello'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The two-phase form is the safest choice when application context is expensive
|
|
73
|
+
or has side effects. If preparation fails, the runtime context never needs to be
|
|
74
|
+
created.
|
|
75
|
+
|
|
76
|
+
When the application does not need that split, run from raw terminal arguments:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
async function runFromArgs(args: readonly string[]): Promise<string> {
|
|
80
|
+
return commands.runFromArgs(args, {
|
|
81
|
+
greeting: 'Hello'
|
|
82
|
+
}, {
|
|
83
|
+
strict: true
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`runFromArgs(...)` is more compact and works well for simple applications. It is
|
|
89
|
+
less flexible because parsing, preparation, and execution happen as one step.
|
|
90
|
+
|
|
91
|
+
Pass the same arguments a user would type in the terminal:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const args = [
|
|
95
|
+
'greet',
|
|
96
|
+
'--name',
|
|
97
|
+
'Alice'
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
process.stdout.write(await runTwoPhase(args));
|
|
101
|
+
process.stdout.write(await runFromArgs(args));
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The terminal prints one line for each call:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
Hello, Alice!
|
|
108
|
+
Hello, Alice!
|
|
109
|
+
```
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Option Schemas
|
|
2
|
+
|
|
3
|
+
Use option schemas to describe the public arguments of a command. The schema
|
|
4
|
+
defines what the user can type in the terminal and what typed values the command
|
|
5
|
+
handler receives.
|
|
6
|
+
|
|
7
|
+
The schema is deliberately small: it covers primitive CLI types and validation
|
|
8
|
+
rules, while application-specific parsing stays in the application. That keeps
|
|
9
|
+
icore reusable and prevents domain rules from leaking into command mechanics.
|
|
10
|
+
|
|
11
|
+
## Start with one command
|
|
12
|
+
|
|
13
|
+
Put a command close to the application code that owns the command behavior:
|
|
14
|
+
|
|
15
|
+
This is preferable to a central "all options" file for most commands. The
|
|
16
|
+
person reading the command can see the public CLI contract and the handler
|
|
17
|
+
together.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import {
|
|
21
|
+
createCommand,
|
|
22
|
+
type InferOptions
|
|
23
|
+
} from 'icore';
|
|
24
|
+
|
|
25
|
+
const command = createCommand();
|
|
26
|
+
|
|
27
|
+
const reportOptions = {
|
|
28
|
+
project: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
required: true,
|
|
31
|
+
alias: 'p'
|
|
32
|
+
},
|
|
33
|
+
format: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
choices: ['table', 'json', 'csv'],
|
|
36
|
+
default: 'table'
|
|
37
|
+
},
|
|
38
|
+
archived: {
|
|
39
|
+
type: 'boolean',
|
|
40
|
+
default: false
|
|
41
|
+
},
|
|
42
|
+
limit: {
|
|
43
|
+
type: 'number',
|
|
44
|
+
integer: true,
|
|
45
|
+
min: 1,
|
|
46
|
+
max: 100,
|
|
47
|
+
default: 20,
|
|
48
|
+
alias: 'l'
|
|
49
|
+
}
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
type ReportOptions = InferOptions<typeof reportOptions>;
|
|
53
|
+
|
|
54
|
+
const reportCommand = command.define({
|
|
55
|
+
path: ['reports', 'list'],
|
|
56
|
+
options: reportOptions,
|
|
57
|
+
handle({ options }: {
|
|
58
|
+
options: ReportOptions;
|
|
59
|
+
}) {
|
|
60
|
+
return JSON.stringify(options, null, 2) + '\n';
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The command now accepts one required string option, one string option with
|
|
66
|
+
choices, one boolean option, and one bounded number option.
|
|
67
|
+
|
|
68
|
+
The `as const` is important because it preserves literal choices for TypeScript
|
|
69
|
+
inference. Without it, the handler would still work at runtime, but the inferred
|
|
70
|
+
type would be wider and less useful.
|
|
71
|
+
|
|
72
|
+
## Run it from the terminal
|
|
73
|
+
|
|
74
|
+
The user can pass long options:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node dist/cli.js reports list --project alpha --format json --archived --limit 5
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The handler receives:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"project": "alpha",
|
|
85
|
+
"format": "json",
|
|
86
|
+
"archived": true,
|
|
87
|
+
"limit": 5
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The user can pass declared short aliases:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
node dist/cli.js reports list -p alpha -l 5
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The handler receives defaults for options that were not typed:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"project": "alpha",
|
|
102
|
+
"format": "table",
|
|
103
|
+
"archived": false,
|
|
104
|
+
"limit": 5
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Defaults are useful for stable command behavior, but they are also part of the
|
|
109
|
+
public contract. Prefer defaults only when the implicit value is obvious and
|
|
110
|
+
safe.
|
|
111
|
+
|
|
112
|
+
## Use boolean negation
|
|
113
|
+
|
|
114
|
+
Boolean options can be turned off with `--no-<name>` when the option is known by
|
|
115
|
+
the schema:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
node dist/cli.js reports list --project alpha --archived
|
|
119
|
+
node dist/cli.js reports list --project alpha --no-archived
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The first command gives the handler `archived: true`; the second gives it
|
|
123
|
+
`archived: false`.
|
|
124
|
+
|
|
125
|
+
This form is more explicit than accepting `--archived=false`. It also avoids
|
|
126
|
+
ambiguous text values such as `0`, `no`, or `off`.
|
|
127
|
+
|
|
128
|
+
Do not pass explicit boolean values:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
node dist/cli.js reports list --project alpha --archived=true
|
|
132
|
+
node dist/cli.js reports list --project alpha --archived=false
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Both forms are rejected. Boolean options use flag syntax, not `true` / `false`
|
|
136
|
+
values.
|
|
137
|
+
|
|
138
|
+
## Restrict a boolean to flag-only syntax
|
|
139
|
+
|
|
140
|
+
Use `syntax: 'flag'` for options that should only mean "enable this behavior":
|
|
141
|
+
|
|
142
|
+
This is a stricter contract. It is a good fit for options like `--dry-run`,
|
|
143
|
+
where the absence of the flag already has a clear meaning.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const schema = {
|
|
147
|
+
'dry-run': {
|
|
148
|
+
type: 'boolean',
|
|
149
|
+
default: false,
|
|
150
|
+
syntax: 'flag'
|
|
151
|
+
}
|
|
152
|
+
} as const;
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The terminal accepts:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
node dist/cli.js reports list --dry-run
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The terminal rejects:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
node dist/cli.js reports list --no-dry-run
|
|
165
|
+
node dist/cli.js reports list --dry-run=false
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Reuse shared schema pieces
|
|
169
|
+
|
|
170
|
+
When several commands reuse the same public options, keep small schema pieces
|
|
171
|
+
and merge them at the command boundary:
|
|
172
|
+
|
|
173
|
+
This reduces duplication without hiding command-specific intent. It becomes a
|
|
174
|
+
bad abstraction when the reusable schema needs many exceptions for individual
|
|
175
|
+
commands.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { mergeOptionsSchema } from 'icore';
|
|
179
|
+
|
|
180
|
+
const pagingOptions = {
|
|
181
|
+
limit: {
|
|
182
|
+
type: 'number',
|
|
183
|
+
integer: true,
|
|
184
|
+
min: 1,
|
|
185
|
+
max: 100,
|
|
186
|
+
default: 20
|
|
187
|
+
}
|
|
188
|
+
} as const;
|
|
189
|
+
|
|
190
|
+
const outputOptions = {
|
|
191
|
+
format: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
choices: ['table', 'json', 'csv'],
|
|
194
|
+
default: 'table'
|
|
195
|
+
}
|
|
196
|
+
} as const;
|
|
197
|
+
|
|
198
|
+
const options = mergeOptionsSchema(pagingOptions, outputOptions);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Later schemas override earlier schemas when an option name is repeated. Keep
|
|
202
|
+
that behavior intentional and visible near the command definition.
|
|
203
|
+
|
|
204
|
+
For larger application patterns that combine shared schemas, global shortcuts,
|
|
205
|
+
and compatibility options, see
|
|
206
|
+
[practical-cli-patterns.md](practical-cli-patterns.md).
|