@loomcli/core 0.7.0 → 0.8.0

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/dist/plugin.d.ts CHANGED
@@ -9,18 +9,21 @@ import type { ProcessSignal } from './signals.js';
9
9
  import type { Palette } from './style-state.js';
10
10
  import type { ContextualStyle, ThemeConstraint, ThemeMapping } from './style.js';
11
11
  import type { Translation, TranslationContributor } from './translators.js';
12
- import type { CommandAttachHook, Host, OptionValue, Out, PluginOptionConfig } from './types.js';
12
+ import type { CommandAttachHook, GlobalOptionConfig, Host, OptionValue, Out } from './types.js';
13
13
  import type { OptionInput } from './validation.js';
14
14
  import type { ViewContribution, ViewSubject } from './view.js';
15
15
  /**
16
- * The declaration record a plugin contributes its options under: the parsing part of an option
17
- * config, keyed by option name. A plugin option carries no schema and no presence rule, so the
18
- * config type publishes neither, and `plugin()` repeats the rule for a JavaScript author.
16
+ * The declaration record a plugin contributes its global options under, keyed by option name. Each
17
+ * entry takes the configuration `globalOption()` takes, so the config type publishes no presence
18
+ * rule, and `plugin()` repeats the rule for a JavaScript author.
19
+ */
20
+ type PluginOptions = Readonly<Record<string, GlobalOptionConfig>>;
21
+ /**
22
+ * The values one plugin's options take, read through the same `OptionValue` an action's options
23
+ * are, so a validated option reads as its validator's output.
19
24
  */
20
- type PluginOptions = Readonly<Record<string, PluginOptionConfig>>;
21
- /** The values one plugin's own options take, read through the same rules an action's options are. */
22
25
  type PluginOptionValues<Options extends PluginOptions> = {
23
- readonly [Name in keyof Options]: OptionValue<Options[Name]>;
26
+ readonly [Name in keyof Options & string]: OptionValue<Options[Name]>;
24
27
  };
25
28
  /**
26
29
  * The spelling that supplied each of one plugin's own options given as a token, such as `-h`,
@@ -48,6 +51,16 @@ declare class PluginDeclaration<Options extends PluginOptions, Theme extends The
48
51
  type Plugin<Options extends PluginOptions = PluginOptions, Theme extends ThemeMapping = {}> = Pick<PluginDeclaration<Options, Theme>, typeof pluginOptions | typeof pluginTheme>;
49
52
  /** The declared options of a plugin, or of the factory that returns one. */
50
53
  type OptionsOf<Contributor> = Contributor extends Plugin<infer Options> ? Options : Contributor extends (...args: never[]) => Plugin<infer Options> ? Options : PluginOptions;
54
+ /**
55
+ * The option values an installed plugin tuple contributes to every action's options, each plugin's
56
+ * read through `PluginOptionValues`. The Application computes it once from the constructor's
57
+ * `plugins`. A plugin typed as the wide `Plugin` states no option names, and a list widened to
58
+ * `Plugin[]` states no plugins, so neither names an option.
59
+ */
60
+ type InstalledOptionValues<Plugins extends readonly Plugin[]> = Plugins extends readonly [
61
+ infer First extends Plugin,
62
+ ...infer Rest extends readonly Plugin[]
63
+ ] ? (string extends keyof OptionsOf<First> ? {} : PluginOptionValues<OptionsOf<First>>) & InstalledOptionValues<Rest> : {};
51
64
  /** A middleware reads its own plugin's options and either takes over or continues the chain. */
52
65
  type Middleware<Contributor extends Plugin | ((...args: never[]) => Plugin)> = (context: MiddlewareContext<OptionsOf<Contributor>>) => Promise<void> | void;
53
66
  /**
@@ -66,11 +79,12 @@ interface SourceContext<Options extends PluginOptions = PluginOptions> {
66
79
  readonly style: ContextualStyle;
67
80
  }
68
81
  /**
69
- * One answer: a value of the option's raw type, a string, a Boolean, or a list of strings for a
70
- * multiple option, and the one-line label core prints in a diagnostic about the value.
82
+ * One answer: a value of the option's raw type, a string, a Boolean, a whole number of 0 or more
83
+ * for a counted option, or a list of strings for a multiple option, and the one-line label core
84
+ * prints in a diagnostic about the value.
71
85
  */
72
86
  interface SourceAnswer {
73
- readonly value: string | boolean | readonly string[];
87
+ readonly value: string | boolean | number | readonly string[];
74
88
  readonly label: string;
75
89
  }
76
90
  /**
@@ -182,17 +196,8 @@ interface BuiltPlugin {
182
196
  }
183
197
  /** The signals the one slot owner claimed, or none when no installed plugin claims the slot. */
184
198
  declare function ownedSignals(plugins: readonly BuiltPlugin[]): readonly ProcessSignal[];
185
- /** The value shape a plugin option takes, which is what `OptionValue` gives its declaration. */
186
- type PluginValues = Record<string, string | string[] | boolean | undefined>;
187
- /**
188
- * One plugin's own option values for one run: what argv or an input source supplied, or the
189
- * declared default, filled without validation. A collected value and an array default are copied,
190
- * so a plugin that writes to what it received changes neither the declaration nor the next run.
191
- * Entries become own keys even for a name such as `__proto__`, which assignment would not.
192
- */
193
- declare function pluginValues(inputs: readonly OptionInput[], values: OptionValues): PluginValues;
194
199
  /** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
195
200
  declare function pluginSpellings(inputs: readonly OptionInput[], values: OptionValues): Readonly<Record<string, string>>;
196
201
  type ThemeOf<Contributor> = [Contributor] extends [never] ? {} : Contributor extends Plugin<PluginOptions, infer Theme> ? Theme : {};
197
- export type { ThemeOf, BuiltPlugin, BuiltSource, PluginValues, SourceAnswer, SourceContext, SourceResolver, Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionSpellings, PluginOptionValues, };
198
- export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews, pluginValues, };
202
+ export type { ThemeOf, BuiltPlugin, BuiltSource, InstalledOptionValues, SourceAnswer, SourceContext, SourceResolver, Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionSpellings, PluginOptionValues, };
203
+ export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews, };
package/dist/plugin.js CHANGED
@@ -5,13 +5,13 @@ import { attach, commandCode, commandNode } from './command.js';
5
5
  import { elided, quoteString, spelled } from './diagnostic-text.js';
6
6
  import { DeclarationError, InternalError, quoted, reasonOf } from './errors.js';
7
7
  import { appliesTo, buildExtensions, isDescriptor, registerDescriptor } from './extension.js';
8
- import { checkDeprecated, checkDescription, checkHidden, factFault, partFinding, pluginOptionSite, slotSite, } from './facts.js';
9
- import { boundOptions, pluginSites } from './globals.js';
8
+ import { checkDeprecated, checkDescription, checkHidden, partFinding, pluginOptionSite, slotSite, } from './facts.js';
9
+ import { boundOptions, checkNoPresenceRule, pluginSites } from './globals.js';
10
10
  import { checkIdentity } from './identity.js';
11
11
  import { coreViews } from './lanes.js';
12
- import { booleanValue, compileOptions } from './options.js';
12
+ import { compileOptions } from './options.js';
13
13
  import { copyOwnKeys, decidePlain, declaring, isPlainObject, shallowList, shallowRecord, } from './plain.js';
14
- import { foreignValue, middlewareActivation, notAFunction, notAList, notAnObject, pluginInstalledTwice, pluginOptionRule, signalClaimedTwice, slotTaken, sourceBinding, sourceBoundOwnOption, unknownSignal, } from './plugin-rules.js';
14
+ import { foreignValue, middlewareActivation, notAFunction, notAList, notAnObject, pluginInstalledTwice, signalClaimedTwice, slotTaken, sourceBinding, sourceBoundOwnOption, unknownSignal, } from './plugin-rules.js';
15
15
  import { pluginLoaderFailed } from './rules.js';
16
16
  import { isProcessSignal } from './signals.js';
17
17
  import { buildTheme } from './theme.js';
@@ -208,10 +208,8 @@ function installPlugins(application, plugins) {
208
208
  }
209
209
  return { descriptors, plugins: installed };
210
210
  }
211
- /** The keys a plugin option may not declare, in the order its diagnostic names them. */
212
- const forbidden = ['validate', 'validateOmitted', 'required'];
213
211
  /**
214
- * The rules a plugin option answers before every rule an ordinary declaration carries, read from
212
+ * The rules a plugin's option answers before every rule an ordinary declaration carries, read from
215
213
  * the one copy of its config that `captureConfig` takes, which it answers with for every later read.
216
214
  * `siteOf` places the option with the value its entry prints as: the config as declared for a value
217
215
  * that is not one, elided for a config whose read threw, and the copy for every later rule.
@@ -236,14 +234,8 @@ function checkPluginOption(siteOf, declared) {
236
234
  },
237
235
  });
238
236
  const site = siteOf(config);
239
- const rejected = forbidden.find((key) => key in config);
240
- if (rejected !== undefined) {
241
- throw factFault(pluginOptionRule, site, {
242
- correction: 'Remove it; a plugin option carries no validator or presence rule, and the middleware interprets the value.',
243
- fact: rejected,
244
- sentence: `${sentence} declares ${rejected}.`,
245
- });
246
- }
237
+ // A plugin's options are global options, so they answer the global option's presence rule.
238
+ checkNoPresenceRule(site, config);
247
239
  checkDescription(site, config.description);
248
240
  checkHidden(site, config.hidden);
249
241
  checkDeprecated(site, config.deprecated);
@@ -293,7 +285,8 @@ function readOptions(identity, declared, build) {
293
285
  }));
294
286
  inputs.push(input);
295
287
  }
296
- // Two options of one plugin meet in the one table the pre-scan reads, so they share its rules.
288
+ // Two options of one plugin meet in the globals table every Command's table holds.
289
+ // They therefore share its rules.
297
290
  const siteOf = pluginSites(identity, inputs);
298
291
  compileOptions(inputs, { siteOf, subject: `plugin ${quoted(identity)}` });
299
292
  claimVariables(boundOptions(inputs, (input) => ({
@@ -661,34 +654,6 @@ function readPlugin(named, definition) {
661
654
  function ownedSignals(plugins) {
662
655
  return plugins.find((entry) => entry.signals.length > 0)?.signals ?? [];
663
656
  }
664
- /** A collected value, or the declared array default, as this run's own copy. */
665
- function collectedValue(collected, declared) {
666
- if (collected) {
667
- return [...collected];
668
- }
669
- return Array.isArray(declared) ? [...declared] : [];
670
- }
671
- /** One plugin option's value for one run: what a tier supplied, or the declared default. */
672
- function pluginValue({ config, name }, values) {
673
- const declared = config.default;
674
- if (config.type === 'boolean') {
675
- return booleanValue(values, name, config);
676
- }
677
- if (config.multiple === true) {
678
- return collectedValue(values.lists.get(name), declared);
679
- }
680
- // Build already proved that a string option without a validator declares a string default.
681
- return values.strings.get(name) ?? (typeof declared === 'string' ? declared : undefined);
682
- }
683
- /**
684
- * One plugin's own option values for one run: what argv or an input source supplied, or the
685
- * declared default, filled without validation. A collected value and an array default are copied,
686
- * so a plugin that writes to what it received changes neither the declaration nor the next run.
687
- * Entries become own keys even for a name such as `__proto__`, which assignment would not.
688
- */
689
- function pluginValues(inputs, values) {
690
- return Object.fromEntries(inputs.map((input) => [input.name, pluginValue(input, values)]));
691
- }
692
657
  /** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
693
658
  function pluginSpellings(inputs, values) {
694
659
  // Entries become own keys even for a name such as `__proto__`, which assignment would not.
@@ -698,4 +663,4 @@ function pluginSpellings(inputs, values) {
698
663
  });
699
664
  return Object.freeze(Object.fromEntries(supplied));
700
665
  }
701
- export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews, pluginValues, };
666
+ export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews, };
package/dist/sources.d.ts CHANGED
@@ -5,7 +5,7 @@ import type { OptionValues } from './options.js';
5
5
  import type { BuiltPlugin } from './plugin.js';
6
6
  import type { ContextualStyle } from './style.js';
7
7
  import type { Host, Out } from './types.js';
8
- import type { OptionInput } from './validation.js';
8
+ import type { OptionInput, Provenance, Validation } from './validation.js';
9
9
  /**
10
10
  * One scope the stage fills: its options in declaration order, and the values argv supplied them.
11
11
  * The stage writes each fill into `values`, so the caller hands it the run's own copy.
@@ -35,23 +35,32 @@ interface SourceStage {
35
35
  signal: AbortSignal;
36
36
  /** The contextual style an action receives, so a source escapes raw data before it warns. */
37
37
  style: ContextualStyle;
38
+ /**
39
+ * Validates the listed options alone, from the values the stage has filled so far, with what the
40
+ * environment tier found about them, under the context every validator of the run reads. A
41
+ * configuration source's own options pass through it before the source loads.
42
+ */
43
+ validate: (inputs: readonly OptionInput[], provenance: Provenance) => Promise<Validation>;
38
44
  }
39
45
  /**
40
46
  * What the stage found beside the values it filled. `labels` names where each filled option's value
41
- * came from, by option name, and `rejected` names the variable of each Boolean option whose value
42
- * is outside the grammar. Both are internal to core's failure messages. `fault` is a configuration
47
+ * came from, by option name, and `rejected` names the variable of each Boolean or counted option
48
+ * whose value is outside its grammar. Both are internal to core's failure messages. `fault` is a configuration
43
49
  * source's own fault, or the failure its resolver threw, which stops the stage and takes the place
44
- * of every validation problem.
50
+ * of every validation problem. `validated` is the pass over the source's own options ahead of its
51
+ * call, whose values and problems the run's one validation pass reads.
45
52
  */
46
- interface SourceOutcome {
53
+ interface SourceOutcome extends Provenance {
47
54
  fault: LoomError | undefined;
48
- labels: ReadonlyMap<string, string>;
49
- rejected: ReadonlyMap<string, string>;
55
+ validated: Validation | undefined;
50
56
  }
51
57
  /**
52
58
  * The input-source stage: the environment, then the configuration source, for every option in
53
- * scope that argv left unfilled. When no option is left to ask, the source never loads. A source
54
- * fault stops the stage and fills nothing more.
59
+ * scope that argv left unfilled. When no option is left to ask, the source never loads. The
60
+ * source's own options pass their validators before it loads, and when one is rejected the source
61
+ * is never called and the problem reports with every other validation problem, while each option
62
+ * the source would have been asked to fill reports no missing value. A source fault stops the
63
+ * stage and fills nothing more.
55
64
  */
56
65
  declare function fillInputs(stage: SourceStage): Promise<SourceOutcome>;
57
66
  export type { SourceOutcome, SourceStage, StageScope };
package/dist/sources.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { asSentence, foreignFailure, InternalError, LoomError, quoted, reasonOf, } from './errors.js';
2
2
  import { isProseLine } from './facts.js';
3
+ import { frozenValues } from './globals.js';
3
4
  import { isSupplied } from './options.js';
4
5
  import { isPlainObject } from './plain.js';
5
- import { loadDefault, pluginSentence, pluginValues } from './plugin.js';
6
+ import { loadDefault, pluginSentence } from './plugin.js';
6
7
  import { foreignThrow, foreignThrowCorrection, sourceAnswers, sourceAnswersCorrection, } from './rules.js';
7
8
  /** The Boolean grammar a bound variable is read through: the whole value, case-insensitive. */
8
9
  const truths = new Map([
@@ -11,10 +12,13 @@ const truths = new Map([
11
12
  ['false', false],
12
13
  ['true', true],
13
14
  ]);
15
+ /** The count grammar a bound variable is read through: the whole value, one or more ASCII digits. */
16
+ const countText = /^[0-9]+$/u;
14
17
  /**
15
- * Reads one option's bound variable. An empty variable is unset and falls through, a string option
16
- * receives the raw string, and a Boolean option reads the whole value through the grammar, so the
17
- * value states the option's value and not a spelling.
18
+ * Reads one option's bound variable. An empty variable is unset and falls through, and a string
19
+ * option receives the raw string, never its implied value. A Boolean option reads the whole value
20
+ * through the grammar, so the value states the option's value and not a spelling, and a counted
21
+ * option reads the whole value as decimal digits.
18
22
  */
19
23
  function readVariable(input, env) {
20
24
  const variable = input.config.env;
@@ -25,6 +29,9 @@ function readVariable(input, env) {
25
29
  if (input.config.type === 'string') {
26
30
  return { kind: 'fill', value: raw };
27
31
  }
32
+ if (input.config.type === 'count') {
33
+ return countText.test(raw) ? { kind: 'fill', value: Number(raw) } : { kind: 'rejected' };
34
+ }
28
35
  const value = truths.get(raw.toLowerCase());
29
36
  return value === undefined ? { kind: 'rejected' } : { kind: 'fill', value };
30
37
  }
@@ -33,6 +40,9 @@ function fill(values, name, value) {
33
40
  if (typeof value === 'boolean') {
34
41
  values.booleans.set(name, value);
35
42
  }
43
+ else if (typeof value === 'number') {
44
+ values.counts.set(name, value);
45
+ }
36
46
  else if (typeof value === 'string') {
37
47
  values.strings.set(name, value);
38
48
  }
@@ -43,12 +53,13 @@ function fill(values, name, value) {
43
53
  /** How a fault names the value an answer owed, by the shape the option takes. */
44
54
  const owed = {
45
55
  boolean: 'a Boolean',
56
+ count: 'a whole number of 0 or more',
46
57
  list: 'an array of strings',
47
58
  string: 'a string',
48
59
  };
49
60
  function rawKind(input) {
50
- if (input.config.type === 'boolean') {
51
- return 'boolean';
61
+ if (input.config.type !== 'string') {
62
+ return input.config.type;
52
63
  }
53
64
  return input.config.multiple === true ? 'list' : 'string';
54
65
  }
@@ -78,6 +89,9 @@ function rawValue(kind, value) {
78
89
  if (kind === 'boolean') {
79
90
  return typeof value === 'boolean' ? value : undefined;
80
91
  }
92
+ if (kind === 'count') {
93
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
94
+ }
81
95
  return typeof value === 'string' ? value : undefined;
82
96
  }
83
97
  /**
@@ -150,7 +164,7 @@ function sourceFailure(sentence, error) {
150
164
  * Core builds the context before the call, so a fault of its own is never the plugin's.
151
165
  */
152
166
  async function askSource(stage, call) {
153
- const { owner, requested } = call;
167
+ const { options, owner, requested } = call;
154
168
  const resolver = await loadDefault(owner.identity, owner.source.load, {
155
169
  guard: isResolverExport,
156
170
  noun: 'source',
@@ -162,7 +176,8 @@ async function askSource(stage, call) {
162
176
  const context = {
163
177
  graph: stage.inspected(),
164
178
  host: stage.host,
165
- options: pluginValues(owner.inputs, stage.globals.values),
179
+ // Validation produced the record the plugin's option types describe.
180
+ options,
166
181
  out: stage.out,
167
182
  requests: requested.map(({ input, scope }) => stage.request(input, scope.global)),
168
183
  style: stage.style,
@@ -193,8 +208,8 @@ async function askSource(stage, call) {
193
208
  }
194
209
  /**
195
210
  * The environment tier: each option in scope that argv left unfilled reads its bound variable. A
196
- * filled option is labeled with its variable, and a Boolean one outside the grammar is recorded
197
- * as rejected, which fills nothing.
211
+ * filled option is labeled with its variable, and a Boolean or counted one outside its grammar is
212
+ * recorded as rejected, which fills nothing.
198
213
  */
199
214
  function fillFromEnvironment(scopes, env) {
200
215
  const labels = new Map();
@@ -228,8 +243,11 @@ function requestedOf(scopes, excluded, bound) {
228
243
  }
229
244
  /**
230
245
  * The input-source stage: the environment, then the configuration source, for every option in
231
- * scope that argv left unfilled. When no option is left to ask, the source never loads. A source
232
- * fault stops the stage and fills nothing more.
246
+ * scope that argv left unfilled. When no option is left to ask, the source never loads. The
247
+ * source's own options pass their validators before it loads, and when one is rejected the source
248
+ * is never called and the problem reports with every other validation problem, while each option
249
+ * the source would have been asked to fill reports no missing value. A source fault stops the
250
+ * stage and fills nothing more.
233
251
  */
234
252
  async function fillInputs(stage) {
235
253
  const scopes = stage.locals ? [stage.globals, stage.locals] : [stage.globals];
@@ -239,20 +257,32 @@ async function fillInputs(stage) {
239
257
  ? requestedOf(scopes, rejected, { binding: owner.source.binding, extensions: stage.extensions })
240
258
  : [];
241
259
  if (!owner || requested.length === 0) {
242
- return { fault: undefined, labels, rejected };
260
+ return { fault: undefined, labels, rejected, validated: undefined };
243
261
  }
262
+ let validated = undefined;
244
263
  try {
245
- for (const { label, target, value } of await askSource(stage, { owner, requested })) {
264
+ validated = await stage.validate(owner.inputs, { labels, rejected });
265
+ // A rejected own option skips the source, and the options it would have filled report no omission.
266
+ if (validated.failure) {
267
+ const unanswered = new Set(requested.map(({ input }) => input));
268
+ return { fault: undefined, labels, rejected, unanswered, validated };
269
+ }
270
+ // A run cancelled while the source's own options were validated loads nothing.
271
+ if (stage.signal.aborted) {
272
+ return { fault: undefined, labels, rejected, validated };
273
+ }
274
+ const options = frozenValues(owner.inputs, validated.values);
275
+ for (const { label, target, value } of await askSource(stage, { options, owner, requested })) {
246
276
  fill(target.scope.values, target.input.name, value);
247
277
  labels.set(target.input.name, label);
248
278
  }
249
- return { fault: undefined, labels, rejected };
279
+ return { fault: undefined, labels, rejected, validated };
250
280
  }
251
281
  catch (error) {
252
282
  // The resolver's own failure keeps its class, and so its code.
253
283
  // Every other fault above is raised as an internal error, and anything else is wrapped the same way.
254
284
  const fault = error instanceof LoomError ? error : foreignFailure(error);
255
- return { fault, labels, rejected };
285
+ return { fault, labels, rejected, validated };
256
286
  }
257
287
  }
258
288
  export { fillInputs };
package/dist/types.d.ts CHANGED
@@ -8,12 +8,18 @@ import type { RenderingPolicy } from './rendering.js';
8
8
  import type { ContextualStyle } from './style.js';
9
9
  type LowercaseLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
10
10
  type ShortAlias = LowercaseLetter | Uppercase<LowercaseLetter>;
11
+ /**
12
+ * The spellings an option declares beyond its long form. `aliases` adds a long spelling for each
13
+ * name, so `shortOnly`, which removes every long spelling, never declares one.
14
+ */
11
15
  type OptionSpelling = {
12
16
  short?: ShortAlias;
13
17
  shortOnly?: false;
18
+ aliases?: readonly string[];
14
19
  } | {
15
20
  short: ShortAlias;
16
21
  shortOnly: true;
22
+ aliases?: never;
17
23
  };
18
24
  type Presence = {
19
25
  required: true;
@@ -48,6 +54,24 @@ type Omission = {
48
54
  } | {
49
55
  validateOmitted?: false;
50
56
  };
57
+ /**
58
+ * The presence rules a global option never declares, because its validation runs on every Command.
59
+ * `GlobalOptionConfig` and `GlobalOmissionConstraint` both read the rule from here.
60
+ */
61
+ type PresenceRuleKey = 'required' | 'validateOmitted';
62
+ /** The fault each presence rule names when a global option declares it. */
63
+ interface PresenceRuleFaults {
64
+ required: {
65
+ 'A global option declares no required; the Commands that read it check for it': never;
66
+ };
67
+ validateOmitted: {
68
+ 'A global option declares no validateOmitted; its omission is plain absence': never;
69
+ };
70
+ }
71
+ /** The first presence rule a config declares, in the order the faults name them. */
72
+ type DeclaredPresenceRule<Config> = {
73
+ [Key in PresenceRuleKey]: [Extract<Config, Record<Key, unknown>>] extends [never] ? never : Key;
74
+ }[PresenceRuleKey];
51
75
  /**
52
76
  * The one-line summary every projection reads. It is a core fact: optional, and a string that holds
53
77
  * a character other than whitespace and no line terminator.
@@ -156,8 +180,11 @@ export interface InputIdentity {
156
180
  export interface SuppliedInputs {
157
181
  /** Raw positional values of the routed Command: one string, or the tokens of a variadic. */
158
182
  args: Readonly<Record<string, string | readonly string[] | undefined>>;
159
- /** Raw option values, globals and locals: a string, every occurrence, or a Boolean presence. */
160
- options: Readonly<Record<string, string | readonly string[] | boolean | undefined>>;
183
+ /**
184
+ * Raw option values, globals and locals: a string, every occurrence, a Boolean presence, or the
185
+ * number of times a counted option was supplied.
186
+ */
187
+ options: Readonly<Record<string, string | readonly string[] | boolean | number | undefined>>;
161
188
  }
162
189
  /**
163
190
  * What core knows when it calls one schema. A default validates before any token is read, so its
@@ -317,7 +344,7 @@ export interface ResultBinding {
317
344
  /**
318
345
  * The routed Command's invocation after parsing and validation, which every middleware reads. The
319
346
  * values are what the action receives, the output of each declaration's schema, for that Command's
320
- * own arguments and local options; global and plugin option values are not here. The records are
347
+ * own arguments and local options; global option values are not here. The records are
321
348
  * untyped and frozen, because a middleware runs ahead of every action and the graph carries no
322
349
  * type for a value.
323
350
  */
@@ -341,6 +368,8 @@ export type StringOption = OptionSpelling & Presence & Multiplicity & Omission &
341
368
  type: 'string';
342
369
  polarity?: never;
343
370
  validate?: StandardSchemaV1;
371
+ /** The value a bare spelling supplies, so an explicit value is attached to the spelling. */
372
+ implied?: string;
344
373
  };
345
374
  /** A variadic argument collects the remaining tokens, so it follows the multiple option rules. */
346
375
  export type VariadicArgument = Presence & Described & ArgumentExtensions & {
@@ -357,6 +386,19 @@ export type ValidatedValue<Config, Raw> = Config extends unknown ? 'validate' ex
357
386
  export type DefaultConstraint<Config> = Config extends unknown ? Config & {
358
387
  default?: 'validate' extends keyof Config ? PerValue<Config, SchemaInput<Config['validate']>> : RawValue<Config>;
359
388
  } : never;
389
+ /**
390
+ * An implied value is the string an operator would otherwise attach, so it must be a string the
391
+ * validator's input type accepts, as a default must be. A multiple option's validator reads one
392
+ * value, so the implied value meets that one value's input type. A validator that declares no types
393
+ * infers `never`, so it states nothing to check against. The key names the fault, the way the
394
+ * other declaration constraints do, because an intersection with the literal would reduce the whole
395
+ * config to `never` and report every key.
396
+ */
397
+ export type ImpliedConstraint<Config> = Config extends {
398
+ implied: infer Implied;
399
+ } ? 'validate' extends keyof Config ? [SchemaInput<Config['validate']>] extends [never] ? unknown : Implied extends SchemaInput<Config['validate']> ? unknown : {
400
+ 'An implied value must be in the validator input type': Config['validate'];
401
+ } : unknown : unknown;
360
402
  /**
361
403
  * A multiple option or a variadic argument passes each value to its validator alone, so the
362
404
  * declared validator must accept one `string`. The key names the fault, the way the other
@@ -404,19 +446,9 @@ export type ValidateOmittedConstraint<Config> = Config extends {
404
446
  /**
405
447
  * A global option declares no presence rule, so its omission is always plain absence. A union
406
448
  * config fails when any member declares the key, and a wide `OptionConfig` passes, because its
407
- * members only allow the key.
449
+ * members only allow the key. `required` is named first when a config declares both.
408
450
  */
409
- export type GlobalOmissionConstraint<Config> = [Extract<Config, {
410
- required: unknown;
411
- }>] extends [
412
- never
413
- ] ? [Extract<Config, {
414
- validateOmitted: unknown;
415
- }>] extends [never] ? unknown : {
416
- 'A global option declares no validateOmitted; its omission is plain absence': never;
417
- } : {
418
- 'A global option declares no required; the Commands that read it check for it': never;
419
- };
451
+ export type GlobalOmissionConstraint<Config> = [DeclaredPresenceRule<Config>] extends [never] ? unknown : PresenceRuleFaults['required' extends DeclaredPresenceRule<Config> ? 'required' : 'validateOmitted'];
420
452
  export type ArgumentValue<Config extends ArgumentConfig> = Config extends {
421
453
  variadic: true;
422
454
  } ? ValidatedValue<Config, string[]> : ValidatedValue<Config, string> | (Config extends {
@@ -433,6 +465,7 @@ export type BooleanOption = (OptionSpelling & Described & Listed & EnvBinding &
433
465
  multiple?: never;
434
466
  required?: never;
435
467
  validateOmitted?: never;
468
+ implied?: never;
436
469
  polarity?: 'positive' | 'negative';
437
470
  }) | (Described & Listed & EnvBinding & OptionExtensions & {
438
471
  type: 'boolean';
@@ -441,27 +474,34 @@ export type BooleanOption = (OptionSpelling & Described & Listed & EnvBinding &
441
474
  multiple?: never;
442
475
  required?: never;
443
476
  validateOmitted?: never;
477
+ implied?: never;
444
478
  polarity: 'both';
445
479
  short?: ShortAlias;
446
480
  shortOnly?: false;
481
+ aliases?: readonly string[];
447
482
  });
448
- export type OptionConfig = StringOption | BooleanOption;
449
483
  /**
450
- * The parsing part of a string option config, which is all a plugin option declares. A plugin
451
- * option carries no schema and no presence rule, because the pre-scan consumes it ahead of routing,
452
- * where the validation context every schema is promised cannot exist. Its middleware interprets
453
- * the value.
454
- * A Boolean plugin option is an ordinary `BooleanOption`, which already declares none of them.
484
+ * A counted option takes no value and reads how many times it was supplied, across every spelling,
485
+ * so it declares no validator, default, presence rule, collection, polarity, or implied value.
455
486
  */
456
- export type PluginStringOption = OptionSpelling & Multiplicity & Described & Listed & OptionExtensions & {
457
- type: 'string';
458
- default?: string | string[];
459
- polarity?: never;
460
- required?: never;
487
+ export type CountOption = OptionSpelling & Described & Listed & EnvBinding & OptionExtensions & {
488
+ type: 'count';
461
489
  validate?: never;
490
+ default?: never;
491
+ multiple?: never;
492
+ required?: never;
462
493
  validateOmitted?: never;
494
+ polarity?: never;
495
+ implied?: never;
463
496
  };
464
- export type PluginOptionConfig = PluginStringOption | BooleanOption;
497
+ export type OptionConfig = StringOption | BooleanOption | CountOption;
498
+ /**
499
+ * The configuration of a global option a plugin declares: everything `option()` takes except the
500
+ * presence rules, which a global option never declares, because its validation runs on every
501
+ * Command. `globalOption()` states the same rule through `GlobalOmissionConstraint`, which also
502
+ * accepts a config typed as the wide `OptionConfig`.
503
+ */
504
+ export type GlobalOptionConfig = OptionConfig & Readonly<Partial<Record<PresenceRuleKey, never>>>;
465
505
  export type OptionValue<Config extends OptionConfig> = Config extends StringOption ? Config extends {
466
506
  multiple: true;
467
507
  } ? ValidatedValue<Config, string[]> : ValidatedValue<Config, string> | (Config extends {
@@ -470,7 +510,7 @@ export type OptionValue<Config extends OptionConfig> = Config extends StringOpti
470
510
  default: unknown;
471
511
  } | {
472
512
  validateOmitted: true;
473
- } ? never : undefined) : boolean;
513
+ } ? never : undefined) : Config extends CountOption ? number : boolean;
474
514
  /**
475
515
  * What an action receives. `graph` is the frozen graph `inspect()` returns for this run, and
476
516
  * `command` is the routed node inside it: the same two values the run's middleware receive.