@loomcli/core 0.1.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/application.d.ts +72 -0
- package/dist/application.js +188 -0
- package/dist/command.d.ts +176 -0
- package/dist/command.js +452 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.js +18 -0
- package/dist/errors.d.ts +158 -0
- package/dist/errors.js +261 -0
- package/dist/globals.d.ts +36 -0
- package/dist/globals.js +71 -0
- package/dist/host.d.ts +3 -0
- package/dist/host.js +37 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/inspect.d.ts +61 -0
- package/dist/inspect.js +98 -0
- package/dist/options.d.ts +38 -0
- package/dist/options.js +219 -0
- package/dist/output.d.ts +52 -0
- package/dist/output.js +188 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +83 -0
- package/dist/validation.js +406 -0
- package/package.json +28 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/** The routed path names the Command a token fault belongs to; an empty path is the root. */
|
|
2
|
+
function routedSentence(command) {
|
|
3
|
+
const name = command.at(-1);
|
|
4
|
+
return name === undefined ? 'The root Command' : commandSentence(name);
|
|
5
|
+
}
|
|
6
|
+
function shortGroupMessage(fault) {
|
|
7
|
+
return fault.reason === 'value-position'
|
|
8
|
+
? `Value option "${fault.token}" must be last in its short group. Supply its value in the next token.`
|
|
9
|
+
: `Short group "${fault.token}" mixes the global option "-${fault.global}" with "-${fault.other}", which is not a global option. Supply global options as separate tokens, and local options after their command name.`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Core's own text for one failure: its message under the category prefix its class carries, with
|
|
13
|
+
* the trailing newline every renderer's text carries. The four categories are disjoint branches of
|
|
14
|
+
* the hierarchy, so one ordered test reads every class, and a class without a prefix of its own
|
|
15
|
+
* writes the sentence alone. A default rendering is the same kind of value as a custom one, so
|
|
16
|
+
* nothing downstream composes the newline for it.
|
|
17
|
+
*/
|
|
18
|
+
function defaultText(failure) {
|
|
19
|
+
if (failure instanceof UsageError) {
|
|
20
|
+
return `Invalid input: ${failure.message}\n`;
|
|
21
|
+
}
|
|
22
|
+
if (failure instanceof DeclarationError) {
|
|
23
|
+
return `Invalid declaration: ${failure.message}\n`;
|
|
24
|
+
}
|
|
25
|
+
if (failure instanceof InternalError) {
|
|
26
|
+
return `Internal error: ${failure.message}\n`;
|
|
27
|
+
}
|
|
28
|
+
return `${failure.message}\n`;
|
|
29
|
+
}
|
|
30
|
+
/** Every prototype in a failure's chain, most derived first, so one walk reads the registry. */
|
|
31
|
+
function chainOf(failure) {
|
|
32
|
+
const chain = [];
|
|
33
|
+
let prototype = Object.getPrototypeOf(failure);
|
|
34
|
+
while (prototype !== null) {
|
|
35
|
+
chain.push(prototype);
|
|
36
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
37
|
+
}
|
|
38
|
+
return chain;
|
|
39
|
+
}
|
|
40
|
+
/** Authored registrations register here, so the public type publishes nothing to reach. */
|
|
41
|
+
const nodes = new WeakMap();
|
|
42
|
+
/** The runtime value `renderFailure` returns. Its pair lives in the registry above. */
|
|
43
|
+
class RegisteredFailure {
|
|
44
|
+
constructor(registration) {
|
|
45
|
+
nodes.set(this, registration);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Reads the pair behind a registered value; anything else is a declaration error. */
|
|
49
|
+
function nodeOf(value) {
|
|
50
|
+
const registration = nodes.get(value);
|
|
51
|
+
if (!registration) {
|
|
52
|
+
throw new DeclarationError('The Application holds a value that is not a failure renderer. Supply the value returned by renderFailure(type, renderer).');
|
|
53
|
+
}
|
|
54
|
+
return registration;
|
|
55
|
+
}
|
|
56
|
+
/** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
|
|
57
|
+
export function commandSubject(name) {
|
|
58
|
+
return name === null ? 'the root Command' : `Command "${name}"`;
|
|
59
|
+
}
|
|
60
|
+
/** The same subject at the start of a sentence. */
|
|
61
|
+
export function commandSentence(name) {
|
|
62
|
+
const subject = commandSubject(name);
|
|
63
|
+
return `${subject.slice(0, 1).toUpperCase()}${subject.slice(1)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Every failure `run()` reports is an instance of a public class. Each class carries the facts its
|
|
67
|
+
* sentence interpolates, so a renderer reads them instead of parsing prose, and the exit status is
|
|
68
|
+
* a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
|
|
69
|
+
* default renderers add it.
|
|
70
|
+
*/
|
|
71
|
+
export class LoomError extends Error {
|
|
72
|
+
exitCode;
|
|
73
|
+
constructor(message, exitCode) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.exitCode = exitCode;
|
|
76
|
+
this.name = 'LoomError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Exit 2: the invocation, not the application, is wrong. */
|
|
80
|
+
export class UsageError extends LoomError {
|
|
81
|
+
constructor(message) {
|
|
82
|
+
super(message, 2);
|
|
83
|
+
this.name = 'UsageError';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The whole validation phase in authoring order, so one failure reports every rejected input. */
|
|
87
|
+
export class InputError extends UsageError {
|
|
88
|
+
problems;
|
|
89
|
+
constructor(message, problems) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = 'InputError';
|
|
92
|
+
this.problems = problems;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export class UnknownCommandError extends UsageError {
|
|
96
|
+
token;
|
|
97
|
+
candidates;
|
|
98
|
+
constructor(token, candidates) {
|
|
99
|
+
super(`Unknown command "${token}". Use one of: ${candidates.join(', ')}.`);
|
|
100
|
+
this.candidates = candidates;
|
|
101
|
+
this.name = 'UnknownCommandError';
|
|
102
|
+
this.token = token;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** A group answers no invocation of its own, so the routed path names no callable Command. */
|
|
106
|
+
export class NonCallableCommandError extends UsageError {
|
|
107
|
+
command;
|
|
108
|
+
candidates;
|
|
109
|
+
constructor(command, candidates) {
|
|
110
|
+
super(`${routedSentence(command)} requires a subcommand. Use one of: ${candidates.join(', ')}.`);
|
|
111
|
+
this.candidates = candidates;
|
|
112
|
+
this.command = command;
|
|
113
|
+
this.name = 'NonCallableCommandError';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export class UnexpectedArgumentError extends UsageError {
|
|
117
|
+
command;
|
|
118
|
+
accepted;
|
|
119
|
+
extra;
|
|
120
|
+
constructor(command, accepted, extra) {
|
|
121
|
+
super(accepted === 0
|
|
122
|
+
? `${routedSentence(command)} accepts no arguments. Remove the supplied values.`
|
|
123
|
+
: `${routedSentence(command)} accepts ${accepted} ${accepted === 1 ? 'argument' : 'arguments'}. Remove the extra values.`);
|
|
124
|
+
this.accepted = accepted;
|
|
125
|
+
this.command = command;
|
|
126
|
+
this.extra = extra;
|
|
127
|
+
this.name = 'UnexpectedArgumentError';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export class UnknownOptionError extends UsageError {
|
|
131
|
+
spelling;
|
|
132
|
+
constructor(spelling) {
|
|
133
|
+
super(`Unknown option "${spelling}". Supply a declared option; prefix a hyphenated path with "./".`);
|
|
134
|
+
this.name = 'UnknownOptionError';
|
|
135
|
+
this.spelling = spelling;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export class MissingValueError extends UsageError {
|
|
139
|
+
spelling;
|
|
140
|
+
constructor(spelling) {
|
|
141
|
+
super(`Option "${spelling}" requires a value. Supply a value after "${spelling}".`);
|
|
142
|
+
this.name = 'MissingValueError';
|
|
143
|
+
this.spelling = spelling;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** A Boolean spelling takes no value, so the token carried one the declaration cannot accept. */
|
|
147
|
+
export class UnexpectedValueError extends UsageError {
|
|
148
|
+
spelling;
|
|
149
|
+
value;
|
|
150
|
+
constructor(spelling, value) {
|
|
151
|
+
super(`Boolean option "${spelling}" does not accept a value. Supply the flag alone.`);
|
|
152
|
+
this.name = 'UnexpectedValueError';
|
|
153
|
+
this.spelling = spelling;
|
|
154
|
+
this.value = value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export class RepeatedOptionError extends UsageError {
|
|
158
|
+
spelling;
|
|
159
|
+
constructor(spelling) {
|
|
160
|
+
super(`Option "${spelling}" can be supplied only once. Remove the repeated option.`);
|
|
161
|
+
this.name = 'RepeatedOptionError';
|
|
162
|
+
this.spelling = spelling;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export class ShortGroupError extends UsageError {
|
|
166
|
+
token;
|
|
167
|
+
reason;
|
|
168
|
+
constructor(fault) {
|
|
169
|
+
super(shortGroupMessage(fault));
|
|
170
|
+
this.name = 'ShortGroupError';
|
|
171
|
+
this.reason = fault.reason;
|
|
172
|
+
this.token = fault.token;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Exit 1: the declaration is wrong, so the author reads the diagnostic. */
|
|
176
|
+
export class DeclarationError extends LoomError {
|
|
177
|
+
constructor(message) {
|
|
178
|
+
super(message, 1);
|
|
179
|
+
this.name = 'DeclarationError';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Exit 1: the application ended the invocation itself. An application may subclass it. */
|
|
183
|
+
export class FatalError extends LoomError {
|
|
184
|
+
constructor(message) {
|
|
185
|
+
super(message, 1);
|
|
186
|
+
this.name = 'FatalError';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Exit 1: an unexpected exception, a non-error throw, or a renderer that could not answer. */
|
|
190
|
+
export class InternalError extends LoomError {
|
|
191
|
+
cause;
|
|
192
|
+
constructor(message, cause) {
|
|
193
|
+
super(message, 1);
|
|
194
|
+
this.cause = cause;
|
|
195
|
+
this.name = 'InternalError';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** What a diagnostic says about an unexpected value, whether or not it was an Error. */
|
|
199
|
+
export function reasonOf(thrown) {
|
|
200
|
+
return thrown instanceof Error ? thrown.message : 'An unknown error occurred.';
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Why a returned value is not the text a renderer owes. A renderer is synchronous, so a returned
|
|
204
|
+
* promise is a non-string return like any other, and its rejection is adopted and swallowed here:
|
|
205
|
+
* an unobserved rejection would end the process before the invocation could report anything.
|
|
206
|
+
*/
|
|
207
|
+
export function notTextReason(value) {
|
|
208
|
+
void Promise.resolve(value).catch(() => undefined);
|
|
209
|
+
return `The renderer returned ${typeof value} instead of a string.`;
|
|
210
|
+
}
|
|
211
|
+
/** Every thrown value reaches reporting as a failure class; anything else is internal. */
|
|
212
|
+
export function toFailure(thrown) {
|
|
213
|
+
return thrown instanceof LoomError ? thrown : new InternalError(reasonOf(thrown), thrown);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* A registration pairing one failure class with a renderer for its instances. The helper is the
|
|
217
|
+
* typed path for a class-keyed list, because an array literal cannot carry a different type
|
|
218
|
+
* parameter per element.
|
|
219
|
+
*/
|
|
220
|
+
export function renderFailure(type, renderer) {
|
|
221
|
+
return new RegisteredFailure({
|
|
222
|
+
name: 'name' in type && typeof type.name === 'string' ? type.name : 'a failure class',
|
|
223
|
+
prototype: 'prototype' in type ? type.prototype : undefined,
|
|
224
|
+
// Resolution reaches this registration through the same class, so the test always holds.
|
|
225
|
+
render: (failure) => (failure instanceof type ? renderer.render(failure) : undefined),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** One class answers to one renderer, so a second registration for it is a declaration fault. */
|
|
229
|
+
export function buildFailures(failures) {
|
|
230
|
+
const registry = new Map();
|
|
231
|
+
for (const failure of failures) {
|
|
232
|
+
const registration = nodeOf(failure);
|
|
233
|
+
if (registry.has(registration.prototype)) {
|
|
234
|
+
throw new DeclarationError(`The Application registers two failure renderers for "${registration.name}". Remove one registration.`);
|
|
235
|
+
}
|
|
236
|
+
registry.set(registration.prototype, registration);
|
|
237
|
+
}
|
|
238
|
+
return registry;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The text core writes for one failure. Resolution walks the failure's prototype chain most
|
|
242
|
+
* derived first through the application's registrations, then falls to core's own text, so a
|
|
243
|
+
* registration for a base class brands every failure below it.
|
|
244
|
+
*/
|
|
245
|
+
export function describeFailure(registry, failure) {
|
|
246
|
+
const registration = chainOf(failure)
|
|
247
|
+
.map((prototype) => registry.get(prototype))
|
|
248
|
+
.find((entry) => entry !== undefined);
|
|
249
|
+
if (!registration) {
|
|
250
|
+
return { kind: 'rendered', text: defaultText(failure) };
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const text = registration.render(failure);
|
|
254
|
+
return typeof text === 'string'
|
|
255
|
+
? { kind: 'rendered', text }
|
|
256
|
+
: { kind: 'unrendered', reason: notTextReason(text), text: defaultText(failure) };
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
return { kind: 'unrendered', reason: reasonOf(error), text: defaultText(failure) };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { compileOptions } from './options.js';
|
|
2
|
+
import type { DefaultConstraint, MultipleConstraint, NameConstraint, OptionConfig, OptionValue, ValidateOmittedConstraint } from './types.js';
|
|
3
|
+
import type { InputDeclaration, ValidatedInputs } from './validation.js';
|
|
4
|
+
/** Phantom key. It keeps the declared option types exact and holds no runtime value. */
|
|
5
|
+
declare const declaredTypes: unique symbol;
|
|
6
|
+
/** The compiled global table: one spelling map shared by every Command in the graph. */
|
|
7
|
+
interface BuiltGlobals {
|
|
8
|
+
inputs: readonly InputDeclaration[];
|
|
9
|
+
names: ReadonlySet<string>;
|
|
10
|
+
options: ReturnType<typeof compileOptions>;
|
|
11
|
+
source: unknown;
|
|
12
|
+
}
|
|
13
|
+
declare class GlobalOptionsBuilder<Options> {
|
|
14
|
+
#private;
|
|
15
|
+
readonly [declaredTypes]: Options;
|
|
16
|
+
constructor(inputs: readonly InputDeclaration[], bind: (values: ValidatedInputs) => Options);
|
|
17
|
+
option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): GlobalOptions<Options & Record<Name, OptionValue<Config>>>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Application-wide options. `option()` is the whole authoring surface: it returns a new value and
|
|
21
|
+
* leaves its receiver unchanged. The value the declarations share is the application's globals.
|
|
22
|
+
* The declarations themselves stay private, so no consumer can read or replace them.
|
|
23
|
+
*/
|
|
24
|
+
type GlobalOptions<Options = {}> = Pick<GlobalOptionsBuilder<Options>, typeof declaredTypes | 'option'>;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a value is an authored GlobalOptions declaration, whichever call produced it. The
|
|
27
|
+
* registry is the test, because `option()` returns a new declaration of its own and the exported
|
|
28
|
+
* constructor is only the first of them.
|
|
29
|
+
*/
|
|
30
|
+
declare function isGlobalOptions(value: unknown): boolean;
|
|
31
|
+
/** Absent globals compile to an empty table whose source is `undefined`, like the declarations. */
|
|
32
|
+
declare function buildGlobals(globals: object | undefined): BuiltGlobals;
|
|
33
|
+
declare function bindGlobals<Options>(globals: GlobalOptions<Options> | undefined, values: ValidatedInputs): Options;
|
|
34
|
+
declare const GlobalOptions: new () => GlobalOptions;
|
|
35
|
+
export type { BuiltGlobals };
|
|
36
|
+
export { bindGlobals, buildGlobals, GlobalOptions, isGlobalOptions };
|
package/dist/globals.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DeclarationError } from './errors.js';
|
|
2
|
+
import { compileOptions } from './options.js';
|
|
3
|
+
import { captureConfig } from './validation.js';
|
|
4
|
+
const globalSubject = 'the global options';
|
|
5
|
+
/** Authored values register here, so the public type publishes no state to reach or replace. */
|
|
6
|
+
const nodes = new WeakMap();
|
|
7
|
+
function compileTable(inputs, source) {
|
|
8
|
+
return {
|
|
9
|
+
inputs,
|
|
10
|
+
names: new Set(inputs.map((input) => input.name)),
|
|
11
|
+
options: compileOptions(inputs.filter((input) => input.kind === 'option'), globalSubject),
|
|
12
|
+
source,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
class GlobalOptionsBuilder {
|
|
16
|
+
#bind;
|
|
17
|
+
#inputs;
|
|
18
|
+
constructor(inputs, bind) {
|
|
19
|
+
this.#bind = bind;
|
|
20
|
+
this.#inputs = inputs;
|
|
21
|
+
nodes.set(this, { bind, build: () => compileTable(inputs, this) });
|
|
22
|
+
}
|
|
23
|
+
option(name, config) {
|
|
24
|
+
const input = {
|
|
25
|
+
config: captureConfig(config),
|
|
26
|
+
kind: 'option',
|
|
27
|
+
name,
|
|
28
|
+
};
|
|
29
|
+
const previous = this.#bind;
|
|
30
|
+
return new GlobalOptionsBuilder([...this.#inputs, input], (values) => ({
|
|
31
|
+
...previous(values),
|
|
32
|
+
...values.option(input),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Reads the declarations behind an authored value; anything else is a declaration error. */
|
|
37
|
+
function nodeOf(globals) {
|
|
38
|
+
const node = nodes.get(globals);
|
|
39
|
+
if (!node) {
|
|
40
|
+
throw new DeclarationError('The Application holds a value that is not a GlobalOptions declaration. Supply the value returned by new GlobalOptions().');
|
|
41
|
+
}
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether a value is an authored GlobalOptions declaration, whichever call produced it. The
|
|
46
|
+
* registry is the test, because `option()` returns a new declaration of its own and the exported
|
|
47
|
+
* constructor is only the first of them.
|
|
48
|
+
*/
|
|
49
|
+
function isGlobalOptions(value) {
|
|
50
|
+
return typeof value === 'object' && value !== null && nodes.has(value);
|
|
51
|
+
}
|
|
52
|
+
/** Absent globals compile to an empty table whose source is `undefined`, like the declarations. */
|
|
53
|
+
function buildGlobals(globals) {
|
|
54
|
+
return globals === undefined ? compileTable([], undefined) : nodeOf(globals).build();
|
|
55
|
+
}
|
|
56
|
+
function bindGlobals(globals, values) {
|
|
57
|
+
const bound = globals === undefined ? {} : nodeOf(globals).bind(values);
|
|
58
|
+
// Last resort: no typed path exists. The public GlobalOptions type hides its declarations.
|
|
59
|
+
// The registry is the only bridge from a value to its binder, and a WeakMap cannot carry the
|
|
60
|
+
// Options type of its key. It holds because the registered binder belongs to this value alone.
|
|
61
|
+
// Its record composes exactly the declarations that its Options type records.
|
|
62
|
+
// A declaration without globals publishes `Options = {}`, and the empty record is exactly that.
|
|
63
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
64
|
+
return bound;
|
|
65
|
+
}
|
|
66
|
+
const GlobalOptions = class extends GlobalOptionsBuilder {
|
|
67
|
+
constructor() {
|
|
68
|
+
super([], () => ({}));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
export { bindGlobals, buildGlobals, GlobalOptions, isGlobalOptions };
|
package/dist/host.d.ts
ADDED
package/dist/host.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Process stream declarations assume a terminal, but pipes omit isTTY at runtime.
|
|
2
|
+
function isTerminal(stream) {
|
|
3
|
+
return stream.isTTY === true;
|
|
4
|
+
}
|
|
5
|
+
function dimension(value) {
|
|
6
|
+
return value === 0 ? undefined : value;
|
|
7
|
+
}
|
|
8
|
+
export function captureHost(overrides, stderr) {
|
|
9
|
+
const { argv, cwd, env, stdin, stdout, terminal } = overrides ?? {};
|
|
10
|
+
return {
|
|
11
|
+
argv: [...(argv ?? process.argv.slice(2))],
|
|
12
|
+
cwd: cwd ?? process.cwd(),
|
|
13
|
+
env: { ...(env ?? process.env) },
|
|
14
|
+
stderr,
|
|
15
|
+
stdin: stdin ?? process.stdin,
|
|
16
|
+
stdout: stdout ?? process.stdout,
|
|
17
|
+
terminal: terminal
|
|
18
|
+
? {
|
|
19
|
+
stderr: { ...terminal.stderr },
|
|
20
|
+
stdin: { ...terminal.stdin },
|
|
21
|
+
stdout: { ...terminal.stdout },
|
|
22
|
+
}
|
|
23
|
+
: {
|
|
24
|
+
stderr: {
|
|
25
|
+
columns: dimension(process.stderr.columns),
|
|
26
|
+
isTTY: isTerminal(process.stderr),
|
|
27
|
+
rows: dimension(process.stderr.rows),
|
|
28
|
+
},
|
|
29
|
+
stdin: { isTTY: isTerminal(process.stdin) },
|
|
30
|
+
stdout: {
|
|
31
|
+
columns: dimension(process.stdout.columns),
|
|
32
|
+
isTTY: isTerminal(process.stdout),
|
|
33
|
+
rows: dimension(process.stdout.rows),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { Application } from './application.js';
|
|
2
|
+
export { Command } from './command.js';
|
|
3
|
+
export { validationContext, validationContextKey } from './context.js';
|
|
4
|
+
export { DeclarationError, FatalError, InputError, InternalError, LoomError, MissingValueError, NonCallableCommandError, RepeatedOptionError, ShortGroupError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, UsageError, renderFailure, } from './errors.js';
|
|
5
|
+
export { GlobalOptions } from './globals.js';
|
|
6
|
+
export { issuePath } from './validation.js';
|
|
7
|
+
export type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
8
|
+
export type { ApplicationMethod, ApplicationOptions } from './application.js';
|
|
9
|
+
export type { FailureRenderer, InputProblem } from './errors.js';
|
|
10
|
+
export type { CommandMethod } from './command.js';
|
|
11
|
+
export type { ArgumentNode, CommandGraph, CommandNode, OptionNode } from './inspect.js';
|
|
12
|
+
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
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Application } from './application.js';
|
|
2
|
+
export { Command } from './command.js';
|
|
3
|
+
export { validationContext, validationContextKey } from './context.js';
|
|
4
|
+
export { DeclarationError, FatalError, InputError, InternalError, LoomError, MissingValueError, NonCallableCommandError, RepeatedOptionError, ShortGroupError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, UsageError, renderFailure, } from './errors.js';
|
|
5
|
+
export { GlobalOptions } from './globals.js';
|
|
6
|
+
export { issuePath } from './validation.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { BuiltCommand } from './command.js';
|
|
2
|
+
import type { BuiltGlobals } from './globals.js';
|
|
3
|
+
/** One declared argument. `default` wraps the declared value, so an explicit `undefined` shows. */
|
|
4
|
+
interface ArgumentNode {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly required: boolean;
|
|
7
|
+
readonly variadic: boolean;
|
|
8
|
+
readonly validated: boolean;
|
|
9
|
+
readonly validateOmitted: boolean;
|
|
10
|
+
readonly default: {
|
|
11
|
+
readonly value: unknown;
|
|
12
|
+
} | undefined;
|
|
13
|
+
}
|
|
14
|
+
/** One declared option, in the shape its type gives it. Spellings are the accepted CLI forms. */
|
|
15
|
+
type OptionNode = {
|
|
16
|
+
readonly type: 'string';
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly long: string | null;
|
|
19
|
+
readonly short: string | null;
|
|
20
|
+
readonly required: boolean;
|
|
21
|
+
readonly multiple: boolean;
|
|
22
|
+
readonly validated: boolean;
|
|
23
|
+
readonly validateOmitted: boolean;
|
|
24
|
+
readonly default: {
|
|
25
|
+
readonly value: unknown;
|
|
26
|
+
} | undefined;
|
|
27
|
+
} | {
|
|
28
|
+
readonly type: 'boolean';
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly long: string | null;
|
|
31
|
+
readonly short: string | null;
|
|
32
|
+
readonly negative: string | null;
|
|
33
|
+
readonly polarity: 'positive' | 'negative' | 'both';
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 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.
|
|
39
|
+
*/
|
|
40
|
+
interface CommandNode {
|
|
41
|
+
readonly name: string | null;
|
|
42
|
+
readonly aliases: readonly string[];
|
|
43
|
+
readonly path: readonly string[];
|
|
44
|
+
readonly hasAction: boolean;
|
|
45
|
+
readonly arguments: readonly ArgumentNode[];
|
|
46
|
+
readonly options: readonly OptionNode[];
|
|
47
|
+
readonly children: readonly CommandNode[];
|
|
48
|
+
}
|
|
49
|
+
/** One built graph as plain data. The globals appear once here and in no `CommandNode`. */
|
|
50
|
+
interface CommandGraph {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly globals: readonly OptionNode[];
|
|
53
|
+
readonly root: CommandNode;
|
|
54
|
+
}
|
|
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;
|
|
59
|
+
}): CommandGraph;
|
|
60
|
+
export type { ArgumentNode, CommandGraph, CommandNode, OptionNode };
|
|
61
|
+
export { inspectGraph };
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { validatesOmission } from './validation.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads the spellings out of the compiled table the parser uses, so inspection cannot report a
|
|
4
|
+
* form the parser does not accept. Each entry carries its own role, so the naming convention has
|
|
5
|
+
* one owner: the table that writes it.
|
|
6
|
+
*/
|
|
7
|
+
function spellingsOf(table, name) {
|
|
8
|
+
const spellings = { long: null, negative: null, short: null };
|
|
9
|
+
for (const [spelling, option] of table) {
|
|
10
|
+
if (option.name === name) {
|
|
11
|
+
spellings[option.role] = spelling;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return spellings;
|
|
15
|
+
}
|
|
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
|
+
/**
|
|
25
|
+
* A snapshot of one declared value. Arrays and plain objects are copied and frozen to any depth, so
|
|
26
|
+
* a consumer cannot reach the declaration through the graph, and a later call reports the declared
|
|
27
|
+
* value again. Primitives and library objects are reported as they are.
|
|
28
|
+
*/
|
|
29
|
+
function snapshot(value) {
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
return Object.freeze(value.map((entry) => snapshot(entry)));
|
|
32
|
+
}
|
|
33
|
+
if (isPlainObject(value)) {
|
|
34
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshot(entry)])));
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
/** A declared default is wrapped, so `default: undefined` reads apart from no default at all. */
|
|
39
|
+
function declaredDefault(config) {
|
|
40
|
+
return 'default' in config ? Object.freeze({ value: snapshot(config.default) }) : undefined;
|
|
41
|
+
}
|
|
42
|
+
function optionNode(input, table) {
|
|
43
|
+
const { config, name } = input;
|
|
44
|
+
const { long, negative, short } = spellingsOf(table, name);
|
|
45
|
+
const node = config.type === 'boolean'
|
|
46
|
+
? { long, name, negative, polarity: config.polarity ?? 'positive', short, type: 'boolean' }
|
|
47
|
+
: {
|
|
48
|
+
default: declaredDefault(config),
|
|
49
|
+
long,
|
|
50
|
+
// The parser reads the same test, so a collection reports as one here and there.
|
|
51
|
+
multiple: config.multiple === true,
|
|
52
|
+
name,
|
|
53
|
+
required: config.required === true,
|
|
54
|
+
short,
|
|
55
|
+
type: 'string',
|
|
56
|
+
validateOmitted: validatesOmission(input),
|
|
57
|
+
validated: config.validate !== undefined,
|
|
58
|
+
};
|
|
59
|
+
return Object.freeze(node);
|
|
60
|
+
}
|
|
61
|
+
/** The built slots already answer presence and arity, so the node repeats no config reading. */
|
|
62
|
+
function argumentNode(slot) {
|
|
63
|
+
const { config, name } = slot.input;
|
|
64
|
+
const node = {
|
|
65
|
+
default: declaredDefault(config),
|
|
66
|
+
name,
|
|
67
|
+
required: slot.required,
|
|
68
|
+
validateOmitted: validatesOmission(slot.input),
|
|
69
|
+
validated: config.validate !== undefined,
|
|
70
|
+
variadic: slot.variadic,
|
|
71
|
+
};
|
|
72
|
+
return Object.freeze(node);
|
|
73
|
+
}
|
|
74
|
+
function optionNodes(inputs, table) {
|
|
75
|
+
return Object.freeze(inputs.filter((input) => input.kind === 'option').map((input) => optionNode(input, table)));
|
|
76
|
+
}
|
|
77
|
+
function commandNode(command, path) {
|
|
78
|
+
const node = {
|
|
79
|
+
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])))),
|
|
82
|
+
hasAction: command.dispatch !== undefined,
|
|
83
|
+
name: command.name,
|
|
84
|
+
options: optionNodes(command.inputs, command.options),
|
|
85
|
+
path,
|
|
86
|
+
};
|
|
87
|
+
return Object.freeze(node);
|
|
88
|
+
}
|
|
89
|
+
/** Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. */
|
|
90
|
+
function inspectGraph(name, graph) {
|
|
91
|
+
const inspected = {
|
|
92
|
+
globals: optionNodes(graph.globals.inputs, graph.globals.options),
|
|
93
|
+
name,
|
|
94
|
+
root: commandNode(graph.root, Object.freeze([])),
|
|
95
|
+
};
|
|
96
|
+
return Object.freeze(inspected);
|
|
97
|
+
}
|
|
98
|
+
export { inspectGraph };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { OptionConfig } from './types.js';
|
|
2
|
+
export interface OptionDeclaration {
|
|
3
|
+
name: string;
|
|
4
|
+
config: OptionConfig;
|
|
5
|
+
}
|
|
6
|
+
export interface OptionValues {
|
|
7
|
+
strings: Map<string, string>;
|
|
8
|
+
lists: Map<string, string[]>;
|
|
9
|
+
booleans: Map<string, boolean>;
|
|
10
|
+
}
|
|
11
|
+
/** Which accepted form a table entry is. The table owns the convention, so readers never re-derive it. */
|
|
12
|
+
type SpellingRole = 'long' | 'negative' | 'short';
|
|
13
|
+
type OptionForm = {
|
|
14
|
+
type: 'string';
|
|
15
|
+
name: string;
|
|
16
|
+
multiple: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'boolean';
|
|
19
|
+
name: string;
|
|
20
|
+
value: boolean;
|
|
21
|
+
};
|
|
22
|
+
type OptionSpelling = OptionForm & {
|
|
23
|
+
role: SpellingRole;
|
|
24
|
+
};
|
|
25
|
+
export declare function compileOptions(declarations: readonly OptionDeclaration[], subject: string): Map<string, OptionSpelling>;
|
|
26
|
+
/** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
|
|
27
|
+
export declare function extractGlobals(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
|
|
28
|
+
rest: string[];
|
|
29
|
+
values: OptionValues;
|
|
30
|
+
};
|
|
31
|
+
/** Global and local keys never overlap, so one merged view feeds a single validation pass. */
|
|
32
|
+
export declare function mergeValues(globals: OptionValues, locals: OptionValues): OptionValues;
|
|
33
|
+
export declare function parseInputs(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
|
|
34
|
+
options: OptionValues;
|
|
35
|
+
passthrough: string[];
|
|
36
|
+
positionals: string[];
|
|
37
|
+
};
|
|
38
|
+
export {};
|