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 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 new Error(`Unexpected duplicate argument '--${alias.name}'`);
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 new Error(`Unexpected argument '${arg}'`);
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 new Error(`Unexpected duplicate argument '--${negatedName}'`);
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 new Error(`Unexpected duplicate argument '--${name}'`);
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 new Error(`Unexpected duplicate alias '-${definition.alias}'`);
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 new Error(`Expected alias for '--${name}' as single ASCII letter`);
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; } });
@@ -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>): void | Promise<void>;
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 CommandContext<TCommand extends AnyCommandDefinition> = TCommand extends CommandDefinition<OptionsSchema, infer TContext, unknown> ? TContext : never;
50
- type CommandResult<TCommand extends AnyCommandDefinition> = TCommand extends CommandDefinition<OptionsSchema, unknown, infer TResult> ? Awaited<TResult> : never;
51
- type CommandSchema<TCommand extends AnyCommandDefinition> = TCommand extends CommandDefinition<infer TSchema, unknown, unknown> ? TSchema : never;
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]>): Promise<CommandResult<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 new Error(`Unknown command: ${formatCommandPositionals(positionals)}`);
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 new Error(`Unknown command: ${formatCommandPositionals((0, argv_1.parseArgv)(args).positionals)}`);
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 new Error(`Unknown command: ${formatCommandPositionals((0, argv_1.parseArgv)(args).positionals)}`);
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
- throw new Error(`Unexpected positional argument for '${command.path.join(' ')}': ${resolved.positionals[0] ?? ''}`);
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
- throw new Error(`Expected command '${path.join(' ')}'`);
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 new Error(`Unexpected duplicate command '${name}'`);
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
+ }
@@ -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 explicit `true` / `false` values unless
39
- * `flagOnly` is enabled.
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
- flagOnly?: boolean;
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 new Error(`Unexpected argument '--${name}'`);
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 new Error(`Expected required argument '--${String(name)}'`);
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 new Error(`Expected default for '--${name}' as string`);
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 new Error(`Expected default for '--${name}' as boolean`);
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 new Error(`Expected default for '--${name}' as number`);
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 new Error(`Expected '--${name}' as string`);
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.flagOnly === true) {
108
+ if (definition.syntax === 'flag') {
108
109
  if (value === true) {
109
110
  return true;
110
111
  }
111
- throw new Error(`Expected '--${name}' as boolean flag`);
112
+ throw createExpectedOptionTypeError(name, 'boolean flag', value);
112
113
  }
113
- if (value === true || value === 'true') {
114
+ if (value === true) {
114
115
  return true;
115
116
  }
116
- if (value === false || value === 'false') {
117
+ if (value === false) {
117
118
  return false;
118
119
  }
119
- throw new Error(`Expected '--${name}' as boolean flag`);
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 new Error(`Expected '--${name}' as number`);
124
+ throw createExpectedOptionTypeError(name, 'number', value);
124
125
  }
125
126
  if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
126
- throw new Error(`Expected '--${name}' as number`);
127
+ throw createExpectedOptionTypeError(name, 'number', value);
127
128
  }
128
129
  const parsed = Number(value);
129
130
  if (!Number.isFinite(parsed)) {
130
- throw new Error(`Expected '--${name}' as number`);
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
- throw new Error(`Expected '--${name}' as integer`);
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
- throw new Error(`Expected '--${name}' to be greater than or equal to ${String(definition.min)}`);
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
- throw new Error(`Expected '--${name}' to be less than or equal to ${String(definition.max)}`);
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
- throw new Error(`Expected '--${name}' as one of: ${choices.join(', ')}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icore",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Declarative command line interface mechanics for Node.js",
5
5
  "keywords": [
6
6
  "command-line",
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`, `--flag=true`, `--flag=false`, `--no-flag` by default;
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
- - [`runCommandFromRegistry(registry, args, context)`](#runcommandfromregistryregistry-args-context)
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
- ### `runCommandFromRegistry(registry, args, context)`
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
  ![yuml diagram](http://yuml.me/diagram/scruffy;dir:LR;/class/[*argv*%20{bg:gray}|External;hello%20--name%20Alice%20--uppercase]->[*matches*%20{bg:lavender}|System;parse,%20resolve,%20validate,%20infer]->[*typed%20result*%20{bg:honeydew}|Container;command=hello;%20name=Alice;%20uppercase=true]->[*your%20app*%20{bg:cornsilk}|System;business%20logic%20and%20output])
@@ -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**, explicit `true` / `false` values, and
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
- Invalid explicit values are rejected:
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 `flagOnly: true` when a boolean option should accept only flag form:
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
- flagOnly: true
538
+ syntax: 'flag'
466
539
  }
467
540
  } as const;
468
541
  ```
469
542
 
470
- With `flagOnly: true`, `--uppercase` is accepted, while `--uppercase=true`,
471
- `--uppercase=false`, and `--no-uppercase` are rejected.
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 `flagOnly: true`. Unknown negated
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 regular `Error` objects with predictable user-facing messages.
577
- Applications should treat these messages as **display text**, not as a
578
- **machine-readable API**.
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**