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,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).