cli-kiss 0.2.3 → 0.2.4

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/src/lib/Option.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ReaderArgs as ReaderOptions } from "./Reader";
1
+ import { ReaderOptionParsing, ReaderArgs as ReaderOptions } from "./Reader";
2
2
  import { Type, typeBoolean } from "./Type";
3
3
  import {
4
4
  TypoError,
@@ -8,20 +8,19 @@ import {
8
8
  typoStyleUserInput,
9
9
  TypoText,
10
10
  } from "./Typo";
11
+ import { UsageOption } from "./Usage";
11
12
 
12
13
  /**
13
- * A CLI option (flag or valued) with its parsing and usage-generation logic.
14
- *
15
- * Created with {@link optionFlag}, {@link optionSingleValue}, or
16
- * {@link optionRepeatable} and passed via the `options` map of {@link operation}.
14
+ * A CLI option. Created with {@link optionFlag}, {@link optionSingleValue},
15
+ * or {@link optionRepeatable}.
17
16
  *
18
17
  * @typeParam Value - Decoded value type.
19
18
  */
20
19
  export type Option<Value> = {
21
20
  /**
22
- * Returns metadata used to render the `Options:` section of help.
21
+ * Returns metadata for the `Options:` section.
23
22
  */
24
- generateUsage(): OptionUsage;
23
+ generateUsage(): UsageOption;
25
24
  /**
26
25
  * Registers the option on `readerOptions` and returns an {@link OptionDecoder}.
27
26
  */
@@ -42,46 +41,18 @@ export type OptionDecoder<Value> = {
42
41
  getAndDecodeValue(): Value;
43
42
  };
44
43
 
45
- /**
46
- * Human-readable metadata for a single option, used to render the `Options:` section
47
- * of the help output produced by {@link usageToStyledLines}.
48
- */
49
- export type OptionUsage = {
50
- /**
51
- * Long-form name without `--` (e.g. `"verbose"`).
52
- */
53
- long: Lowercase<string>;
54
- /**
55
- * Short-form name without `-` (e.g. `"v"`).
56
- */
57
- short: string | undefined;
58
- /**
59
- * Help text in usage.
60
- */
61
- description: string | undefined;
62
- /**
63
- * Short note shown in parentheses.
64
- */
65
- hint: string | undefined;
66
- /**
67
- * Value placeholder in help (e.g. `"<FILE>"`). `undefined` for flags.
68
- */
69
- label: Uppercase<string> | undefined;
70
- };
71
-
72
44
  /**
73
45
  * Creates a boolean flag option (`--verbose`, optionally `--flag=no`).
74
46
  *
75
47
  * Parsing: absent → `false`; `--flag` / `--flag=yes` → `true`; `--flag=no` → `false`;
76
48
  * specified more than once → {@link TypoError}.
77
49
  *
78
- * @param definition - Flag configuration.
79
50
  * @param definition.long - Long-form name (without `--`).
80
51
  * @param definition.short - Short-form name (without `-`).
81
52
  * @param definition.description - Help text.
82
53
  * @param definition.hint - Short note shown in parentheses.
83
54
  * @param definition.aliases - Additional names.
84
- * @param definition.default - Default when absent. Defaults to `() => false`.
55
+ * @param definition.default - Default when absent. Defaults to `false`.
85
56
  * @returns An {@link Option}`<boolean>`.
86
57
  *
87
58
  * @example
@@ -99,28 +70,44 @@ export function optionFlag(definition: {
99
70
  description?: string;
100
71
  hint?: string;
101
72
  aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
102
- default?: () => boolean;
73
+ default?: boolean;
103
74
  }): Option<boolean> {
104
75
  const label = `<${typeBoolean.content.toUpperCase()}>`;
105
76
  return {
106
77
  generateUsage() {
107
78
  return {
108
- description: definition.description,
109
- hint: definition.hint,
110
- long: definition.long,
111
79
  short: definition.short,
80
+ long: definition.long,
112
81
  label: undefined,
82
+ annotation: "[=no]",
83
+ description: definition.description,
84
+ hint: definition.hint,
113
85
  };
114
86
  },
115
87
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
116
- const key = registerOption(readerOptions, {
117
- ...definition,
118
- valued: false,
88
+ const longNegative = `no-${definition.long}` as Lowercase<string>;
89
+ const aliasesLongsNegatives = definition.aliases?.longs?.map(
90
+ (aliasLong) => `no-${aliasLong}` as Lowercase<string>,
91
+ );
92
+ const keyNegative = registerOption(readerOptions, {
93
+ long: longNegative,
94
+ short: undefined,
95
+ aliasesShorts: undefined,
96
+ aliasesLongs: aliasesLongsNegatives,
97
+ parsing: { consumeShortGroup: false, consumeNextArg: () => false },
98
+ });
99
+ const keyPositive = registerOption(readerOptions, {
100
+ long: definition.long,
101
+ short: definition.short,
102
+ aliasesLongs: definition.aliases?.longs,
103
+ aliasesShorts: definition.aliases?.shorts,
104
+ parsing: { consumeShortGroup: false, consumeNextArg: () => false },
119
105
  });
120
106
  return {
121
107
  getAndDecodeValue() {
122
- const optionValues = readerOptions.getOptionValues(key);
123
- if (optionValues.length > 1) {
108
+ const negativeResults = readerOptions.getOptionValues(keyNegative);
109
+ const positiveResults = readerOptions.getOptionValues(keyPositive);
110
+ if (positiveResults.length > 1) {
124
111
  throw new TypoError(
125
112
  new TypoText(
126
113
  new TypoString(`--${definition.long}`, typoStyleConstants),
@@ -128,27 +115,49 @@ export function optionFlag(definition: {
128
115
  ),
129
116
  );
130
117
  }
131
- const optionValue = optionValues[0];
132
- if (optionValue === undefined) {
133
- try {
134
- return definition.default ? definition.default() : false;
135
- } catch (error) {
118
+ if (negativeResults.length > 1) {
119
+ throw new TypoError(
120
+ new TypoText(
121
+ new TypoString(`--${longNegative}`, typoStyleConstants),
122
+ new TypoString(`: Must not be set multiple times`),
123
+ ),
124
+ );
125
+ }
126
+ if (negativeResults.length > 0 && positiveResults.length > 0) {
127
+ throw new TypoError(
128
+ new TypoText(
129
+ new TypoString(`--${definition.long}`, typoStyleConstants),
130
+ new TypoString(`: Must not be set in combination with: `),
131
+ new TypoString(`--${longNegative}`, typoStyleConstants),
132
+ ),
133
+ );
134
+ }
135
+ if (negativeResults.length > 0) {
136
+ const negativeResult = negativeResults[0]!;
137
+ if (negativeResult.inlined) {
136
138
  throw new TypoError(
137
139
  new TypoText(
138
- new TypoString(`--${definition.long}`, typoStyleConstants),
139
- new TypoString(`: Failed to get default value`),
140
+ new TypoString(`--${longNegative}`, typoStyleConstants),
141
+ new TypoString(`: Must not have a value`),
140
142
  ),
141
- error,
142
143
  );
143
144
  }
145
+ return false;
144
146
  }
145
- return decodeValue({
146
- long: definition.long,
147
- short: definition.short,
148
- label,
149
- type: typeBoolean,
150
- input: optionValue,
151
- });
147
+ if (positiveResults.length > 0) {
148
+ const positiveResult = positiveResults[0]!;
149
+ return decodeValue({
150
+ long: definition.long,
151
+ short: definition.short,
152
+ label,
153
+ type: typeBoolean,
154
+ input:
155
+ positiveResult.inlined === null
156
+ ? "true"
157
+ : positiveResult.inlined,
158
+ });
159
+ }
160
+ return definition.default ?? false;
152
161
  },
153
162
  };
154
163
  },
@@ -163,7 +172,6 @@ export function optionFlag(definition: {
163
172
  *
164
173
  * @typeParam Value - Type produced by the decoder.
165
174
  *
166
- * @param definition - Option configuration.
167
175
  * @param definition.long - Long-form name (without `--`).
168
176
  * @param definition.short - Short-form name (without `-`).
169
177
  * @param definition.description - Help text.
@@ -200,22 +208,30 @@ export function optionSingleValue<Value>(definition: {
200
208
  return {
201
209
  generateUsage() {
202
210
  return {
203
- description: definition.description,
204
- hint: definition.hint,
205
- long: definition.long,
206
211
  short: definition.short,
212
+ long: definition.long,
207
213
  label: label as Uppercase<string>,
214
+ annotation: undefined,
215
+ description: definition.description,
216
+ hint: definition.hint,
208
217
  };
209
218
  },
210
219
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
211
220
  const key = registerOption(readerOptions, {
212
- ...definition,
213
- valued: true,
221
+ long: definition.long,
222
+ short: definition.short,
223
+ aliasesLongs: definition.aliases?.longs,
224
+ aliasesShorts: definition.aliases?.shorts,
225
+ parsing: {
226
+ consumeShortGroup: true,
227
+ consumeNextArg: (inlined, separated) =>
228
+ inlined === null && separated.length === 0,
229
+ },
214
230
  });
215
231
  return {
216
232
  getAndDecodeValue() {
217
- const optionValues = readerOptions.getOptionValues(key);
218
- if (optionValues.length > 1) {
233
+ const optionResults = readerOptions.getOptionValues(key);
234
+ if (optionResults.length > 1) {
219
235
  throw new TypoError(
220
236
  new TypoText(
221
237
  new TypoString(`--${definition.long}`, typoStyleConstants),
@@ -223,8 +239,8 @@ export function optionSingleValue<Value>(definition: {
223
239
  ),
224
240
  );
225
241
  }
226
- const optionValue = optionValues[0];
227
- if (optionValue === undefined) {
242
+ const optionResult = optionResults[0];
243
+ if (optionResult === undefined) {
228
244
  try {
229
245
  return definition.default();
230
246
  } catch (error) {
@@ -242,7 +258,7 @@ export function optionSingleValue<Value>(definition: {
242
258
  short: definition.short,
243
259
  label,
244
260
  type: definition.type,
245
- input: optionValue,
261
+ input: optionResult.inlined ?? optionResult.separated[0]!,
246
262
  });
247
263
  },
248
264
  };
@@ -258,7 +274,6 @@ export function optionSingleValue<Value>(definition: {
258
274
  *
259
275
  * @typeParam Value - Type produced by the decoder for each occurrence.
260
276
  *
261
- * @param definition - Option configuration.
262
277
  * @param definition.long - Long-form name (without `--`).
263
278
  * @param definition.short - Short-form name (without `-`).
264
279
  * @param definition.description - Help text.
@@ -294,28 +309,36 @@ export function optionRepeatable<Value>(definition: {
294
309
  generateUsage() {
295
310
  // TODO - showcase that it can be repeated ?
296
311
  return {
297
- description: definition.description,
298
- hint: definition.hint,
299
- long: definition.long,
300
312
  short: definition.short,
313
+ long: definition.long,
301
314
  label: label as Uppercase<string>,
315
+ annotation: " [*]",
316
+ description: definition.description,
317
+ hint: definition.hint,
302
318
  };
303
319
  },
304
320
  registerAndMakeDecoder(readerOptions: ReaderOptions) {
305
321
  const key = registerOption(readerOptions, {
306
- ...definition,
307
- valued: true,
322
+ long: definition.long,
323
+ short: definition.short,
324
+ aliasesLongs: definition.aliases?.longs,
325
+ aliasesShorts: definition.aliases?.shorts,
326
+ parsing: {
327
+ consumeShortGroup: true,
328
+ consumeNextArg: (inlined, separated) =>
329
+ inlined === null && separated.length === 0,
330
+ },
308
331
  });
309
332
  return {
310
333
  getAndDecodeValue() {
311
- const optionValues = readerOptions.getOptionValues(key);
312
- return optionValues.map((optionValue) =>
334
+ const optionResults = readerOptions.getOptionValues(key);
335
+ return optionResults.map((optionResult) =>
313
336
  decodeValue({
314
337
  long: definition.long,
315
338
  short: definition.short,
316
339
  label,
317
340
  type: definition.type,
318
- input: optionValue,
341
+ input: optionResult.inlined ?? optionResult.separated[0]!,
319
342
  }),
320
343
  );
321
344
  },
@@ -336,14 +359,14 @@ function decodeValue<Value>(params: {
336
359
  () => {
337
360
  const text = new TypoText();
338
361
  if (params.short) {
339
- text.pushString(new TypoString(`-${params.short}`, typoStyleConstants));
340
- text.pushString(new TypoString(`, `));
362
+ text.push(new TypoString(`-${params.short}`, typoStyleConstants));
363
+ text.push(new TypoString(`, `));
341
364
  }
342
- text.pushString(new TypoString(`--${params.long}`, typoStyleConstants));
343
- text.pushString(new TypoString(`: `));
344
- text.pushString(new TypoString(params.label, typoStyleUserInput));
345
- text.pushString(new TypoString(`: `));
346
- text.pushString(new TypoString(params.type.content, typoStyleLogic));
365
+ text.push(new TypoString(`--${params.long}`, typoStyleConstants));
366
+ text.push(new TypoString(`: `));
367
+ text.push(new TypoString(params.label, typoStyleUserInput));
368
+ text.push(new TypoString(`: `));
369
+ text.push(new TypoString(params.type.content, typoStyleLogic));
347
370
  return text;
348
371
  },
349
372
  );
@@ -353,19 +376,20 @@ function registerOption(
353
376
  readerOptions: ReaderOptions,
354
377
  definition: {
355
378
  long: Lowercase<string>;
356
- short?: string;
357
- aliases?: { longs?: Array<Lowercase<string>>; shorts?: Array<string> };
358
- valued: boolean;
379
+ short: undefined | string;
380
+ aliasesLongs: undefined | Array<Lowercase<string>>;
381
+ aliasesShorts: undefined | Array<string>;
382
+ parsing: ReaderOptionParsing;
359
383
  },
360
384
  ) {
361
- const { long, short, aliases, valued } = definition;
385
+ const { long, short, aliasesLongs, aliasesShorts, parsing } = definition;
362
386
  const longs = long ? [long] : [];
363
- if (aliases?.longs) {
364
- longs.push(...aliases?.longs);
387
+ if (aliasesLongs) {
388
+ longs.push(...aliasesLongs);
365
389
  }
366
390
  const shorts = short ? [short] : [];
367
- if (aliases?.shorts) {
368
- shorts.push(...aliases?.shorts);
391
+ if (aliasesShorts) {
392
+ shorts.push(...aliasesShorts);
369
393
  }
370
- return readerOptions.registerOption({ longs, shorts, valued });
394
+ return readerOptions.registerOption({ longs, shorts, parsing });
371
395
  }
@@ -7,21 +7,19 @@ import {
7
7
  typoStyleUserInput,
8
8
  TypoText,
9
9
  } from "./Typo";
10
+ import { UsagePositional } from "./Usage";
10
11
 
11
12
  /**
12
- * A bare (non-option) positional argument with its parsing and usage-generation logic.
13
- *
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.
13
+ * A positional argument. Created with {@link positionalRequired}, {@link positionalOptional},
14
+ * or {@link positionalVariadics}.
17
15
  *
18
16
  * @typeParam Value - Decoded value type.
19
17
  */
20
18
  export type Positional<Value> = {
21
19
  /**
22
- * Returns metadata used to render the `Positionals:` section of help.
20
+ * Returns metadata for the `Positionals:` section.
23
21
  */
24
- generateUsage(): PositionalUsage;
22
+ generateUsage(): UsagePositional;
25
23
  /**
26
24
  * Consumes the next positional token from `readerPositionals`.
27
25
  * Returns a decoder that produces the final value.
@@ -45,33 +43,12 @@ export type PositionalDecoder<Value> = {
45
43
  decodeValue(): Value;
46
44
  };
47
45
 
48
- /**
49
- * Human-readable metadata for a single positional argument, used to render the
50
- * `Positionals:` section of the help output produced by {@link usageToStyledLines}.
51
- */
52
- export type PositionalUsage = {
53
- /**
54
- * Help text.
55
- */
56
- description: string | undefined;
57
- /**
58
- * Short note shown in parentheses.
59
- */
60
- hint: string | undefined;
61
- /**
62
- * Placeholder label shown in the usage line and the `Positionals:` section.
63
- * Required: `<NAME>`, optional: `[NAME]`, variadic: `[NAME]...`.
64
- */
65
- label: Uppercase<string>;
66
- };
67
-
68
46
  /**
69
47
  * Creates a required positional — missing token throws {@link TypoError}.
70
48
  * Label defaults to uppercased `type.content` in angle brackets (e.g. `<STRING>`).
71
49
  *
72
50
  * @typeParam Value - Type produced by the decoder.
73
51
  *
74
- * @param definition - Positional configuration.
75
52
  * @param definition.description - Help text.
76
53
  * @param definition.hint - Short note shown in parentheses.
77
54
  * @param definition.label - Label without brackets; defaults to uppercased `type.content`.
@@ -128,7 +105,6 @@ export function positionalRequired<Value>(definition: {
128
105
  *
129
106
  * @typeParam Value - Type produced by the decoder (or the default).
130
107
  *
131
- * @param definition - Positional configuration.
132
108
  * @param definition.description - Help text.
133
109
  * @param definition.hint - Short note shown in parentheses.
134
110
  * @param definition.label - Label without brackets; defaults to uppercased `type.content`.
@@ -194,7 +170,6 @@ export function positionalOptional<Value>(definition: {
194
170
  *
195
171
  * @typeParam Value - Type produced by the decoder for each token.
196
172
  *
197
- * @param definition - Positional configuration.
198
173
  * @param definition.endDelimiter - Sentinel token that stops collection (consumed, not included).
199
174
  * @param definition.description - Help text.
200
175
  * @param definition.hint - Short note shown in parentheses.
@@ -228,7 +203,7 @@ export function positionalVariadics<Value>(definition: {
228
203
  hint: definition.hint,
229
204
  label: (`${label}...` +
230
205
  (definition.endDelimiter
231
- ? `["${definition.endDelimiter}"]`
206
+ ? ` ["${definition.endDelimiter}"]`
232
207
  : "")) as Uppercase<string>,
233
208
  };
234
209
  },
@@ -246,9 +221,9 @@ export function positionalVariadics<Value>(definition: {
246
221
  }
247
222
  return {
248
223
  decodeValue() {
249
- return positionals.map((positional) => {
250
- return decodeValue(label, definition.type, positional);
251
- });
224
+ return positionals.map((positional) =>
225
+ decodeValue(label, definition.type, positional),
226
+ );
252
227
  },
253
228
  };
254
229
  },