cli-kiss 0.2.1 → 0.2.3

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.
@@ -9,28 +9,40 @@ import {
9
9
  } from "./Typo";
10
10
 
11
11
  /**
12
- * Describes a single positional argument a bare (non-option) token on the command
13
- * line — together with its parsing and usage-generation logic.
12
+ * A bare (non-option) positional argument with its parsing and usage-generation logic.
14
13
  *
15
- * Positionals are created with {@link positionalRequired}, {@link positionalOptional}, or
16
- * {@link positionalVariadics} and are passed via the `positionals` array of
17
- * {@link operation}, where they are consumed in declaration order.
14
+ * Created with {@link positionalRequired}, {@link positionalOptional}, or
15
+ * {@link positionalVariadics} and passed via the `positionals` array of
16
+ * {@link operation}, consumed in declaration order.
18
17
  *
19
- * @typeParam Value - The TypeScript type of the parsed positional value.
18
+ * @typeParam Value - Decoded value type.
20
19
  */
21
20
  export type Positional<Value> = {
22
- /** Returns human-readable metadata used to render the `Positionals:` section of help. */
21
+ /**
22
+ * Returns metadata used to render the `Positionals:` section of help.
23
+ */
23
24
  generateUsage(): PositionalUsage;
24
25
  /**
25
- * Consumes the next positional token(s) from `readerPositionals` and returns the
26
- * decoded value.
26
+ * Consumes the next positional token from `readerPositionals`.
27
+ * Returns a decoder that produces the final value.
28
+ */
29
+ consumeAndMakeDecoder(
30
+ readerPositionals: ReaderPositionals,
31
+ ): PositionalDecoder<Value>;
32
+ };
33
+
34
+ /**
35
+ * Produced by {@link Positional.consumeAndMakeDecoder}.
36
+ *
37
+ * @typeParam Value - Decoded value type.
38
+ */
39
+ export type PositionalDecoder<Value> = {
40
+ /**
41
+ * Returns the decoded positional value.
27
42
  *
28
- * @param readerPositionals - The shared {@link ReaderArgs} that manages the queue of
29
- * remaining positional tokens.
30
- * @throws {@link TypoError} if the positional is required but absent, or if the raw
31
- * value fails type decoding.
43
+ * @throws {@link TypoError} if decoding failed.
32
44
  */
33
- consumePositionals(readerPositionals: ReaderPositionals): Value;
45
+ decodeValue(): Value;
34
46
  };
35
47
 
36
48
  /**
@@ -38,40 +50,32 @@ export type Positional<Value> = {
38
50
  * `Positionals:` section of the help output produced by {@link usageToStyledLines}.
39
51
  */
40
52
  export type PositionalUsage = {
41
- /** Short description of what the positional represents. */
53
+ /**
54
+ * Help text.
55
+ */
42
56
  description: string | undefined;
43
57
  /**
44
- * Optional supplementary note shown in parentheses next to the description.
45
- * Suitable for short caveats such as `"defaults to 'world'"`.
58
+ * Short note shown in parentheses.
46
59
  */
47
60
  hint: string | undefined;
48
61
  /**
49
- * The placeholder label shown in the usage line and the `Positionals:` section.
50
- * Required positionals use angle-bracket notation (e.g. `"<NAME>"`); optional ones
51
- * use square-bracket notation (e.g. `"[FILE]"`); variadic ones append `...`
52
- * (e.g. `"[ITEM]..."`).
62
+ * Placeholder label shown in the usage line and the `Positionals:` section.
63
+ * Required: `<NAME>`, optional: `[NAME]`, variadic: `[NAME]...`.
53
64
  */
54
65
  label: Uppercase<string>;
55
66
  };
56
67
 
57
68
  /**
58
- * Creates a required positional argument one that must be present on the command line.
69
+ * Creates a required positional — missing token throws {@link TypoError}.
70
+ * Label defaults to uppercased `type.content` in angle brackets (e.g. `<STRING>`).
59
71
  *
60
- * The parser consumes the next available positional token and decodes it with
61
- * `definition.type`. If no token is available, a {@link TypoError} is thrown immediately
62
- * during parsing (i.e. inside {@link OperationDescriptor.createFactory}).
72
+ * @typeParam Value - Type produced by the decoder.
63
73
  *
64
- * The label displayed in the usage line defaults to the uppercased `type.content`
65
- * wrapped in angle brackets (e.g. `<STRING>`). Supply `label` to override.
66
- *
67
- * @typeParam Value - The TypeScript type produced by the type decoder.
68
- *
69
- * @param definition - Configuration for the positional.
70
- * @param definition.description - Human-readable description for the help output.
71
- * @param definition.hint - Optional supplementary note shown in parentheses.
72
- * @param definition.label - Custom label shown in the usage line (without angle brackets).
73
- * Defaults to the uppercased `type.content`.
74
- * @param definition.type - The {@link Type} used to decode the raw string token.
74
+ * @param definition - Positional configuration.
75
+ * @param definition.description - Help text.
76
+ * @param definition.hint - Short note shown in parentheses.
77
+ * @param definition.label - Label without brackets; defaults to uppercased `type.content`.
78
+ * @param definition.type - Decoder for the raw token.
75
79
  * @returns A {@link Positional}`<Value>`.
76
80
  *
77
81
  * @example
@@ -99,7 +103,7 @@ export function positionalRequired<Value>(definition: {
99
103
  label: label as Uppercase<string>,
100
104
  };
101
105
  },
102
- consumePositionals(readerPositionals: ReaderPositionals) {
106
+ consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
103
107
  const positional = readerPositionals.consumePositional();
104
108
  if (positional === undefined) {
105
109
  throw new TypoError(
@@ -109,32 +113,27 @@ export function positionalRequired<Value>(definition: {
109
113
  ),
110
114
  );
111
115
  }
112
- return decodeValue(label, definition.type, positional);
116
+ return {
117
+ decodeValue() {
118
+ return decodeValue(label, definition.type, positional);
119
+ },
120
+ };
113
121
  },
114
122
  };
115
123
  }
116
124
 
117
125
  /**
118
- * Creates an optional positional argument one that may or may not appear on the
119
- * command line.
120
- *
121
- * The parser consumes the next available positional token. If no token is available,
122
- * `definition.default()` is called to supply the fallback value. If the default factory
123
- * throws, a {@link TypoError} is produced.
126
+ * Creates an optional positional — absent token falls back to `default()`.
127
+ * Label defaults to uppercased `type.content` in square brackets (e.g. `[STRING]`).
124
128
  *
125
- * The label displayed in the usage line defaults to the uppercased `type.content`
126
- * wrapped in square brackets (e.g. `[STRING]`). Supply `label` to override.
129
+ * @typeParam Value - Type produced by the decoder (or the default).
127
130
  *
128
- * @typeParam Value - The TypeScript type produced by the type decoder (or the default).
129
- *
130
- * @param definition - Configuration for the positional.
131
- * @param definition.description - Human-readable description for the help output.
132
- * @param definition.hint - Optional supplementary note shown in parentheses.
133
- * @param definition.label - Custom label shown in the usage line (without square brackets).
134
- * Defaults to the uppercased `type.content`.
135
- * @param definition.type - The {@link Type} used to decode the raw string token.
136
- * @param definition.default - Factory called when the positional is absent to supply the
137
- * default value. Throw from this factory to make omission an error.
131
+ * @param definition - Positional configuration.
132
+ * @param definition.description - Help text.
133
+ * @param definition.hint - Short note shown in parentheses.
134
+ * @param definition.label - Label without brackets; defaults to uppercased `type.content`.
135
+ * @param definition.type - Decoder for the raw token.
136
+ * @param definition.default - Value when absent. Throw to make it required.
138
137
  * @returns A {@link Positional}`<Value>`.
139
138
  *
140
139
  * @example
@@ -165,52 +164,42 @@ export function positionalOptional<Value>(definition: {
165
164
  label: label as Uppercase<string>,
166
165
  };
167
166
  },
168
- consumePositionals(readerPositionals: ReaderPositionals) {
167
+ consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
169
168
  const positional = readerPositionals.consumePositional();
170
- if (positional === undefined) {
171
- try {
172
- return definition.default();
173
- } catch (error) {
174
- throw new TypoError(
175
- new TypoText(
176
- new TypoString(label, typoStyleUserInput),
177
- new TypoString(`: Failed to get default value`),
178
- ),
179
- error,
180
- );
181
- }
182
- }
183
- return decodeValue(label, definition.type, positional);
169
+ return {
170
+ decodeValue() {
171
+ if (positional === undefined) {
172
+ try {
173
+ return definition.default();
174
+ } catch (error) {
175
+ throw new TypoError(
176
+ new TypoText(
177
+ new TypoString(label, typoStyleUserInput),
178
+ new TypoString(`: Failed to get default value`),
179
+ ),
180
+ error,
181
+ );
182
+ }
183
+ }
184
+ return decodeValue(label, definition.type, positional);
185
+ },
186
+ };
184
187
  },
185
188
  };
186
189
  }
187
190
 
188
191
  /**
189
- * Creates a variadic positional argument — one that collects zero or more remaining
190
- * positional tokens into an array.
191
- *
192
- * The parser greedily consumes tokens until either there are no more tokens or it
193
- * encounters the optional `endDelimiter` sentinel string, which is consumed but not
194
- * included in the result. Each token is decoded independently with `definition.type`.
195
- *
196
- * If absent entirely, the result is an empty array `[]`.
192
+ * Creates a variadic positional that collects zero or more remaining tokens into an array.
193
+ * Stops at `endDelimiter` (consumed, not included). Label: `[TYPE]...` notation.
197
194
  *
198
- * The label displayed in the usage line defaults to the uppercased `type.content`
199
- * wrapped in square brackets followed by `...` (e.g. `[STRING]...`). When an
200
- * `endDelimiter` is configured, the delimiter is also shown (e.g. `[STRING]...["--"]`).
201
- * Supply `label` to override the base label.
195
+ * @typeParam Value - Type produced by the decoder for each token.
202
196
  *
203
- * @typeParam Value - The TypeScript type produced by the type decoder for each token.
204
- *
205
- * @param definition - Configuration for the variadic positional.
206
- * @param definition.endDelimiter - Optional sentinel string that signals the end of
207
- * the variadic sequence (e.g. `"--"`). When encountered it is consumed but not
208
- * included in the result array.
209
- * @param definition.description - Human-readable description for the help output.
210
- * @param definition.hint - Optional supplementary note shown in parentheses.
211
- * @param definition.label - Custom label shown in the usage line (without brackets).
212
- * Defaults to the uppercased `type.content`.
213
- * @param definition.type - The {@link Type} used to decode each raw string token.
197
+ * @param definition - Positional configuration.
198
+ * @param definition.endDelimiter - Sentinel token that stops collection (consumed, not included).
199
+ * @param definition.description - Help text.
200
+ * @param definition.hint - Short note shown in parentheses.
201
+ * @param definition.label - Label without brackets; defaults to uppercased `type.content`.
202
+ * @param definition.type - Decoder applied to each token.
214
203
  * @returns A {@link Positional}`<Array<Value>>`.
215
204
  *
216
205
  * @example
@@ -243,8 +232,8 @@ export function positionalVariadics<Value>(definition: {
243
232
  : "")) as Uppercase<string>,
244
233
  };
245
234
  },
246
- consumePositionals(readerPositionals: ReaderPositionals) {
247
- const positionals: Array<Value> = [];
235
+ consumeAndMakeDecoder(readerPositionals: ReaderPositionals) {
236
+ const positionals = new Array<string>();
248
237
  while (true) {
249
238
  const positional = readerPositionals.consumePositional();
250
239
  if (
@@ -253,9 +242,15 @@ export function positionalVariadics<Value>(definition: {
253
242
  ) {
254
243
  break;
255
244
  }
256
- positionals.push(decodeValue(label, definition.type, positional));
245
+ positionals.push(positional);
257
246
  }
258
- return positionals;
247
+ return {
248
+ decodeValue() {
249
+ return positionals.map((positional) => {
250
+ return decodeValue(label, definition.type, positional);
251
+ });
252
+ },
253
+ };
259
254
  },
260
255
  };
261
256
  }
@@ -263,10 +258,10 @@ export function positionalVariadics<Value>(definition: {
263
258
  function decodeValue<Value>(
264
259
  label: string,
265
260
  type: Type<Value>,
266
- value: string,
261
+ input: string,
267
262
  ): Value {
268
263
  return TypoError.tryWithContext(
269
- () => type.decoder(value),
264
+ () => type.decoder(input),
270
265
  () =>
271
266
  new TypoText(
272
267
  new TypoString(label, typoStyleUserInput),
package/src/lib/Reader.ts CHANGED
@@ -7,35 +7,26 @@ import {
7
7
  } from "./Typo";
8
8
 
9
9
  /**
10
- * An opaque key that uniquely identifies a registered CLI option within a
11
- * {@link ReaderArgs} instance.
12
- *
13
- * Keys are returned by {@link ReaderArgs.registerOption} and passed back to
14
- * {@link ReaderArgs.getOptionValues} to retrieve the parsed values. The internal
15
- * representation is intentionally opaque — treat it as a handle, not a string.
10
+ * Opaque key identifying a registered option within a {@link ReaderArgs} instance.
11
+ * Returned by {@link ReaderArgs.registerOption}; passed to {@link ReaderArgs.getOptionValues}.
16
12
  */
17
13
  export type ReaderOptionKey = (string | { __brand: "ReaderOptionKey" }) & {
18
14
  __brand: "ReaderOptionKey";
19
15
  };
20
16
 
21
17
  /**
22
- * Interface for registering and querying CLI options during argument parsing.
23
- *
24
- * {@link ReaderArgs} implements both `ReaderOptions` and {@link ReaderPositionals}.
25
- * The two interfaces are exposed separately so that option and positional parsing logic
26
- * can depend only on the capability they need.
18
+ * Option registration and query interface, implemented by {@link ReaderArgs}.
19
+ * Exposed separately from {@link ReaderPositionals} so parsers depend only on what they need.
27
20
  */
28
21
  export type ReaderOptions = {
29
22
  /**
30
- * Registers a new option so the parser can recognise it when scanning argument tokens.
23
+ * Registers an option so the parser can recognise it.
31
24
  *
32
- * @param definition.longs - The long-form names (without `--`) for this option.
33
- * @param definition.shorts - The short-form names (without `-`) for this option.
34
- * @param definition.valued - When `true`, the option consumes the following token as
35
- * its value. When `false`, the option is a boolean flag.
36
- * @returns An opaque {@link ReaderOptionKey} used to retrieve parsed values later.
37
- * @throws `Error` if any of the given names has already been registered, or if a
38
- * short name overlaps (is a prefix of, or has as a prefix, another registered short).
25
+ * @param definition.longs - Long-form names (without `--`).
26
+ * @param definition.shorts - Short-form names (without `-`).
27
+ * @param definition.valued - `true` if the option takes a value; `false` for flags.
28
+ * @returns A {@link ReaderOptionKey} for later retrieval.
29
+ * @throws `Error` if a name is already registered or short names overlap.
39
30
  */
40
31
  registerOption(definition: {
41
32
  longs: Array<string>;
@@ -45,48 +36,34 @@ export type ReaderOptions = {
45
36
  /**
46
37
  * Returns all values collected for the option identified by `key`.
47
38
  *
48
- * @param key - The key returned by a prior {@link ReaderOptions.registerOption} call.
49
- * @returns An array of raw string values, one per occurrence of the option on the
50
- * command line. Empty if the option was never provided.
51
- * @throws `Error` if `key` was not previously registered on this instance.
39
+ * @param key - Key from {@link ReaderOptions.registerOption}.
40
+ * @returns Raw string values, one per occurrence; empty if never provided.
41
+ * @throws `Error` if `key` was not registered.
52
42
  */
53
43
  getOptionValues(key: ReaderOptionKey): Array<string>;
54
44
  };
55
45
 
56
46
  /**
57
- * Interface for consuming positional (non-option) argument tokens during parsing.
58
- *
59
- * {@link ReaderArgs} implements both {@link ReaderOptions} and `ReaderPositionals`.
47
+ * Positional token consumption interface, implemented by {@link ReaderArgs}.
60
48
  */
61
49
  export type ReaderPositionals = {
62
50
  /**
63
- * Consumes and returns the next positional token from the argument list, skipping
64
- * any option tokens (which are parsed as side-effects).
51
+ * Returns the next positional token, parsing intervening options as side-effects.
65
52
  *
66
- * @returns The next positional string value, or `undefined` if no more positionals
67
- * are available.
68
- * @throws {@link TypoError} if an unrecognised option token is encountered while
69
- * scanning for the next positional.
53
+ * @returns The next positional, or `undefined` when exhausted.
54
+ * @throws {@link TypoError} on an unrecognised option.
70
55
  */
71
56
  consumePositional(): string | undefined;
72
57
  };
73
58
 
74
59
  /**
75
- * The core argument parser for `cli-kiss`. Parses a flat array of raw CLI tokens into
76
- * named options and positional values.
60
+ * Core argument parser: converts raw CLI tokens into named options and positionals.
61
+ * Options must be registered before {@link ReaderArgs.consumePositional} is called.
77
62
  *
78
- * Options must be registered with {@link ReaderArgs.registerOption} **before**
79
- * {@link ReaderArgs.consumePositional} is called, because the parser needs to know
80
- * whether each token is an option name, an option value, or a bare positional.
63
+ * Supported syntax: `--name`, `--name value`, `--name=value`,
64
+ * `-n`, `-n value`, `-nvalue`, `-abc` (stacked), `--` (end-of-options).
81
65
  *
82
- * **Supported argument syntax:**
83
- * - Long options: `--name`, `--name value`, `--name=value`
84
- * - Short options: `-n`, `-n value`, `-n=value`, `-nvalue`, `-abc` (stacked flags)
85
- * - End-of-options separator: `--` — all subsequent tokens are treated as positionals.
86
- *
87
- * In most cases you do not need to use `ReaderArgs` directly; it is created internally
88
- * by {@link runAndExit}. It is exposed for advanced use cases such as building
89
- * custom runners.
66
+ * Created internally by {@link runAndExit}; exposed for advanced / custom runners.
90
67
  */
91
68
  export class ReaderArgs {
92
69
  #args: ReadonlyArray<string>;
@@ -98,8 +75,7 @@ export class ReaderArgs {
98
75
  #resultByKey: Map<ReaderOptionKey, Array<string>>;
99
76
 
100
77
  /**
101
- * @param args - The raw command-line tokens to parse. Typically `process.argv.slice(2)`.
102
- * The array is not modified; a read cursor is maintained internally.
78
+ * @param args - Raw CLI tokens (e.g. `process.argv.slice(2)`). Not mutated.
103
79
  */
104
80
  constructor(args: ReadonlyArray<string>) {
105
81
  this.#args = args;
@@ -112,22 +88,15 @@ export class ReaderArgs {
112
88
  }
113
89
 
114
90
  /**
115
- * Registers a CLI option so the parser can recognise it.
116
- *
117
- * All `longs` and `shorts` are associated with the same returned key. Calling
118
- * `getOptionValues(key)` after parsing will return values collected under any of the
119
- * registered names.
120
- *
121
- * Short names support stacking (e.g. `-abc` is parsed as `-a -b -c`) and inline
122
- * values (e.g. `-nvalue`). Short names must not be a prefix of, nor have as a prefix,
123
- * any other registered short name — the parser uses prefix matching to parse stacked
124
- * shorts, so overlapping prefixes would be ambiguous.
91
+ * Registers an option; all `longs` and `shorts` share the same key.
92
+ * Short names support stacking (e.g. `-abc`) and inline values (e.g. `-nvalue`),
93
+ * but must not be prefixes of one another.
125
94
  *
126
95
  * @param definition.longs - Long-form names (without `--`).
127
96
  * @param definition.shorts - Short-form names (without `-`).
128
- * @param definition.valued - `true` if the option consumes a value; `false` for flags.
129
- * @returns An opaque {@link ReaderOptionKey} to pass to {@link ReaderArgs.getOptionValues}.
130
- * @throws `Error` if any name is already registered or if two short names overlap.
97
+ * @param definition.valued - `true` if the option takes a value; `false` for flags.
98
+ * @returns A {@link ReaderOptionKey} for {@link ReaderArgs.getOptionValues}.
99
+ * @throws `Error` if any name is already registered or short names overlap.
131
100
  */
132
101
  registerOption(definition: {
133
102
  longs: Array<string>;
@@ -139,12 +108,17 @@ export class ReaderArgs {
139
108
  ...definition.shorts.map((short) => `-${short}`),
140
109
  ].join(", ") as ReaderOptionKey;
141
110
  for (const long of definition.longs) {
111
+ if (!this.#isValidOptionName(long)) {
112
+ throw new Error(`Invalid option name: --${long}`);
113
+ }
142
114
  if (this.#keyByLong.has(long)) {
143
115
  throw new Error(`Option already registered: --${long}`);
144
116
  }
145
- this.#keyByLong.set(long, key);
146
117
  }
147
118
  for (const short of definition.shorts) {
119
+ if (!this.#isValidOptionName(short)) {
120
+ throw new Error(`Invalid option name: -${short}`);
121
+ }
148
122
  if (this.#keyByShort.has(short)) {
149
123
  throw new Error(`Option already registered: -${short}`);
150
124
  }
@@ -163,6 +137,11 @@ export class ReaderArgs {
163
137
  );
164
138
  }
165
139
  }
140
+ }
141
+ for (const long of definition.longs) {
142
+ this.#keyByLong.set(long, key);
143
+ }
144
+ for (const short of definition.shorts) {
166
145
  this.#keyByShort.set(short, key);
167
146
  }
168
147
  this.#valuedByKey.set(key, definition.valued);
@@ -171,13 +150,11 @@ export class ReaderArgs {
171
150
  }
172
151
 
173
152
  /**
174
- * Returns all raw string values collected for the given option key.
153
+ * Returns all values collected for the option key.
175
154
  *
176
- * @param key - A key previously returned by {@link ReaderArgs.registerOption}.
177
- * @returns An array of string values, one per occurrence on the command line. For
178
- * flags this will be `["true"]` per occurrence; for valued options it will be the
179
- * literal value strings.
180
- * @throws `Error` if `key` was not registered on this instance.
155
+ * @param key - Key from {@link ReaderArgs.registerOption}.
156
+ * @returns String values, one per occurrence.
157
+ * @throws `Error` if `key` was not registered.
181
158
  */
182
159
  getOptionValues(key: ReaderOptionKey): Array<string> {
183
160
  const optionResult = this.#resultByKey.get(key);
@@ -188,18 +165,12 @@ export class ReaderArgs {
188
165
  }
189
166
 
190
167
  /**
191
- * Scans forward through the argument list and returns the next bare positional token,
192
- * consuming and parsing any intervening option tokens as side-effects.
193
- *
194
- * Option tokens encountered during the scan are recorded in the internal results map
195
- * (equivalent to recording their values against their key). Any unrecognised option token
196
- * causes a {@link TypoError} to be thrown immediately.
197
- *
198
- * After `--` is encountered, all remaining tokens are treated as positionals.
168
+ * Returns the next bare positional token.
169
+ * Parse intervening options as side-effects.
170
+ * All tokens after `--` are treated as positionals.
199
171
  *
200
- * @returns The next positional string, or `undefined` when the argument list is
201
- * exhausted.
202
- * @throws {@link TypoError} if an unrecognised option (long or short) is encountered.
172
+ * @returns The next positional, or `undefined` when exhausted.
173
+ * @throws {@link TypoError} on an unrecognised option.
203
174
  */
204
175
  consumePositional(): string | undefined {
205
176
  while (true) {
@@ -348,4 +319,8 @@ export class ReaderArgs {
348
319
  #acknowledgeOption(key: ReaderOptionKey, value: string) {
349
320
  this.getOptionValues(key).push(value);
350
321
  }
322
+
323
+ #isValidOptionName(name: string): boolean {
324
+ return name.length > 0 && !name.includes("=");
325
+ }
351
326
  }