@loomcli/core 0.1.1 → 0.2.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/index.d.ts CHANGED
@@ -2,11 +2,17 @@ export { Application } from './application.js';
2
2
  export { Command } from './command.js';
3
3
  export { validationContext, validationContextKey } from './context.js';
4
4
  export { DeclarationError, FatalError, InputError, InternalError, LoomError, MissingValueError, NonCallableCommandError, RepeatedOptionError, ShortGroupError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, UsageError, renderFailure, } from './errors.js';
5
+ export { extension, readExtension } from './extension.js';
5
6
  export { GlobalOptions } from './globals.js';
7
+ export { plugin } from './plugin.js';
6
8
  export { issuePath } from './validation.js';
7
9
  export type { StandardSchemaV1 } from '@standard-schema/spec';
8
10
  export type { ApplicationMethod, ApplicationOptions } from './application.js';
11
+ export type { ChainOutcome, MiddlewareContext } from './chain.js';
9
12
  export type { FailureRenderer, InputProblem } from './errors.js';
10
- export type { CommandMethod } from './command.js';
13
+ export type { AnyExtension, Extension, ExtensionValue } from './extension.js';
14
+ export type { CommandMethod, CommandOptions } from './command.js';
15
+ export type { CancellationReason } from './signals.js';
11
16
  export type { ArgumentNode, CommandGraph, CommandNode, OptionNode } from './inspect.js';
17
+ export type { Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionValues, } from './plugin.js';
12
18
  export type { Action, ActionArgs, ActionContext, ActionHandler, ActionOptions, ArgumentConfig, BooleanOption, ExitCode, Host, InputIdentity, InputTerminal, Out, OptionConfig, OutputTerminal, Renderer, RunOptions, ScalarArgument, StringOption, SuppliedInputs, ValidationContext, VariadicArgument, } from './types.js';
package/dist/index.js CHANGED
@@ -2,5 +2,7 @@ export { Application } from './application.js';
2
2
  export { Command } from './command.js';
3
3
  export { validationContext, validationContextKey } from './context.js';
4
4
  export { DeclarationError, FatalError, InputError, InternalError, LoomError, MissingValueError, NonCallableCommandError, RepeatedOptionError, ShortGroupError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, UsageError, renderFailure, } from './errors.js';
5
+ export { extension, readExtension } from './extension.js';
5
6
  export { GlobalOptions } from './globals.js';
7
+ export { plugin } from './plugin.js';
6
8
  export { issuePath } from './validation.js';
package/dist/inspect.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { BuiltCommand } from './command.js';
2
- import type { BuiltGlobals } from './globals.js';
1
+ import type { BuiltGraph } from './command.js';
3
2
  /** One declared argument. `default` wraps the declared value, so an explicit `undefined` shows. */
4
3
  interface ArgumentNode {
5
4
  readonly name: string;
5
+ readonly description: string | undefined;
6
6
  readonly required: boolean;
7
7
  readonly variadic: boolean;
8
8
  readonly validated: boolean;
@@ -10,11 +10,22 @@ interface ArgumentNode {
10
10
  readonly default: {
11
11
  readonly value: unknown;
12
12
  } | undefined;
13
+ readonly extensions: Readonly<Record<string, unknown>>;
13
14
  }
14
- /** One declared option, in the shape its type gives it. Spellings are the accepted CLI forms. */
15
+ /**
16
+ * One declared option, in the shape its type gives it. Spellings are the accepted CLI forms, and
17
+ * `scope` tells an application's own option from a plugin option, which reaches no action.
18
+ * `hidden` is `false` unless the declaration says `true`, and `deprecated` is the declared
19
+ * migration message or `undefined`. A listing projection omits a hidden node and marks a
20
+ * deprecated one; parsing binds without reading either.
21
+ */
15
22
  type OptionNode = {
16
23
  readonly type: 'string';
17
24
  readonly name: string;
25
+ readonly description: string | undefined;
26
+ readonly hidden: boolean;
27
+ readonly deprecated: string | undefined;
28
+ readonly scope: 'application' | 'plugin';
18
29
  readonly long: string | null;
19
30
  readonly short: string | null;
20
31
  readonly required: boolean;
@@ -24,38 +35,61 @@ type OptionNode = {
24
35
  readonly default: {
25
36
  readonly value: unknown;
26
37
  } | undefined;
38
+ readonly extensions: Readonly<Record<string, unknown>>;
27
39
  } | {
28
40
  readonly type: 'boolean';
29
41
  readonly name: string;
42
+ readonly description: string | undefined;
43
+ readonly hidden: boolean;
44
+ readonly deprecated: string | undefined;
45
+ readonly scope: 'application' | 'plugin';
30
46
  readonly long: string | null;
31
47
  readonly short: string | null;
32
48
  readonly negative: string | null;
33
49
  readonly polarity: 'positive' | 'negative' | 'both';
50
+ readonly extensions: Readonly<Record<string, unknown>>;
34
51
  };
35
52
  /**
36
53
  * One Command in the graph. `name` is `null` for the root, and `path` is its route from it.
37
- * `aliases` holds the hidden aliases in declaration order, so a Command appears once, under its
38
- * canonical name, and `path` never holds an alias.
54
+ * `aliases` holds the aliases in declaration order, so a Command appears once, under its canonical
55
+ * name, and `path` never holds an alias. The root reports the Application's description, so a
56
+ * projection that walks nodes never special-cases it, and it reads `hidden: false` and
57
+ * `deprecated: undefined`, the two core facts a listing reads on every other node.
39
58
  */
40
59
  interface CommandNode {
41
60
  readonly name: string | null;
42
61
  readonly aliases: readonly string[];
43
62
  readonly path: readonly string[];
63
+ readonly description: string | undefined;
64
+ readonly hidden: boolean;
65
+ readonly deprecated: string | undefined;
44
66
  readonly hasAction: boolean;
45
67
  readonly arguments: readonly ArgumentNode[];
46
68
  readonly options: readonly OptionNode[];
47
69
  readonly children: readonly CommandNode[];
70
+ readonly extensions: Readonly<Record<string, unknown>>;
48
71
  }
49
- /** One built graph as plain data. The globals appear once here and in no `CommandNode`. */
72
+ /**
73
+ * One built graph as plain data. The globals appear once here and in no `CommandNode`. `version`
74
+ * and `description` are the Application's own core facts. `version` is the declared string, or
75
+ * `0.0.0` when the Application declares none, so it is never `undefined`. `description` stays
76
+ * `undefined` where the Application declares none.
77
+ */
50
78
  interface CommandGraph {
51
79
  readonly name: string;
80
+ readonly version: string;
81
+ readonly description: string | undefined;
52
82
  readonly globals: readonly OptionNode[];
53
83
  readonly root: CommandNode;
54
84
  }
55
- /** Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. */
56
- declare function inspectGraph(name: string, graph: {
57
- globals: BuiltGlobals;
58
- root: BuiltCommand;
85
+ /**
86
+ * Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. The
87
+ * globals list holds the application's own options, then each installed plugin's in installation
88
+ * order, which is the order the globals table holds them in.
89
+ */
90
+ declare function inspectGraph(name: string, graph: BuiltGraph, facts: {
91
+ description: string | undefined;
92
+ version: string;
59
93
  }): CommandGraph;
60
94
  export type { ArgumentNode, CommandGraph, CommandNode, OptionNode };
61
95
  export { inspectGraph };
package/dist/inspect.js CHANGED
@@ -1,4 +1,7 @@
1
+ import { isPlainObject } from './facts.js';
1
2
  import { validatesOmission } from './validation.js';
3
+ /** A declaration that carries no extension value publishes one shared, empty frozen record. */
4
+ const noExtensions = Object.freeze({});
2
5
  /**
3
6
  * Reads the spellings out of the compiled table the parser uses, so inspection cannot report a
4
7
  * form the parser does not accept. Each entry carries its own role, so the naming convention has
@@ -13,14 +16,6 @@ function spellingsOf(table, name) {
13
16
  }
14
17
  return spellings;
15
18
  }
16
- /** A structural value core can copy faithfully. Anything else is a library object it leaves alone. */
17
- function isPlainObject(value) {
18
- if (value === null || typeof value !== 'object') {
19
- return false;
20
- }
21
- const prototype = Object.getPrototypeOf(value);
22
- return prototype === Object.prototype || prototype === null;
23
- }
24
19
  /**
25
20
  * A snapshot of one declared value. Arrays and plain objects are copied and frozen to any depth, so
26
21
  * a consumer cannot reach the declaration through the graph, and a later call reports the declared
@@ -39,18 +34,40 @@ function snapshot(value) {
39
34
  function declaredDefault(config) {
40
35
  return 'default' in config ? Object.freeze({ value: snapshot(config.default) }) : undefined;
41
36
  }
42
- function optionNode(input, table) {
37
+ /** One declaration's extension record, which is the shared empty one when it carries no value. */
38
+ function extensionsOf(records, declaration) {
39
+ return records.get(declaration) ?? noExtensions;
40
+ }
41
+ function optionNode(input, { records, scope, table }) {
43
42
  const { config, name } = input;
44
43
  const { long, negative, short } = spellingsOf(table, name);
44
+ const extensions = extensionsOf(records, input);
45
45
  const node = config.type === 'boolean'
46
- ? { long, name, negative, polarity: config.polarity ?? 'positive', short, type: 'boolean' }
46
+ ? {
47
+ deprecated: config.deprecated,
48
+ description: config.description,
49
+ extensions,
50
+ hidden: config.hidden === true,
51
+ long,
52
+ name,
53
+ negative,
54
+ polarity: config.polarity ?? 'positive',
55
+ scope,
56
+ short,
57
+ type: 'boolean',
58
+ }
47
59
  : {
48
60
  default: declaredDefault(config),
61
+ deprecated: config.deprecated,
62
+ description: config.description,
63
+ extensions,
64
+ hidden: config.hidden === true,
49
65
  long,
50
66
  // The parser reads the same test, so a collection reports as one here and there.
51
67
  multiple: config.multiple === true,
52
68
  name,
53
69
  required: config.required === true,
70
+ scope,
54
71
  short,
55
72
  type: 'string',
56
73
  validateOmitted: validatesOmission(input),
@@ -59,10 +76,12 @@ function optionNode(input, table) {
59
76
  return Object.freeze(node);
60
77
  }
61
78
  /** The built slots already answer presence and arity, so the node repeats no config reading. */
62
- function argumentNode(slot) {
79
+ function argumentNode(slot, records) {
63
80
  const { config, name } = slot.input;
64
81
  const node = {
65
82
  default: declaredDefault(config),
83
+ description: config.description,
84
+ extensions: extensionsOf(records, slot.input),
66
85
  name,
67
86
  required: slot.required,
68
87
  validateOmitted: validatesOmission(slot.input),
@@ -71,27 +90,51 @@ function argumentNode(slot) {
71
90
  };
72
91
  return Object.freeze(node);
73
92
  }
74
- function optionNodes(inputs, table) {
75
- return Object.freeze(inputs.filter((input) => input.kind === 'option').map((input) => optionNode(input, table)));
93
+ function optionNodes(inputs, read) {
94
+ return inputs.filter((input) => input.kind === 'option').map((input) => optionNode(input, read));
76
95
  }
77
- function commandNode(command, path) {
96
+ /**
97
+ * One Command as frozen plain data. A child reports the description its own declaration carries,
98
+ * and the root reports the Application's, which is why the caller supplies that one.
99
+ */
100
+ function commandNode(command, place) {
101
+ const { path, records } = place;
78
102
  const node = {
79
103
  aliases: Object.freeze([...command.aliases]),
80
- arguments: Object.freeze(command.arguments.map((slot) => argumentNode(slot))),
81
- children: Object.freeze([...command.children].map(([name, child]) => commandNode(child, Object.freeze([...path, name])))),
104
+ arguments: Object.freeze(command.arguments.map((slot) => argumentNode(slot, records))),
105
+ children: Object.freeze([...command.children].map(([name, child]) => commandNode(child, { path: Object.freeze([...path, name]), records }))),
106
+ deprecated: command.deprecated,
107
+ description: 'description' in place ? place.description : command.description,
108
+ extensions: command.extensions,
82
109
  hasAction: command.dispatch !== undefined,
110
+ hidden: command.hidden,
83
111
  name: command.name,
84
- options: optionNodes(command.inputs, command.options),
112
+ options: Object.freeze(optionNodes(command.inputs, { records, scope: 'application', table: command.options })),
85
113
  path,
86
114
  };
87
115
  return Object.freeze(node);
88
116
  }
89
- /** Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. */
90
- function inspectGraph(name, graph) {
117
+ /**
118
+ * Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. The
119
+ * globals list holds the application's own options, then each installed plugin's in installation
120
+ * order, which is the order the globals table holds them in.
121
+ */
122
+ function inspectGraph(name, graph, facts) {
123
+ const records = graph.extensions;
124
+ const table = graph.globals.options;
91
125
  const inspected = {
92
- globals: optionNodes(graph.globals.inputs, graph.globals.options),
126
+ description: facts.description,
127
+ globals: Object.freeze([
128
+ ...optionNodes(graph.globals.inputs, { records, scope: 'application', table }),
129
+ ...graph.globals.plugins.flatMap((installed) => optionNodes(installed.inputs, { records, scope: 'plugin', table })),
130
+ ]),
93
131
  name,
94
- root: commandNode(graph.root, Object.freeze([])),
132
+ root: commandNode(graph.root, {
133
+ description: facts.description,
134
+ path: Object.freeze([]),
135
+ records,
136
+ }),
137
+ version: facts.version,
95
138
  };
96
139
  return Object.freeze(inspected);
97
140
  }
package/dist/options.d.ts CHANGED
@@ -22,6 +22,13 @@ type OptionForm = {
22
22
  type OptionSpelling = OptionForm & {
23
23
  role: SpellingRole;
24
24
  };
25
+ /**
26
+ * One Boolean option's value for one invocation: the value the parser consumed, or the value its
27
+ * declared polarity gives an absent option. A negative-only option is absent as `true`, because its
28
+ * one spelling turns the value off. Every scope reads it here, so a plugin option and a validated
29
+ * declaration answer the same rule.
30
+ */
31
+ export declare function booleanValue(values: OptionValues, name: string, config: OptionConfig): boolean;
25
32
  export declare function compileOptions(declarations: readonly OptionDeclaration[], subject: string): Map<string, OptionSpelling>;
26
33
  /** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
27
34
  export declare function extractGlobals(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
package/dist/options.js CHANGED
@@ -48,6 +48,15 @@ function addSpelling(spellings, spelling, option) {
48
48
  }
49
49
  spellings.set(spelling, option);
50
50
  }
51
+ /**
52
+ * One Boolean option's value for one invocation: the value the parser consumed, or the value its
53
+ * declared polarity gives an absent option. A negative-only option is absent as `true`, because its
54
+ * one spelling turns the value off. Every scope reads it here, so a plugin option and a validated
55
+ * declaration answer the same rule.
56
+ */
57
+ export function booleanValue(values, name, config) {
58
+ return values.booleans.get(name) ?? config.polarity === 'negative';
59
+ }
51
60
  export function compileOptions(declarations, subject) {
52
61
  const spellings = new Map();
53
62
  const names = new Set();
@@ -0,0 +1,116 @@
1
+ import type { MiddlewareContext } from './chain.js';
2
+ import type { FailureRenderer } from './errors.js';
3
+ import type { AnyExtension, DescriptorRegistry, ExtensionRecords } from './extension.js';
4
+ import type { ProcessSignal } from './signals.js';
5
+ import type { OptionValue, PluginOptionConfig } from './types.js';
6
+ import type { OptionInput } from './validation.js';
7
+ /**
8
+ * The declaration record a plugin contributes its options under: the parsing part of an option
9
+ * config, keyed by option name. A plugin option carries no schema and no presence rule, so the
10
+ * config type publishes neither, and build repeats the rule for a JavaScript author.
11
+ */
12
+ type PluginOptions = Readonly<Record<string, PluginOptionConfig>>;
13
+ /** The values one plugin's own options take, read through the same rules an action's options are. */
14
+ type PluginOptionValues<Options extends PluginOptions> = {
15
+ readonly [Name in keyof Options]: OptionValue<Options[Name]>;
16
+ };
17
+ /** Phantom key. It carries a plugin's declared options in a read position and holds no value. */
18
+ declare const pluginOptions: unique symbol;
19
+ /**
20
+ * One plugin's declarations as the registry holds them, with the generic parts erased. Build reads
21
+ * every one of them defensively, because a JavaScript author reaches the same slots, so the erased
22
+ * shape is what the rules below read and no declaration is claimed to be well formed here.
23
+ */
24
+ interface DeclaredPlugin {
25
+ options?: PluginOptions;
26
+ middleware?: {
27
+ activate?: unknown;
28
+ load?: unknown;
29
+ };
30
+ extensions?: readonly AnyExtension[];
31
+ failures?: readonly FailureRenderer[];
32
+ signals?: unknown;
33
+ }
34
+ /** The declarations behind one plugin value, read by this package alone. */
35
+ interface PluginNode {
36
+ definition: DeclaredPlugin;
37
+ identity: unknown;
38
+ }
39
+ /**
40
+ * The runtime value `plugin()` returns. `Options` appears in a read position alone, which makes it
41
+ * covariant: a `plugins` list holds plugins with different options the way `failures` holds
42
+ * renderers for different classes, and `Middleware` and `load` accept a narrower plugin.
43
+ */
44
+ declare class PluginDeclaration<Options extends PluginOptions> {
45
+ readonly [pluginOptions]: () => Options;
46
+ constructor(node: PluginNode);
47
+ }
48
+ /**
49
+ * One plugin, as the opaque value `plugin()` returns. The declarations behind it stay private to
50
+ * this package, so no consumer can read or replace them.
51
+ */
52
+ type Plugin<Options extends PluginOptions = PluginOptions> = Pick<PluginDeclaration<Options>, typeof pluginOptions>;
53
+ /** The declared options of a plugin, or of the factory that returns one. */
54
+ type OptionsOf<Contributor> = Contributor extends Plugin<infer Options> ? Options : Contributor extends (...args: never[]) => Plugin<infer Options> ? Options : PluginOptions;
55
+ /** A middleware reads its own plugin's options and either takes over or continues the chain. */
56
+ type Middleware<Contributor extends Plugin | ((...args: never[]) => Plugin)> = (context: MiddlewareContext<OptionsOf<Contributor>>) => Promise<void> | void;
57
+ /** Everything a plugin declares. It holds declarations alone and performs no work. */
58
+ interface PluginDefinition<Options extends PluginOptions = PluginOptions> {
59
+ options?: Options;
60
+ middleware?: {
61
+ activate: 'always' | readonly (keyof Options & string)[];
62
+ load: () => Promise<{
63
+ default: Middleware<Plugin<Options>>;
64
+ }>;
65
+ };
66
+ extensions?: readonly AnyExtension[];
67
+ failures?: readonly FailureRenderer[];
68
+ signals?: readonly ('SIGINT' | 'SIGTERM')[];
69
+ }
70
+ /**
71
+ * One plugin: an identity and the declarations it contributes. The value performs no work when it
72
+ * is created and none when it is installed, so an installed plugin an invocation never reaches
73
+ * costs that invocation nothing.
74
+ */
75
+ declare function plugin<Options extends PluginOptions = PluginOptions>(identity: string, definition: PluginDefinition<Options>): Plugin<Options>;
76
+ /** One installed plugin, with the declarations build reads out of it in installation order. */
77
+ interface InstalledPlugin {
78
+ declaration: DeclaredPlugin;
79
+ identity: string;
80
+ }
81
+ /** How every plugin diagnostic names one plugin at the start of a sentence. */
82
+ declare function pluginSentence(identity: string): string;
83
+ /**
84
+ * The installed list in composition order, with the rules that read the list itself. The slot is
85
+ * read defensively, because a JavaScript author reaches it with any value. Each plugin's own
86
+ * declarations are read by the build steps that consume them, in the order those steps run.
87
+ */
88
+ declare function installPlugins(plugins: unknown): readonly InstalledPlugin[];
89
+ /** One plugin's declared middleware: what wakes it, and the loader that fetches its module. */
90
+ interface BuiltMiddleware {
91
+ activate: 'always' | readonly string[];
92
+ load: () => unknown;
93
+ }
94
+ /** One installed plugin's declarations, read once per build in installation order. */
95
+ interface BuiltPlugin {
96
+ failures: readonly FailureRenderer[];
97
+ identity: string;
98
+ inputs: readonly OptionInput[];
99
+ middleware: BuiltMiddleware | undefined;
100
+ signals: readonly ProcessSignal[];
101
+ }
102
+ /** The shared registers one build fills while it reads each plugin's contributions. */
103
+ interface PluginBuild {
104
+ descriptors: DescriptorRegistry;
105
+ extensions: ExtensionRecords;
106
+ }
107
+ /**
108
+ * Every installed plugin's declarations, in installation order. A plugin's own extensions register
109
+ * before any declaration carries a value, so a duplicated package copy is reported from the list
110
+ * that installed it.
111
+ */
112
+ declare function buildPlugins(installed: readonly InstalledPlugin[], build: PluginBuild): readonly BuiltPlugin[];
113
+ /** The signals the one slot owner claimed, or none when no installed plugin claims the slot. */
114
+ declare function ownedSignals(plugins: readonly BuiltPlugin[]): readonly ProcessSignal[];
115
+ export type { BuiltPlugin, InstalledPlugin, Middleware, OptionsOf, Plugin, PluginBuild, PluginDefinition, PluginOptions, PluginOptionValues, };
116
+ export { buildPlugins, installPlugins, ownedSignals, plugin, pluginSentence };