icore 1.0.11 → 1.0.13
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 +121 -0
- package/dist/argv.js +31 -6
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +3 -1
- package/dist/commands.d.ts +58 -13
- package/dist/commands.js +99 -15
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +29 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/options.d.ts +3 -3
- package/dist/options.js +107 -23
- package/package.json +1 -1
- package/readme.md +133 -21
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable user-facing changes to this package are documented here.
|
|
4
|
+
|
|
5
|
+
This file was reconstructed from npm publish metadata and local git history.
|
|
6
|
+
Older releases did not have a maintained changelog, so early entries are
|
|
7
|
+
intentionally conservative.
|
|
8
|
+
|
|
9
|
+
## 1.0.13 - Unreleased
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added machine-readable `IcoreError` codes and details.
|
|
14
|
+
- Added opt-in `strict: true` command resolution for rejecting options before command paths.
|
|
15
|
+
- Added `strict: true` support to direct `runCommand` execution.
|
|
16
|
+
- Added typed prepared command `payload` returned from `prepare` and passed to `handle`.
|
|
17
|
+
- Added this changelog.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Boolean options reject explicit values such as `--flag=true` and `--flag=false`; use `--flag` and `--no-flag`.
|
|
22
|
+
|
|
23
|
+
## 1.0.12 - 2026-07-02
|
|
24
|
+
|
|
25
|
+
- Replaced the preliminary flag-only boolean option naming with `syntax: 'flag'`.
|
|
26
|
+
- Documented flag-only boolean option behavior.
|
|
27
|
+
|
|
28
|
+
## 1.0.11 - 2026-07-02
|
|
29
|
+
|
|
30
|
+
- Added preliminary flag-only boolean option support.
|
|
31
|
+
|
|
32
|
+
## 1.0.10 - 2026-07-02
|
|
33
|
+
|
|
34
|
+
- Added two-phase command execution.
|
|
35
|
+
- Added `PreparedCommandInput`.
|
|
36
|
+
- Added `PreparedCommand`.
|
|
37
|
+
- Added `prepareCommandFromArgs`.
|
|
38
|
+
- Added `runPreparedCommand`.
|
|
39
|
+
- Added command `metadata`.
|
|
40
|
+
- Added command `prepare` hook.
|
|
41
|
+
- Changed `runCommand` and `runCommandFromRegistry` to use the prepare-and-run flow internally.
|
|
42
|
+
- Added focused tests for prepare-time validation and handler safety.
|
|
43
|
+
|
|
44
|
+
## 1.0.9 - 2026-07-02
|
|
45
|
+
|
|
46
|
+
- Refined npm package metadata.
|
|
47
|
+
- Updated repository metadata for npm.
|
|
48
|
+
- Simplified published package contents.
|
|
49
|
+
|
|
50
|
+
## 1.0.8 - 2026-07-02
|
|
51
|
+
|
|
52
|
+
- Updated package author metadata.
|
|
53
|
+
- Normalized README heading structure.
|
|
54
|
+
|
|
55
|
+
## 1.0.7 - 2026-07-02
|
|
56
|
+
|
|
57
|
+
- Renamed README examples from `upper` to `uppercase`.
|
|
58
|
+
- Cleaned duplicated command test coverage without changing public behavior.
|
|
59
|
+
|
|
60
|
+
## 1.0.6 - 2026-07-02
|
|
61
|
+
|
|
62
|
+
- Added explicit long boolean values such as `--flag=true` and `--flag=false`.
|
|
63
|
+
- Added schema-known boolean negation such as `--no-flag`.
|
|
64
|
+
- Added short option aliases such as `-f` and `-n value`.
|
|
65
|
+
- Added alias validation and duplicate detection between short and long option forms.
|
|
66
|
+
- Documented supported option syntax.
|
|
67
|
+
|
|
68
|
+
## 1.0.5 - 2026-07-01
|
|
69
|
+
|
|
70
|
+
- Rebuilt README as the main public documentation entry point.
|
|
71
|
+
- Removed separate generated reference docs from the published documentation flow.
|
|
72
|
+
|
|
73
|
+
## 1.0.4 - 2026-07-01
|
|
74
|
+
|
|
75
|
+
- Reorganized README and documentation references.
|
|
76
|
+
|
|
77
|
+
## 1.0.3 - 2026-07-01
|
|
78
|
+
|
|
79
|
+
- Added README table of contents.
|
|
80
|
+
- Polished README formatting and emphasis.
|
|
81
|
+
|
|
82
|
+
## 1.0.2 - 2026-06-30
|
|
83
|
+
|
|
84
|
+
- Clarified CLI scope and supported syntax in README.
|
|
85
|
+
- Added module boundary comments to production source files.
|
|
86
|
+
- Updated package description and keywords.
|
|
87
|
+
|
|
88
|
+
## 1.0.1 - 2026-06-30
|
|
89
|
+
|
|
90
|
+
- Published documentation updates after the first stable release.
|
|
91
|
+
- No dedicated version commit is present in local git history for this patch.
|
|
92
|
+
|
|
93
|
+
## 1.0.0 - 2026-06-30
|
|
94
|
+
|
|
95
|
+
- First stable release of the new `icore` CLI mechanics package.
|
|
96
|
+
- Added declarative option parsing and validation.
|
|
97
|
+
- Added schema-aware argv parsing.
|
|
98
|
+
- Added option presence metadata through `parseOptionsDetailed`.
|
|
99
|
+
- Added option schema composition through `mergeOptionsSchema`.
|
|
100
|
+
- Added command registry routing.
|
|
101
|
+
- Added typed command handler input.
|
|
102
|
+
- Added CommonJS package entry points and TypeScript declarations.
|
|
103
|
+
- Lowered the runtime Node.js engine to `>=16.9.0`.
|
|
104
|
+
|
|
105
|
+
## 1.0.0-alpha - 2026-06-28
|
|
106
|
+
|
|
107
|
+
- Started the new CLI mechanics codebase after pruning the legacy package code.
|
|
108
|
+
- Added the initial TypeScript build, test, and lint setup for the new package.
|
|
109
|
+
|
|
110
|
+
## 0.1.38 - 2019-06-12
|
|
111
|
+
|
|
112
|
+
- Final legacy web-framework line release.
|
|
113
|
+
- Added legacy tests and documentation.
|
|
114
|
+
- Updated the legacy package description.
|
|
115
|
+
- Trimmed legacy runtime dependencies.
|
|
116
|
+
|
|
117
|
+
## 0.0.37 and earlier
|
|
118
|
+
|
|
119
|
+
- Original legacy npm package line published between 2017 and 2019.
|
|
120
|
+
- Legacy history is preserved for package provenance.
|
|
121
|
+
- Detailed changelog entries were not maintained for these releases.
|
package/dist/argv.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.parseArgv = parseArgv;
|
|
15
|
+
const errors_1 = require("./errors");
|
|
15
16
|
/**
|
|
16
17
|
* Parses raw CLI arguments into positionals and raw long-option values.
|
|
17
18
|
*/
|
|
@@ -40,7 +41,7 @@ function parseArgv(args, schema) {
|
|
|
40
41
|
continue;
|
|
41
42
|
}
|
|
42
43
|
if (Object.hasOwn(options, alias.name)) {
|
|
43
|
-
throw
|
|
44
|
+
throw createDuplicateArgumentError(alias.name);
|
|
44
45
|
}
|
|
45
46
|
if (alias.definition.type === 'boolean') {
|
|
46
47
|
options[alias.name] = true;
|
|
@@ -65,7 +66,7 @@ function parseArgv(args, schema) {
|
|
|
65
66
|
? option
|
|
66
67
|
: option.slice(0, separatorIndex);
|
|
67
68
|
if (name === '') {
|
|
68
|
-
throw
|
|
69
|
+
throw createUnexpectedArgumentError(arg);
|
|
69
70
|
}
|
|
70
71
|
const definition = schema?.[name];
|
|
71
72
|
if (separatorIndex === -1 && definition === undefined && name.startsWith('no-')) {
|
|
@@ -73,14 +74,14 @@ function parseArgv(args, schema) {
|
|
|
73
74
|
const negatedDefinition = schema?.[negatedName];
|
|
74
75
|
if (negatedDefinition?.type === 'boolean') {
|
|
75
76
|
if (Object.hasOwn(options, negatedName)) {
|
|
76
|
-
throw
|
|
77
|
+
throw createDuplicateArgumentError(negatedName);
|
|
77
78
|
}
|
|
78
79
|
options[negatedName] = false;
|
|
79
80
|
continue;
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
if (Object.hasOwn(options, name)) {
|
|
83
|
-
throw
|
|
84
|
+
throw createDuplicateArgumentError(name);
|
|
84
85
|
}
|
|
85
86
|
if (separatorIndex !== -1) {
|
|
86
87
|
options[name] = option.slice(separatorIndex + 1);
|
|
@@ -120,7 +121,7 @@ function buildShortAliasMap(schema) {
|
|
|
120
121
|
}
|
|
121
122
|
assertShortAlias(name, definition.alias);
|
|
122
123
|
if (aliases.has(definition.alias)) {
|
|
123
|
-
throw
|
|
124
|
+
throw createDuplicateAliasError(definition.alias);
|
|
124
125
|
}
|
|
125
126
|
aliases.set(definition.alias, {
|
|
126
127
|
name,
|
|
@@ -133,8 +134,32 @@ function assertShortAlias(name, alias) {
|
|
|
133
134
|
if (/^[A-Za-z]$/.test(alias)) {
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
|
-
throw
|
|
137
|
+
throw createInvalidOptionAliasError(name, alias);
|
|
137
138
|
}
|
|
138
139
|
function isShortOptionToken(arg) {
|
|
139
140
|
return arg.length === 2 && arg.startsWith('-') && !arg.startsWith('--');
|
|
140
141
|
}
|
|
142
|
+
function createDuplicateArgumentError(name) {
|
|
143
|
+
return new errors_1.IcoreError('DUPLICATE_ARGUMENT', `Unexpected duplicate argument '--${name}'`, {
|
|
144
|
+
argument: `--${name}`,
|
|
145
|
+
option: name
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function createUnexpectedArgumentError(argument) {
|
|
149
|
+
return new errors_1.IcoreError('UNEXPECTED_ARGUMENT', `Unexpected argument '${argument}'`, {
|
|
150
|
+
argument
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function createDuplicateAliasError(alias) {
|
|
154
|
+
return new errors_1.IcoreError('DUPLICATE_ALIAS', `Unexpected duplicate alias '-${alias}'`, {
|
|
155
|
+
alias,
|
|
156
|
+
argument: `-${alias}`
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function createInvalidOptionAliasError(name, alias) {
|
|
160
|
+
return new errors_1.IcoreError('INVALID_OPTION_ALIAS', `Expected alias for '--${name}' as single ASCII letter`, {
|
|
161
|
+
argument: `--${name}`,
|
|
162
|
+
option: name,
|
|
163
|
+
alias
|
|
164
|
+
});
|
|
165
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -9,5 +9,6 @@
|
|
|
9
9
|
* This file must not contain parser, validator, or command runtime logic.
|
|
10
10
|
*/
|
|
11
11
|
export { parseArgv, type ParsedArgv } from './argv';
|
|
12
|
-
export { defineCommand, defineCommandRegistry, isCommandName, prepareCommandFromArgs, resolveCommand, resolveCommandFromArgs, runCommand, runPreparedCommand, runCommandFromRegistry, type CommandDefinition, type CommandInput, type CommandName, type CommandRegistry, type PreparedCommand, type PreparedCommandInput, type ResolvedCommand } from './commands';
|
|
12
|
+
export { defineCommand, defineCommandRegistry, isCommandName, prepareCommandFromArgs, resolveCommand, resolveCommandFromArgs, runCommand, runPreparedCommand, runCommandFromRegistry, type CommandDefinition, type CommandInput, type CommandName, type CommandRegistry, type CommandResolutionOptions, type PreparedCommand, type PreparedCommandInput, type ResolvedCommand } from './commands';
|
|
13
|
+
export { IcoreError, type IcoreErrorCode, type IcoreErrorDetails } from './errors';
|
|
13
14
|
export { mergeOptionsSchema, parseOptions, parseOptionsDetailed, type BooleanOption, type InferOptions, type InferProvidedOptions, type MergeOptionsSchemas, type NumberOption, type OptionDefinition, type OptionsSchema, type ParseOptionsResult, type RawOptionValue, type StringOption } from './options';
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* This file must not contain parser, validator, or command runtime logic.
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.parseOptionsDetailed = exports.parseOptions = exports.mergeOptionsSchema = exports.runCommandFromRegistry = exports.runPreparedCommand = exports.runCommand = exports.resolveCommandFromArgs = exports.resolveCommand = exports.prepareCommandFromArgs = exports.isCommandName = exports.defineCommandRegistry = exports.defineCommand = exports.parseArgv = void 0;
|
|
13
|
+
exports.parseOptionsDetailed = exports.parseOptions = exports.mergeOptionsSchema = exports.IcoreError = exports.runCommandFromRegistry = exports.runPreparedCommand = exports.runCommand = exports.resolveCommandFromArgs = exports.resolveCommand = exports.prepareCommandFromArgs = exports.isCommandName = exports.defineCommandRegistry = exports.defineCommand = exports.parseArgv = void 0;
|
|
14
14
|
var argv_1 = require("./argv");
|
|
15
15
|
Object.defineProperty(exports, "parseArgv", { enumerable: true, get: function () { return argv_1.parseArgv; } });
|
|
16
16
|
var commands_1 = require("./commands");
|
|
@@ -23,6 +23,8 @@ Object.defineProperty(exports, "resolveCommandFromArgs", { enumerable: true, get
|
|
|
23
23
|
Object.defineProperty(exports, "runCommand", { enumerable: true, get: function () { return commands_1.runCommand; } });
|
|
24
24
|
Object.defineProperty(exports, "runPreparedCommand", { enumerable: true, get: function () { return commands_1.runPreparedCommand; } });
|
|
25
25
|
Object.defineProperty(exports, "runCommandFromRegistry", { enumerable: true, get: function () { return commands_1.runCommandFromRegistry; } });
|
|
26
|
+
var errors_1 = require("./errors");
|
|
27
|
+
Object.defineProperty(exports, "IcoreError", { enumerable: true, get: function () { return errors_1.IcoreError; } });
|
|
26
28
|
var options_1 = require("./options");
|
|
27
29
|
Object.defineProperty(exports, "mergeOptionsSchema", { enumerable: true, get: function () { return options_1.mergeOptionsSchema; } });
|
|
28
30
|
Object.defineProperty(exports, "parseOptions", { enumerable: true, get: function () { return options_1.parseOptions; } });
|
package/dist/commands.d.ts
CHANGED
|
@@ -23,9 +23,15 @@ export type PreparedCommandInput<TSchema extends OptionsSchema> = {
|
|
|
23
23
|
/**
|
|
24
24
|
* Input passed to a command handler after command path and option validation.
|
|
25
25
|
*/
|
|
26
|
-
export type CommandInput<TSchema extends OptionsSchema, TContext> = PreparedCommandInput<TSchema> & {
|
|
26
|
+
export type CommandInput<TSchema extends OptionsSchema, TContext, TPayload = void> = PreparedCommandInput<TSchema> & {
|
|
27
27
|
context: TContext;
|
|
28
|
-
}
|
|
28
|
+
} & ([
|
|
29
|
+
TPayload
|
|
30
|
+
] extends [void] ? {
|
|
31
|
+
payload?: TPayload;
|
|
32
|
+
} : {
|
|
33
|
+
payload: TPayload;
|
|
34
|
+
});
|
|
29
35
|
/**
|
|
30
36
|
* Declarative command contract.
|
|
31
37
|
*
|
|
@@ -33,22 +39,49 @@ export type CommandInput<TSchema extends OptionsSchema, TContext> = PreparedComm
|
|
|
33
39
|
* application-specific work such as API calls, request building, and output
|
|
34
40
|
* formatting.
|
|
35
41
|
*/
|
|
36
|
-
export type CommandDefinition<TSchema extends OptionsSchema, TContext, TResult, TPath extends readonly [string, ...string[]] = readonly [string, ...string[]], TMetadata = unknown> = {
|
|
42
|
+
export type CommandDefinition<TSchema extends OptionsSchema, TContext, TResult, TPath extends readonly [string, ...string[]] = readonly [string, ...string[]], TMetadata = unknown, TPayload = void> = {
|
|
37
43
|
path: TPath;
|
|
38
44
|
options: TSchema;
|
|
39
45
|
metadata?: TMetadata;
|
|
40
46
|
allowExtraPositionals?: boolean;
|
|
41
|
-
prepare?(input: PreparedCommandInput<TSchema>):
|
|
42
|
-
handle(input: CommandInput<TSchema, TContext>): TResult | Promise<TResult>;
|
|
47
|
+
prepare?(input: PreparedCommandInput<TSchema>): TPayload | Promise<TPayload>;
|
|
48
|
+
handle(input: CommandInput<TSchema, TContext, TPayload>): TResult | Promise<TResult>;
|
|
49
|
+
};
|
|
50
|
+
type AnyCommandInput = PreparedCommandInput<OptionsSchema> & {
|
|
51
|
+
context: unknown;
|
|
52
|
+
payload?: unknown;
|
|
53
|
+
};
|
|
54
|
+
type AnyCommandDefinition = {
|
|
55
|
+
path: readonly [string, ...string[]];
|
|
56
|
+
options: OptionsSchema;
|
|
57
|
+
metadata?: unknown;
|
|
58
|
+
allowExtraPositionals?: boolean;
|
|
59
|
+
prepare?(input: PreparedCommandInput<OptionsSchema>): unknown | Promise<unknown>;
|
|
60
|
+
handle(input: AnyCommandInput): unknown | Promise<unknown>;
|
|
43
61
|
};
|
|
44
|
-
type AnyCommandDefinition = CommandDefinition<OptionsSchema, unknown, unknown, readonly [string, ...string[]], unknown>;
|
|
45
62
|
type CommandPathName<TPath extends readonly string[]> = number extends TPath['length'] ? string : TPath extends readonly [infer THead extends string] ? THead : TPath extends readonly [
|
|
46
63
|
infer THead extends string,
|
|
47
64
|
...infer TRest extends readonly string[]
|
|
48
65
|
] ? `${THead} ${CommandPathName<TRest>}` : never;
|
|
49
|
-
type
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
type CommandDefinitionParts<TCommand extends AnyCommandDefinition> = TCommand extends CommandDefinition<infer TSchema, infer TContext, infer TResult, infer TPath, infer TMetadata, infer TPayload> ? {
|
|
67
|
+
schema: TSchema;
|
|
68
|
+
context: TContext;
|
|
69
|
+
result: TResult;
|
|
70
|
+
path: TPath;
|
|
71
|
+
metadata: TMetadata;
|
|
72
|
+
payload: TPayload;
|
|
73
|
+
} : {
|
|
74
|
+
schema: TCommand['options'];
|
|
75
|
+
context: unknown;
|
|
76
|
+
result: unknown;
|
|
77
|
+
path: TCommand['path'];
|
|
78
|
+
metadata: TCommand['metadata'];
|
|
79
|
+
payload: unknown;
|
|
80
|
+
};
|
|
81
|
+
type CommandContext<TCommand extends AnyCommandDefinition> = CommandDefinitionParts<TCommand>['context'];
|
|
82
|
+
type CommandResult<TCommand extends AnyCommandDefinition> = Awaited<CommandDefinitionParts<TCommand>['result']>;
|
|
83
|
+
type CommandSchema<TCommand extends AnyCommandDefinition> = CommandDefinitionParts<TCommand>['schema'];
|
|
84
|
+
type CommandPayload<TCommand extends AnyCommandDefinition> = CommandDefinitionParts<TCommand>['payload'];
|
|
52
85
|
/**
|
|
53
86
|
* Infers the public command name from a command path.
|
|
54
87
|
*/
|
|
@@ -60,6 +93,17 @@ export type CommandRegistry<TCommands extends readonly AnyCommandDefinition[]> =
|
|
|
60
93
|
commands: TCommands;
|
|
61
94
|
commandNames: readonly CommandName<TCommands[number]>[];
|
|
62
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* Options for command resolution from raw CLI arguments.
|
|
98
|
+
*/
|
|
99
|
+
export type CommandResolutionOptions = {
|
|
100
|
+
/**
|
|
101
|
+
* When enabled, command path must appear before options. Options before the
|
|
102
|
+
* command path are rejected instead of participating in legacy candidate
|
|
103
|
+
* schema parsing.
|
|
104
|
+
*/
|
|
105
|
+
strict?: boolean;
|
|
106
|
+
};
|
|
63
107
|
/**
|
|
64
108
|
* Result of resolving a command from user positionals.
|
|
65
109
|
*/
|
|
@@ -79,11 +123,12 @@ export type PreparedCommand<TCommand extends AnyCommandDefinition> = {
|
|
|
79
123
|
options: InferOptions<CommandSchema<TCommand>>;
|
|
80
124
|
provided: InferProvidedOptions<CommandSchema<TCommand>>;
|
|
81
125
|
positionals: string[];
|
|
126
|
+
payload: CommandPayload<TCommand>;
|
|
82
127
|
};
|
|
83
128
|
/**
|
|
84
129
|
* Defines a command while preserving literal option schema types.
|
|
85
130
|
*/
|
|
86
|
-
export declare function defineCommand<const TSchema extends OptionsSchema, const TPath extends readonly [string, ...string[]], TContext = undefined, TResult = unknown, TMetadata = unknown>(command: CommandDefinition<TSchema, TContext, TResult, TPath, TMetadata>): CommandDefinition<TSchema, TContext, TResult, TPath, TMetadata>;
|
|
131
|
+
export declare function defineCommand<const TSchema extends OptionsSchema, const TPath extends readonly [string, ...string[]], TContext = undefined, TResult = unknown, TMetadata = unknown, TPayload = void>(command: CommandDefinition<TSchema, TContext, TResult, TPath, TMetadata, TPayload>): CommandDefinition<TSchema, TContext, TResult, TPath, TMetadata, TPayload>;
|
|
87
132
|
/**
|
|
88
133
|
* Defines a command registry while preserving literal command path types.
|
|
89
134
|
*/
|
|
@@ -103,7 +148,7 @@ export declare function resolveCommandFromArgs<const TCommands extends readonly
|
|
|
103
148
|
/**
|
|
104
149
|
* Resolves and validates a command without requiring runtime context.
|
|
105
150
|
*/
|
|
106
|
-
export declare function prepareCommandFromArgs<const TCommands extends readonly AnyCommandDefinition[]>(registry: CommandRegistry<TCommands>, args: readonly string[]): Promise<PreparedCommand<TCommands[number]>>;
|
|
151
|
+
export declare function prepareCommandFromArgs<const TCommands extends readonly AnyCommandDefinition[]>(registry: CommandRegistry<TCommands>, args: readonly string[], options?: CommandResolutionOptions): Promise<PreparedCommand<TCommands[number]>>;
|
|
107
152
|
/**
|
|
108
153
|
* Runs a prepared command with caller-provided runtime context.
|
|
109
154
|
*/
|
|
@@ -111,10 +156,10 @@ export declare function runPreparedCommand<TCommand extends AnyCommandDefinition
|
|
|
111
156
|
/**
|
|
112
157
|
* Resolves a command from a registry and runs its handler.
|
|
113
158
|
*/
|
|
114
|
-
export declare function runCommandFromRegistry<const TCommands extends readonly AnyCommandDefinition[]>(registry: CommandRegistry<TCommands>, args: readonly string[], context: CommandContext<TCommands[number]
|
|
159
|
+
export declare function runCommandFromRegistry<const TCommands extends readonly AnyCommandDefinition[]>(registry: CommandRegistry<TCommands>, args: readonly string[], context: CommandContext<TCommands[number]>, options?: CommandResolutionOptions): Promise<CommandResult<TCommands[number]>>;
|
|
115
160
|
/**
|
|
116
161
|
* Parses arguments, validates command mechanics, and executes a command
|
|
117
162
|
* handler.
|
|
118
163
|
*/
|
|
119
|
-
export declare function runCommand<const TSchema extends OptionsSchema, TContext, TResult>(command: CommandDefinition<TSchema, TContext, TResult>, args: readonly string[], context: TContext): Promise<TResult>;
|
|
164
|
+
export declare function runCommand<const TSchema extends OptionsSchema, TContext, TResult, const TPath extends readonly [string, ...string[]] = readonly [string, ...string[]], TMetadata = unknown, TPayload = void>(command: CommandDefinition<TSchema, TContext, TResult, TPath, TMetadata, TPayload>, args: readonly string[], context: TContext, options?: CommandResolutionOptions): Promise<TResult>;
|
|
120
165
|
export {};
|
package/dist/commands.js
CHANGED
|
@@ -22,6 +22,7 @@ exports.runPreparedCommand = runPreparedCommand;
|
|
|
22
22
|
exports.runCommandFromRegistry = runCommandFromRegistry;
|
|
23
23
|
exports.runCommand = runCommand;
|
|
24
24
|
const argv_1 = require("./argv");
|
|
25
|
+
const errors_1 = require("./errors");
|
|
25
26
|
const options_1 = require("./options");
|
|
26
27
|
/**
|
|
27
28
|
* Defines a command while preserving literal option schema types.
|
|
@@ -57,7 +58,7 @@ function resolveCommand(registry, positionals) {
|
|
|
57
58
|
return resolved;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
throw
|
|
61
|
+
throw createUnknownCommandError(positionals);
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
63
64
|
* Resolves a command from raw CLI arguments using each command's option schema.
|
|
@@ -70,12 +71,15 @@ function resolveCommandFromArgs(registry, args) {
|
|
|
70
71
|
return resolved;
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
|
-
throw
|
|
74
|
+
throw createUnknownCommandError((0, argv_1.parseArgv)(args).positionals);
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
77
|
* Resolves and validates a command without requiring runtime context.
|
|
77
78
|
*/
|
|
78
|
-
async function prepareCommandFromArgs(registry, args) {
|
|
79
|
+
async function prepareCommandFromArgs(registry, args, options = {}) {
|
|
80
|
+
if (options.strict === true) {
|
|
81
|
+
return prepareCommandFromArgsStrict(registry, args);
|
|
82
|
+
}
|
|
79
83
|
for (const command of commandsBySpecificity(registry.commands)) {
|
|
80
84
|
const argv = (0, argv_1.parseArgv)(args, command.options);
|
|
81
85
|
const resolved = resolveCommandCandidate(command, argv.positionals);
|
|
@@ -83,7 +87,7 @@ async function prepareCommandFromArgs(registry, args) {
|
|
|
83
87
|
return prepareResolvedCommand(resolved, argv.options);
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
|
-
throw
|
|
90
|
+
throw createUnknownCommandError((0, argv_1.parseArgv)(args).positionals);
|
|
87
91
|
}
|
|
88
92
|
/**
|
|
89
93
|
* Runs a prepared command with caller-provided runtime context.
|
|
@@ -93,26 +97,30 @@ async function runPreparedCommand(prepared, context) {
|
|
|
93
97
|
options: prepared.options,
|
|
94
98
|
provided: prepared.provided,
|
|
95
99
|
positionals: prepared.positionals,
|
|
96
|
-
context
|
|
100
|
+
context,
|
|
101
|
+
payload: prepared.payload
|
|
97
102
|
});
|
|
98
103
|
return result;
|
|
99
104
|
}
|
|
100
105
|
/**
|
|
101
106
|
* Resolves a command from a registry and runs its handler.
|
|
102
107
|
*/
|
|
103
|
-
async function runCommandFromRegistry(registry, args, context) {
|
|
104
|
-
const prepared = await prepareCommandFromArgs(registry, args);
|
|
108
|
+
async function runCommandFromRegistry(registry, args, context, options = {}) {
|
|
109
|
+
const prepared = await prepareCommandFromArgs(registry, args, options);
|
|
105
110
|
return runPreparedCommand(prepared, context);
|
|
106
111
|
}
|
|
107
112
|
/**
|
|
108
113
|
* Parses arguments, validates command mechanics, and executes a command
|
|
109
114
|
* handler.
|
|
110
115
|
*/
|
|
111
|
-
async function runCommand(command, args, context) {
|
|
112
|
-
const prepared = await prepareCommand(command, args);
|
|
116
|
+
async function runCommand(command, args, context, options = {}) {
|
|
117
|
+
const prepared = await prepareCommand(command, args, options);
|
|
113
118
|
return runPreparedCommand(prepared, context);
|
|
114
119
|
}
|
|
115
|
-
async function prepareCommand(command, args) {
|
|
120
|
+
async function prepareCommand(command, args, options = {}) {
|
|
121
|
+
if (options.strict === true) {
|
|
122
|
+
assertNoOptionBeforeCommand(args);
|
|
123
|
+
}
|
|
116
124
|
const argv = (0, argv_1.parseArgv)(args, command.options);
|
|
117
125
|
const extraPositionals = resolveCommandPositionals(command.path, argv.positionals);
|
|
118
126
|
const resolved = {
|
|
@@ -123,10 +131,27 @@ async function prepareCommand(command, args) {
|
|
|
123
131
|
};
|
|
124
132
|
return prepareResolvedCommand(resolved, argv.options);
|
|
125
133
|
}
|
|
134
|
+
async function prepareCommandFromArgsStrict(registry, args) {
|
|
135
|
+
const command = findStrictCommand(registry, args);
|
|
136
|
+
if (command === undefined) {
|
|
137
|
+
throw createUnknownCommandError(commandPositionalsBeforeOptions(args));
|
|
138
|
+
}
|
|
139
|
+
const argv = (0, argv_1.parseArgv)(args, command.options);
|
|
140
|
+
const resolved = resolveCommandCandidate(command, argv.positionals);
|
|
141
|
+
if (resolved === undefined) {
|
|
142
|
+
throw createUnknownCommandError(argv.positionals);
|
|
143
|
+
}
|
|
144
|
+
return prepareResolvedCommand(resolved, argv.options);
|
|
145
|
+
}
|
|
126
146
|
async function prepareResolvedCommand(resolved, rawOptions) {
|
|
127
147
|
const { command } = resolved;
|
|
128
148
|
if (resolved.positionals.length > 0 && command.allowExtraPositionals !== true) {
|
|
129
|
-
|
|
149
|
+
const positional = resolved.positionals[0] ?? '';
|
|
150
|
+
throw new errors_1.IcoreError('UNEXPECTED_POSITIONAL', `Unexpected positional argument for '${command.path.join(' ')}': ${positional}`, {
|
|
151
|
+
command: command.path.join(' '),
|
|
152
|
+
positional,
|
|
153
|
+
positionals: [...resolved.positionals]
|
|
154
|
+
});
|
|
130
155
|
}
|
|
131
156
|
const parsed = (0, options_1.parseOptionsDetailed)(command.options, rawOptions);
|
|
132
157
|
const input = {
|
|
@@ -134,20 +159,26 @@ async function prepareResolvedCommand(resolved, rawOptions) {
|
|
|
134
159
|
provided: parsed.provided,
|
|
135
160
|
positionals: resolved.positionals
|
|
136
161
|
};
|
|
137
|
-
await command.prepare?.(input);
|
|
162
|
+
const payload = await command.prepare?.(input);
|
|
138
163
|
return {
|
|
139
164
|
name: resolved.name,
|
|
140
165
|
path: resolved.path,
|
|
141
166
|
command,
|
|
142
167
|
options: parsed.options,
|
|
143
168
|
provided: parsed.provided,
|
|
144
|
-
positionals: resolved.positionals
|
|
169
|
+
positionals: resolved.positionals,
|
|
170
|
+
payload
|
|
145
171
|
};
|
|
146
172
|
}
|
|
147
173
|
function resolveCommandPositionals(path, positionals) {
|
|
148
174
|
for (let index = 0; index < path.length; index += 1) {
|
|
149
175
|
if (positionals[index] !== path[index]) {
|
|
150
|
-
|
|
176
|
+
const command = path.join(' ');
|
|
177
|
+
throw new errors_1.IcoreError('UNKNOWN_COMMAND', `Expected command '${command}'`, {
|
|
178
|
+
command,
|
|
179
|
+
path: [...path],
|
|
180
|
+
positionals: [...positionals]
|
|
181
|
+
});
|
|
151
182
|
}
|
|
152
183
|
}
|
|
153
184
|
return positionals.slice(path.length);
|
|
@@ -159,7 +190,7 @@ function assertNoDuplicateCommandNames(commandNames) {
|
|
|
159
190
|
const seen = new Set();
|
|
160
191
|
for (const name of commandNames) {
|
|
161
192
|
if (seen.has(name)) {
|
|
162
|
-
throw
|
|
193
|
+
throw createDuplicateCommandError(name);
|
|
163
194
|
}
|
|
164
195
|
seen.add(name);
|
|
165
196
|
}
|
|
@@ -167,6 +198,21 @@ function assertNoDuplicateCommandNames(commandNames) {
|
|
|
167
198
|
function commandsBySpecificity(commands) {
|
|
168
199
|
return [...commands].sort((left, right) => right.path.length - left.path.length);
|
|
169
200
|
}
|
|
201
|
+
function findStrictCommand(registry, args) {
|
|
202
|
+
assertNoOptionBeforeCommand(args);
|
|
203
|
+
for (const command of commandsBySpecificity(registry.commands)) {
|
|
204
|
+
if (commandPathMatchesArgs(command.path, args)) {
|
|
205
|
+
return command;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
function assertNoOptionBeforeCommand(args) {
|
|
211
|
+
const firstArg = args[0];
|
|
212
|
+
if (firstArg !== undefined && isOptionBeforeCommand(firstArg)) {
|
|
213
|
+
throw createUnexpectedArgumentError(firstArg);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
170
216
|
function resolveCommandCandidate(command, positionals) {
|
|
171
217
|
const extraPositionals = resolveMatchingCommandPositionals(command.path, positionals);
|
|
172
218
|
if (extraPositionals === undefined) {
|
|
@@ -190,3 +236,41 @@ function resolveMatchingCommandPositionals(path, positionals) {
|
|
|
190
236
|
function formatCommandPositionals(positionals) {
|
|
191
237
|
return positionals.length === 0 ? '<empty>' : positionals.join(' ');
|
|
192
238
|
}
|
|
239
|
+
function commandPathMatchesArgs(path, args) {
|
|
240
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
241
|
+
if (args[index] !== path[index]) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
function commandPositionalsBeforeOptions(args) {
|
|
248
|
+
const positionals = [];
|
|
249
|
+
for (const arg of args) {
|
|
250
|
+
if (isOptionBeforeCommand(arg)) {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
positionals.push(arg);
|
|
254
|
+
}
|
|
255
|
+
return positionals;
|
|
256
|
+
}
|
|
257
|
+
function isOptionBeforeCommand(arg) {
|
|
258
|
+
return arg !== '-' && arg.startsWith('-');
|
|
259
|
+
}
|
|
260
|
+
function createUnknownCommandError(positionals) {
|
|
261
|
+
const command = formatCommandPositionals(positionals);
|
|
262
|
+
return new errors_1.IcoreError('UNKNOWN_COMMAND', `Unknown command: ${command}`, {
|
|
263
|
+
command,
|
|
264
|
+
positionals: [...positionals]
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function createUnexpectedArgumentError(argument) {
|
|
268
|
+
return new errors_1.IcoreError('UNEXPECTED_ARGUMENT', `Unexpected argument '${argument}'`, {
|
|
269
|
+
argument
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function createDuplicateCommandError(command) {
|
|
273
|
+
return new errors_1.IcoreError('DUPLICATE_COMMAND', `Unexpected duplicate command '${command}'`, {
|
|
274
|
+
command
|
|
275
|
+
});
|
|
276
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error module defines machine-readable errors emitted by CLI mechanics.
|
|
3
|
+
*
|
|
4
|
+
* Allowed here:
|
|
5
|
+
* - defining stable error codes;
|
|
6
|
+
* - preserving human-readable messages on Error instances;
|
|
7
|
+
* - carrying structured details for application-level handling;
|
|
8
|
+
*
|
|
9
|
+
* This file must not contain parser, validator, or command resolution logic.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Stable machine-readable error code for `icore` CLI mechanics errors.
|
|
13
|
+
*/
|
|
14
|
+
export type IcoreErrorCode = 'UNKNOWN_COMMAND' | 'UNEXPECTED_ARGUMENT' | 'DUPLICATE_ARGUMENT' | 'EXPECTED_REQUIRED_ARGUMENT' | 'INVALID_OPTION_TYPE' | 'INVALID_OPTION_CHOICE' | 'UNEXPECTED_POSITIONAL' | 'INVALID_OPTION_ALIAS' | 'DUPLICATE_ALIAS' | 'INVALID_OPTION_DEFAULT' | 'DUPLICATE_COMMAND';
|
|
15
|
+
/**
|
|
16
|
+
* Structured context for application-level error handling.
|
|
17
|
+
*/
|
|
18
|
+
export type IcoreErrorDetails = Readonly<Record<string, unknown>>;
|
|
19
|
+
/**
|
|
20
|
+
* Error thrown by `icore` for CLI parsing, option validation, command
|
|
21
|
+
* resolution, and schema configuration failures.
|
|
22
|
+
*/
|
|
23
|
+
export declare class IcoreError extends Error {
|
|
24
|
+
readonly code: IcoreErrorCode;
|
|
25
|
+
readonly details: IcoreErrorDetails;
|
|
26
|
+
constructor(code: IcoreErrorCode, message: string, details?: IcoreErrorDetails);
|
|
27
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The error module defines machine-readable errors emitted by CLI mechanics.
|
|
4
|
+
*
|
|
5
|
+
* Allowed here:
|
|
6
|
+
* - defining stable error codes;
|
|
7
|
+
* - preserving human-readable messages on Error instances;
|
|
8
|
+
* - carrying structured details for application-level handling;
|
|
9
|
+
*
|
|
10
|
+
* This file must not contain parser, validator, or command resolution logic.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.IcoreError = void 0;
|
|
14
|
+
/**
|
|
15
|
+
* Error thrown by `icore` for CLI parsing, option validation, command
|
|
16
|
+
* resolution, and schema configuration failures.
|
|
17
|
+
*/
|
|
18
|
+
class IcoreError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
details;
|
|
21
|
+
constructor(code, message, details = {}) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'IcoreError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.details = details;
|
|
26
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.IcoreError = IcoreError;
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { defineCommand, defineCommandRegistry, isCommandName, mergeOptionsSchema, parseArgv, parseOptions, parseOptionsDetailed, prepareCommandFromArgs, resolveCommand, resolveCommandFromArgs, runCommand, runPreparedCommand, runCommandFromRegistry, type BooleanOption, type CommandDefinition, type CommandInput, type CommandName, type CommandRegistry, type InferOptions, type InferProvidedOptions, type MergeOptionsSchemas, type NumberOption, type OptionDefinition, type OptionsSchema, type ParseOptionsResult, type ParsedArgv, type PreparedCommand, type PreparedCommandInput, type RawOptionValue, type ResolvedCommand, type StringOption } from './cli';
|
|
1
|
+
export { defineCommand, defineCommandRegistry, IcoreError, isCommandName, mergeOptionsSchema, parseArgv, parseOptions, parseOptionsDetailed, prepareCommandFromArgs, resolveCommand, resolveCommandFromArgs, runCommand, runPreparedCommand, runCommandFromRegistry, type BooleanOption, type CommandDefinition, type CommandInput, type CommandName, type CommandRegistry, type CommandResolutionOptions, type InferOptions, type InferProvidedOptions, type IcoreErrorCode, type IcoreErrorDetails, type MergeOptionsSchemas, type NumberOption, type OptionDefinition, type OptionsSchema, type ParseOptionsResult, type ParsedArgv, type PreparedCommand, type PreparedCommandInput, type RawOptionValue, type ResolvedCommand, type StringOption } from './cli';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runCommandFromRegistry = exports.runPreparedCommand = exports.runCommand = exports.resolveCommandFromArgs = exports.resolveCommand = exports.prepareCommandFromArgs = exports.parseOptionsDetailed = exports.parseOptions = exports.parseArgv = exports.mergeOptionsSchema = exports.isCommandName = exports.defineCommandRegistry = exports.defineCommand = void 0;
|
|
3
|
+
exports.runCommandFromRegistry = exports.runPreparedCommand = exports.runCommand = exports.resolveCommandFromArgs = exports.resolveCommand = exports.prepareCommandFromArgs = exports.parseOptionsDetailed = exports.parseOptions = exports.parseArgv = exports.mergeOptionsSchema = exports.isCommandName = exports.IcoreError = exports.defineCommandRegistry = exports.defineCommand = void 0;
|
|
4
4
|
var cli_1 = require("./cli");
|
|
5
5
|
Object.defineProperty(exports, "defineCommand", { enumerable: true, get: function () { return cli_1.defineCommand; } });
|
|
6
6
|
Object.defineProperty(exports, "defineCommandRegistry", { enumerable: true, get: function () { return cli_1.defineCommandRegistry; } });
|
|
7
|
+
Object.defineProperty(exports, "IcoreError", { enumerable: true, get: function () { return cli_1.IcoreError; } });
|
|
7
8
|
Object.defineProperty(exports, "isCommandName", { enumerable: true, get: function () { return cli_1.isCommandName; } });
|
|
8
9
|
Object.defineProperty(exports, "mergeOptionsSchema", { enumerable: true, get: function () { return cli_1.mergeOptionsSchema; } });
|
|
9
10
|
Object.defineProperty(exports, "parseArgv", { enumerable: true, get: function () { return cli_1.parseArgv; } });
|
package/dist/options.d.ts
CHANGED
|
@@ -35,11 +35,11 @@ export type StringOption<TChoices extends readonly string[] = readonly string[]>
|
|
|
35
35
|
/**
|
|
36
36
|
* Declarative boolean flag contract.
|
|
37
37
|
*
|
|
38
|
-
* Boolean options accept flag form and
|
|
39
|
-
*
|
|
38
|
+
* Boolean options accept flag form and schema-known negation unless flag
|
|
39
|
+
* syntax is enforced.
|
|
40
40
|
*/
|
|
41
41
|
export type BooleanOption = OptionBase<'boolean', boolean> & {
|
|
42
|
-
|
|
42
|
+
syntax?: 'flag';
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
45
|
* Declarative number option contract.
|
package/dist/options.js
CHANGED
|
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.mergeOptionsSchema = mergeOptionsSchema;
|
|
16
16
|
exports.parseOptions = parseOptions;
|
|
17
17
|
exports.parseOptionsDetailed = parseOptionsDetailed;
|
|
18
|
+
const errors_1 = require("./errors");
|
|
18
19
|
/**
|
|
19
20
|
* Merges option schemas while preserving literal option definition types.
|
|
20
21
|
*
|
|
@@ -38,7 +39,7 @@ function parseOptionsDetailed(schema, values) {
|
|
|
38
39
|
const provided = {};
|
|
39
40
|
for (const name of Object.keys(values)) {
|
|
40
41
|
if (!Object.hasOwn(schema, name)) {
|
|
41
|
-
throw
|
|
42
|
+
throw createUnexpectedArgumentError(name);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
for (const name of Object.keys(schema)) {
|
|
@@ -54,7 +55,7 @@ function parseOptionsDetailed(schema, values) {
|
|
|
54
55
|
continue;
|
|
55
56
|
}
|
|
56
57
|
if (definition.required === true) {
|
|
57
|
-
throw
|
|
58
|
+
throw createExpectedRequiredArgumentError(String(name));
|
|
58
59
|
}
|
|
59
60
|
parsed[name] = undefined;
|
|
60
61
|
continue;
|
|
@@ -79,74 +80,157 @@ function parseDefaultOptionValue(name, definition) {
|
|
|
79
80
|
const value = definition.default;
|
|
80
81
|
if (definition.type === 'string') {
|
|
81
82
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
82
|
-
throw
|
|
83
|
+
throw createExpectedDefaultError(name, 'string', value);
|
|
83
84
|
}
|
|
84
|
-
assertChoice(name, definition.choices, value);
|
|
85
|
+
assertChoice(name, definition.choices, value, 'default');
|
|
85
86
|
return value;
|
|
86
87
|
}
|
|
87
88
|
if (definition.type === 'boolean') {
|
|
88
89
|
if (typeof value !== 'boolean') {
|
|
89
|
-
throw
|
|
90
|
+
throw createExpectedDefaultError(name, 'boolean', value);
|
|
90
91
|
}
|
|
91
92
|
return value;
|
|
92
93
|
}
|
|
93
94
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
94
|
-
throw
|
|
95
|
+
throw createExpectedDefaultError(name, 'number', value);
|
|
95
96
|
}
|
|
96
|
-
validateNumberConstraints(name, definition, value);
|
|
97
|
+
validateNumberConstraints(name, definition, value, 'default');
|
|
97
98
|
return value;
|
|
98
99
|
}
|
|
99
100
|
function parseStringOption(name, definition, value) {
|
|
100
101
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
101
|
-
throw
|
|
102
|
+
throw createExpectedOptionTypeError(name, 'string', value);
|
|
102
103
|
}
|
|
103
104
|
assertChoice(name, definition.choices, value);
|
|
104
105
|
return value;
|
|
105
106
|
}
|
|
106
107
|
function parseBooleanOption(name, definition, value) {
|
|
107
|
-
if (definition.
|
|
108
|
+
if (definition.syntax === 'flag') {
|
|
108
109
|
if (value === true) {
|
|
109
110
|
return true;
|
|
110
111
|
}
|
|
111
|
-
throw
|
|
112
|
+
throw createExpectedOptionTypeError(name, 'boolean flag', value);
|
|
112
113
|
}
|
|
113
|
-
if (value === true
|
|
114
|
+
if (value === true) {
|
|
114
115
|
return true;
|
|
115
116
|
}
|
|
116
|
-
if (value === false
|
|
117
|
+
if (value === false) {
|
|
117
118
|
return false;
|
|
118
119
|
}
|
|
119
|
-
throw
|
|
120
|
+
throw createExpectedOptionTypeError(name, 'boolean flag', value);
|
|
120
121
|
}
|
|
121
122
|
function parseNumberOption(name, definition, value) {
|
|
122
123
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
123
|
-
throw
|
|
124
|
+
throw createExpectedOptionTypeError(name, 'number', value);
|
|
124
125
|
}
|
|
125
126
|
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
|
|
126
|
-
throw
|
|
127
|
+
throw createExpectedOptionTypeError(name, 'number', value);
|
|
127
128
|
}
|
|
128
129
|
const parsed = Number(value);
|
|
129
130
|
if (!Number.isFinite(parsed)) {
|
|
130
|
-
throw
|
|
131
|
+
throw createExpectedOptionTypeError(name, 'number', value);
|
|
131
132
|
}
|
|
132
133
|
validateNumberConstraints(name, definition, parsed);
|
|
133
134
|
return parsed;
|
|
134
135
|
}
|
|
135
|
-
function validateNumberConstraints(name, definition, parsed) {
|
|
136
|
+
function validateNumberConstraints(name, definition, parsed, source = 'value') {
|
|
136
137
|
if (definition.integer === true && !Number.isInteger(parsed)) {
|
|
137
|
-
|
|
138
|
+
if (source === 'default') {
|
|
139
|
+
throw createInvalidOptionDefaultError(name, `Expected '--${name}' as integer`, {
|
|
140
|
+
expected: 'integer',
|
|
141
|
+
value: parsed
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
throw createExpectedOptionTypeError(name, 'integer', parsed);
|
|
138
145
|
}
|
|
139
146
|
if (definition.min !== undefined && parsed < definition.min) {
|
|
140
|
-
|
|
147
|
+
const message = `Expected '--${name}' to be greater than or equal to ${String(definition.min)}`;
|
|
148
|
+
if (source === 'default') {
|
|
149
|
+
throw createInvalidOptionDefaultError(name, message, {
|
|
150
|
+
expected: 'minimum',
|
|
151
|
+
min: definition.min,
|
|
152
|
+
value: parsed
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
throw createInvalidOptionTypeError(name, message, {
|
|
156
|
+
expected: 'minimum',
|
|
157
|
+
min: definition.min,
|
|
158
|
+
value: parsed
|
|
159
|
+
});
|
|
141
160
|
}
|
|
142
161
|
if (definition.max !== undefined && parsed > definition.max) {
|
|
143
|
-
|
|
162
|
+
const message = `Expected '--${name}' to be less than or equal to ${String(definition.max)}`;
|
|
163
|
+
if (source === 'default') {
|
|
164
|
+
throw createInvalidOptionDefaultError(name, message, {
|
|
165
|
+
expected: 'maximum',
|
|
166
|
+
max: definition.max,
|
|
167
|
+
value: parsed
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
throw createInvalidOptionTypeError(name, message, {
|
|
171
|
+
expected: 'maximum',
|
|
172
|
+
max: definition.max,
|
|
173
|
+
value: parsed
|
|
174
|
+
});
|
|
144
175
|
}
|
|
145
|
-
assertChoice(name, definition.choices, parsed);
|
|
176
|
+
assertChoice(name, definition.choices, parsed, source);
|
|
146
177
|
}
|
|
147
|
-
function assertChoice(name, choices, value) {
|
|
178
|
+
function assertChoice(name, choices, value, source = 'value') {
|
|
148
179
|
if (choices === undefined || choices.includes(value)) {
|
|
149
180
|
return;
|
|
150
181
|
}
|
|
151
|
-
|
|
182
|
+
const message = `Expected '--${name}' as one of: ${choices.join(', ')}`;
|
|
183
|
+
if (source === 'default') {
|
|
184
|
+
throw createInvalidOptionDefaultError(name, message, {
|
|
185
|
+
choices: [...choices],
|
|
186
|
+
value
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
throw createInvalidOptionChoiceError(name, choices, value, message);
|
|
190
|
+
}
|
|
191
|
+
function createUnexpectedArgumentError(name) {
|
|
192
|
+
return new errors_1.IcoreError('UNEXPECTED_ARGUMENT', `Unexpected argument '--${name}'`, {
|
|
193
|
+
argument: `--${name}`,
|
|
194
|
+
option: name
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function createExpectedRequiredArgumentError(name) {
|
|
198
|
+
return new errors_1.IcoreError('EXPECTED_REQUIRED_ARGUMENT', `Expected required argument '--${name}'`, {
|
|
199
|
+
argument: `--${name}`,
|
|
200
|
+
option: name
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function createExpectedOptionTypeError(name, expected, value) {
|
|
204
|
+
return createInvalidOptionTypeError(name, `Expected '--${name}' as ${expected}`, {
|
|
205
|
+
expected,
|
|
206
|
+
value
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function createExpectedDefaultError(name, expected, value) {
|
|
210
|
+
return createInvalidOptionDefaultError(name, `Expected default for '--${name}' as ${expected}`, {
|
|
211
|
+
expected,
|
|
212
|
+
value
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function createInvalidOptionTypeError(name, message, details) {
|
|
216
|
+
return new errors_1.IcoreError('INVALID_OPTION_TYPE', message, {
|
|
217
|
+
argument: `--${name}`,
|
|
218
|
+
option: name,
|
|
219
|
+
...details
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function createInvalidOptionChoiceError(name, choices, value, message) {
|
|
223
|
+
return new errors_1.IcoreError('INVALID_OPTION_CHOICE', message, {
|
|
224
|
+
argument: `--${name}`,
|
|
225
|
+
option: name,
|
|
226
|
+
choices: [...choices],
|
|
227
|
+
value
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function createInvalidOptionDefaultError(name, message, details) {
|
|
231
|
+
return new errors_1.IcoreError('INVALID_OPTION_DEFAULT', message, {
|
|
232
|
+
argument: `--${name}`,
|
|
233
|
+
option: name,
|
|
234
|
+
...details
|
|
235
|
+
});
|
|
152
236
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -10,7 +10,7 @@ Small dependency-free command line interface mechanics for [Node.js®](https://n
|
|
|
10
10
|
Supports a practical GNU-style option syntax:
|
|
11
11
|
|
|
12
12
|
- long options: `--name value`, `--name=value`;
|
|
13
|
-
- boolean flags: `--flag`, `--
|
|
13
|
+
- boolean flags: `--flag`, `--no-flag`;
|
|
14
14
|
- short aliases: `-f`, `-n value`;
|
|
15
15
|
- option terminator: `--`.
|
|
16
16
|
|
|
@@ -34,9 +34,10 @@ npm install icore
|
|
|
34
34
|
- [`isCommandName(registry, value)`](#iscommandnameregistry-value)
|
|
35
35
|
- [`resolveCommand(registry, positionals)`](#resolvecommandregistry-positionals)
|
|
36
36
|
- [`resolveCommandFromArgs(registry, args)`](#resolvecommandfromargsregistry-args)
|
|
37
|
-
- [`
|
|
37
|
+
- [`prepareCommandFromArgs(registry, args, options?)`](#preparecommandfromargsregistry-args-options)
|
|
38
|
+
- [`runCommandFromRegistry(registry, args, context, options?)`](#runcommandfromregistryregistry-args-context-options)
|
|
38
39
|
- [`mergeOptionsSchema(...schemas)`](#mergeoptionsschemaschemas)
|
|
39
|
-
- [`runCommand(command, args, context)`](#runcommandcommand-args-context)
|
|
40
|
+
- [`runCommand(command, args, context, options?)`](#runcommandcommand-args-context-options)
|
|
40
41
|
- [How It Works](#how-it-works)
|
|
41
42
|
- [Example](#example)
|
|
42
43
|
- [Option Schemas](#option-schemas)
|
|
@@ -278,7 +279,71 @@ const resolved = resolveCommandFromArgs(registry, [
|
|
|
278
279
|
]);
|
|
279
280
|
```
|
|
280
281
|
|
|
281
|
-
|
|
282
|
+
`resolveCommandFromArgs` keeps legacy option-anywhere command resolution. Use
|
|
283
|
+
`prepareCommandFromArgs(..., { strict: true })` when command path diagnostics
|
|
284
|
+
should reject options before the command path.
|
|
285
|
+
|
|
286
|
+
### `prepareCommandFromArgs(registry, args, options?)`
|
|
287
|
+
|
|
288
|
+
Resolves and validates a command without requiring runtime context or calling
|
|
289
|
+
the command handler.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const prepared = await prepareCommandFromArgs(
|
|
293
|
+
registry,
|
|
294
|
+
['hello', '--name', 'Alice', '--uppercase'],
|
|
295
|
+
{
|
|
296
|
+
strict: true
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
A command `prepare` hook can return a typed payload. The payload is available
|
|
302
|
+
on the prepared command before runtime context is created, then passed to the
|
|
303
|
+
handler:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
const helloCommand = defineCommand({
|
|
307
|
+
path: ['hello'],
|
|
308
|
+
options: {
|
|
309
|
+
name: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
required: true
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
prepare({ options }) {
|
|
315
|
+
return {
|
|
316
|
+
normalizedName: options.name.trim().toLowerCase()
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
handle({ payload, context }) {
|
|
320
|
+
return context.greeter.greet(payload.normalizedName);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const prepared = await prepareCommandFromArgs(registry, [
|
|
325
|
+
'hello',
|
|
326
|
+
'--name',
|
|
327
|
+
' Alice '
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
prepared.payload.normalizedName;
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Use payload for derived CLI data. Runtime resources such as databases, sockets,
|
|
334
|
+
or SDK clients should still be created outside `icore` and passed as `context`.
|
|
335
|
+
|
|
336
|
+
With `strict: true`, the command path must appear before options:
|
|
337
|
+
|
|
338
|
+
```console
|
|
339
|
+
$ node cli.js --unknown hello
|
|
340
|
+
Unexpected argument '--unknown'
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Without strict mode, legacy candidate-schema resolution is preserved for
|
|
344
|
+
backward compatibility.
|
|
345
|
+
|
|
346
|
+
### `runCommandFromRegistry(registry, args, context, options?)`
|
|
282
347
|
|
|
283
348
|
Resolves a command from a registry and runs its handler.
|
|
284
349
|
|
|
@@ -286,7 +351,10 @@ Resolves a command from a registry and runs its handler.
|
|
|
286
351
|
const output = await runCommandFromRegistry(
|
|
287
352
|
registry,
|
|
288
353
|
['hello', '--name', 'Alice', '--uppercase'],
|
|
289
|
-
context
|
|
354
|
+
context,
|
|
355
|
+
{
|
|
356
|
+
strict: true
|
|
357
|
+
}
|
|
290
358
|
);
|
|
291
359
|
```
|
|
292
360
|
|
|
@@ -317,7 +385,7 @@ const greetingOptions = {
|
|
|
317
385
|
const options = mergeOptionsSchema(nameOptions, greetingOptions);
|
|
318
386
|
```
|
|
319
387
|
|
|
320
|
-
### `runCommand(command, args, context)`
|
|
388
|
+
### `runCommand(command, args, context, options?)`
|
|
321
389
|
|
|
322
390
|
Parses arguments, validates options, checks command positionals, and runs the
|
|
323
391
|
handler.
|
|
@@ -326,13 +394,19 @@ handler.
|
|
|
326
394
|
const output = await runCommand(
|
|
327
395
|
command,
|
|
328
396
|
['hello', '--name', 'Alice', '--uppercase'],
|
|
329
|
-
context
|
|
397
|
+
context,
|
|
398
|
+
{
|
|
399
|
+
strict: true
|
|
400
|
+
}
|
|
330
401
|
);
|
|
331
402
|
```
|
|
332
403
|
|
|
333
404
|
**By default**, extra positionals are rejected. A command can opt in to extra
|
|
334
405
|
positionals with `allowExtraPositionals: true`.
|
|
335
406
|
|
|
407
|
+
With `strict: true`, direct command execution also requires the command path to
|
|
408
|
+
appear before options.
|
|
409
|
+
|
|
336
410
|
## How It Works
|
|
337
411
|
|
|
338
412
|

|
|
@@ -378,7 +452,7 @@ HELLO, ALICE!
|
|
|
378
452
|
```
|
|
379
453
|
|
|
380
454
|
The command handler receives parsed options, user-provided option metadata,
|
|
381
|
-
remaining positionals, and caller provided context.
|
|
455
|
+
remaining positionals, prepared payload, and caller provided context.
|
|
382
456
|
|
|
383
457
|
## Option Schemas
|
|
384
458
|
|
|
@@ -435,19 +509,18 @@ const schema = {
|
|
|
435
509
|
} as const;
|
|
436
510
|
```
|
|
437
511
|
|
|
438
|
-
Boolean options accept **flag form
|
|
439
|
-
schema-known negation:
|
|
512
|
+
Boolean options accept **flag form** and schema-known negation:
|
|
440
513
|
|
|
441
514
|
```sh
|
|
442
515
|
--uppercase
|
|
443
|
-
--uppercase=true
|
|
444
|
-
--uppercase=false
|
|
445
516
|
--no-uppercase
|
|
446
517
|
```
|
|
447
518
|
|
|
448
|
-
|
|
519
|
+
Explicit values are rejected:
|
|
449
520
|
|
|
450
521
|
```sh
|
|
522
|
+
--uppercase=true
|
|
523
|
+
--uppercase=false
|
|
451
524
|
--uppercase=yes
|
|
452
525
|
--uppercase=
|
|
453
526
|
```
|
|
@@ -455,20 +528,20 @@ Invalid explicit values are rejected:
|
|
|
455
528
|
`--uppercase false` keeps `--uppercase` as `true` and leaves `false` as a positional
|
|
456
529
|
argument.
|
|
457
530
|
|
|
458
|
-
Use `
|
|
531
|
+
Use `syntax: 'flag'` when a boolean option should accept only flag form:
|
|
459
532
|
|
|
460
533
|
```ts
|
|
461
534
|
const schema = {
|
|
462
535
|
uppercase: {
|
|
463
536
|
type: 'boolean',
|
|
464
537
|
default: false,
|
|
465
|
-
|
|
538
|
+
syntax: 'flag'
|
|
466
539
|
}
|
|
467
540
|
} as const;
|
|
468
541
|
```
|
|
469
542
|
|
|
470
|
-
With `
|
|
471
|
-
|
|
543
|
+
With `syntax: 'flag'`, `--uppercase` is accepted, while `--uppercase=value`
|
|
544
|
+
and `--no-uppercase` are rejected.
|
|
472
545
|
|
|
473
546
|
### `type: 'number'`
|
|
474
547
|
|
|
@@ -567,15 +640,51 @@ Attached short values such as `-nvalue` and grouped short booleans such as
|
|
|
567
640
|
compatibility.
|
|
568
641
|
|
|
569
642
|
Negated syntax such as `--no-cache` is interpreted as `cache: false` when
|
|
570
|
-
`cache` is a known boolean option without `
|
|
643
|
+
`cache` is a known boolean option without `syntax: 'flag'`. Unknown negated
|
|
571
644
|
options, negation for string or number options, and negation for flag-only
|
|
572
645
|
boolean options are rejected.
|
|
573
646
|
|
|
574
647
|
## Error Messages
|
|
575
648
|
|
|
576
|
-
`icore` throws
|
|
577
|
-
|
|
578
|
-
|
|
649
|
+
`icore` throws `IcoreError` objects for CLI parsing, option validation, and
|
|
650
|
+
command resolution failures. `IcoreError` extends the regular `Error` class and
|
|
651
|
+
adds a stable machine-readable `code` plus structured `details`.
|
|
652
|
+
|
|
653
|
+
Applications should treat `error.message` as **display text**. Use `error.code`
|
|
654
|
+
for machine-readable handling:
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
import { IcoreError } from 'icore';
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
await main(args);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
if (error instanceof IcoreError && error.code === 'UNKNOWN_COMMAND') {
|
|
663
|
+
printHelp();
|
|
664
|
+
process.exitCode = 2;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Supported error codes:
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
type IcoreErrorCode =
|
|
676
|
+
| 'UNKNOWN_COMMAND'
|
|
677
|
+
| 'UNEXPECTED_ARGUMENT'
|
|
678
|
+
| 'DUPLICATE_ARGUMENT'
|
|
679
|
+
| 'EXPECTED_REQUIRED_ARGUMENT'
|
|
680
|
+
| 'INVALID_OPTION_TYPE'
|
|
681
|
+
| 'INVALID_OPTION_CHOICE'
|
|
682
|
+
| 'UNEXPECTED_POSITIONAL'
|
|
683
|
+
| 'INVALID_OPTION_ALIAS'
|
|
684
|
+
| 'DUPLICATE_ALIAS'
|
|
685
|
+
| 'INVALID_OPTION_DEFAULT'
|
|
686
|
+
| 'DUPLICATE_COMMAND';
|
|
687
|
+
```
|
|
579
688
|
|
|
580
689
|
Applications can catch these errors and decide how to print them. For example,
|
|
581
690
|
after printing `error.message`, terminal output can look like this:
|
|
@@ -591,6 +700,9 @@ $ node cli.js hello --name=
|
|
|
591
700
|
Expected '--name' as string
|
|
592
701
|
```
|
|
593
702
|
|
|
703
|
+
Errors thrown by command `prepare` and `handle` functions are application errors
|
|
704
|
+
and pass through unchanged.
|
|
705
|
+
|
|
594
706
|
## Project Boundary
|
|
595
707
|
|
|
596
708
|
`icore` is intended to be a **small CLI mechanics module**. It should **not**
|