@noresources/command 0.0.0-stage → 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.
@@ -0,0 +1,343 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file Commander.js adapter functions
5
+ */
6
+
7
+ import {
8
+ merge,
9
+ toCamelCase
10
+ } from '@noresources/core';
11
+ import {
12
+ Command,
13
+ Program
14
+ } from '../Command.mjs';
15
+ import {
16
+ OPTION_TYPE
17
+ } from '../Options.mjs';
18
+ import {
19
+ Input
20
+ } from '../Input.mjs';
21
+ import {
22
+ createPositionalArgumentResult,
23
+ POSITIONAL_TYPE
24
+ } from '../Positionals.mjs';
25
+
26
+ import * as commander from 'commander';
27
+ /**
28
+ * @description Commander.js command handler adapter.
29
+ * This function is designed to act as the final execution handler for a command.
30
+ * It gathers positional arguments, processes options, and invokes the
31
+ * registered handler function, passing a fully populated Input object.
32
+ * @function commandHandler
33
+ * @returns {number} The result of the handler invocation, typically used as the process exit code.
34
+ * @throws {Error} Throws an error if unexpected positional arguments are encountered.
35
+ */
36
+ function commandHandler() {
37
+ const input = new Input;
38
+ const array = Array.from (arguments);
39
+ const cmd = array.pop();
40
+ /* options */ array.pop();
41
+ const options = cmd.optsWithGlobals();
42
+
43
+ let command = this.context;
44
+ if (array.length) {
45
+ const positionals = command.positionals;
46
+ if (!positionals) {
47
+ /**
48
+ * @todo Better message
49
+ */
50
+ throw new Error ('Unexpected positional argument(s)');
51
+ }
52
+
53
+ const dispatched = [];
54
+ for (let i = 0; i < Math.min(positionals.length, array.length); ++i) {
55
+ const p = positionals[i];
56
+ let a = array[i];
57
+
58
+ if (p.type == POSITIONAL_TYPE.VALUE) {
59
+ a = p.parse (a);
60
+ } else if (p.type == POSITIONAL_TYPE.VARIADIC) {
61
+ if (!Array.isArray(a)) {
62
+ throw new Error ('Array expected');
63
+ }
64
+ a = a.map(v => p.parse(v));
65
+ }
66
+
67
+ dispatched.push ({
68
+ 'property': p.property,
69
+ 'values': Array.isArray(a) ? a : [a]
70
+ });
71
+ }
72
+ input.arguments = createPositionalArgumentResult(positionals, dispatched, {
73
+ 'dispatched': true
74
+ });
75
+ }
76
+
77
+ if (Array.isArray (this.commands)) {
78
+ this.commands.forEach (a => input.commands.push (a));
79
+ } else if (this.command) {
80
+ input.commands.push(this.command);
81
+ }
82
+
83
+ for (const e of Object.entries (options)) {
84
+ let [name, value] = e;
85
+ const option = this.options.get (name);
86
+ name = option.property;
87
+
88
+ if (option.type == OPTION_TYPE.VARIADIC) {
89
+ const list = [];
90
+ value.forEach (v => {
91
+ v = option.parse (v);
92
+ list.push(v);
93
+ });
94
+ value = list;
95
+ } else {
96
+ value = option.parse (value);
97
+ }
98
+
99
+ input.options[name] = value;
100
+ }
101
+
102
+ const contextArg = this.context || {};
103
+ return this.handler.apply (contextArg, [input]);
104
+ }
105
+
106
+ /**
107
+ * @description Determines the display names (short, long, alternate) for a given option
108
+ * while ensuring no internal naming conflicts exist.
109
+ * @param {Option} option The option object whose names need to be resolved.
110
+ * @param {Array<string>} excludes A list of names that should not be used due to prior conflicts.
111
+ * @returns {Array<string>} An array containing the usable, resolved names for the option.
112
+ */
113
+ export function selectOptionNames (option, excludes) {
114
+ let short;
115
+ let long;
116
+ let alt;
117
+ const names = [];
118
+
119
+ for (const n of option.names) {
120
+ if (excludes.includes(n)) {
121
+ continue;
122
+ }
123
+ if (n.length > 1) {
124
+ if (!long) {
125
+ long = n;
126
+ continue;
127
+ }
128
+
129
+ if (!alt) {
130
+ alt = n;
131
+ continue;
132
+ }
133
+
134
+ continue;
135
+
136
+ }
137
+
138
+ if (short) {
139
+ continue;
140
+ }
141
+
142
+ short = n;
143
+ }
144
+ if (short) {
145
+ names.push(short);
146
+ }
147
+ if (long) {
148
+ names.push(long);
149
+ }
150
+
151
+ if (alt && names.length < 2) {
152
+ names.push (alt);
153
+ }
154
+ return names;
155
+ }
156
+
157
+ /**
158
+ * @description Constructs the standard '--name' or '-n' declaration string for a given option,
159
+ * including parameter declarations for required or optional arguments.
160
+ * @param {Option} option The option object defining the parameter structure.
161
+ * @param {Array<string>} names A list of resolved names for the option (e.g., ['o', 'output']).
162
+ * @returns {string} The complete CLI parameter declaration line (e.g., "--output <value>" or "output [...]").
163
+ */
164
+ export function buildOptionLine(option, names) {
165
+ let line = names.map (n => {
166
+ return ((n.length == 1) ? '-' : '--') + n;
167
+ }).join(', ');
168
+
169
+ if (option.type == OPTION_TYPE.VALUE) {
170
+ line += ' ' + buildParameterDeclaration(option.parameter, {
171
+ 'required': option.require
172
+ });
173
+ } else if (option.type == OPTION_TYPE.VARIADIC) {
174
+ line += ' ' + buildParameterDeclaration(option.parameters, {
175
+ 'multiple': true,
176
+ 'required': option.required
177
+ });
178
+ }
179
+ return line;
180
+ }
181
+
182
+ /**
183
+ * @description Builds the formal parameter declaration string (e.g., 'value', '<value>', or '[value]')
184
+ * @param {Parameter} parameter The parameter definition object.
185
+ * @param {object} options The options object containing metadata about how the parameter should be rendered.
186
+ * @param {boolean} [options.multiple] - Flag indicating if the argument can accept multiple values (...).
187
+ * @returns {string} The formatted declaration string ready to be appended to the command line argument list.
188
+ */
189
+ export function buildParameterDeclaration (parameter, options) {
190
+ let declaration = options.name
191
+ || (parameter && (parameter.hint || parameter.type))
192
+ || 'value';
193
+
194
+
195
+ if (options.multiple) {
196
+ declaration += '...';
197
+ }
198
+ if (options.required) {
199
+ declaration = '<' + declaration + '>';
200
+ } else {
201
+ declaration = '[' + declaration + ']';
202
+ }
203
+
204
+ return declaration;
205
+ }
206
+
207
+ /**
208
+ * @description Creates a fully configured commander.Option object from a resource-defined Option.
209
+ * This function ensures correct naming conventions and property mapping before instantiation.
210
+ * @param {Option} option The internal Option object to convert and use.
211
+ * @param {object} context The context containing necessary state, such as existing name mappings.
212
+ * @param {Array<string>} [context.names] - A list of option names already consumed by other options during the current build phase.
213
+ * @param {Map<string, Option>} [context.properties] - A map tracking the conversion from commander property names back to the Option object.
214
+ * @returns {commander.Option} A ready-to-use commander.js Option instance, complete with arguments and defaults.
215
+ * @throws {commander.Error} Throws if the option cannot be assigned a unique name.
216
+ */
217
+ export function createOption (option, context) {
218
+ if (!context) {
219
+ context = {};
220
+ }
221
+ const excludes = Array.isArray(context.names) ? context.names : [];
222
+ const names = selectOptionNames(option, excludes);
223
+ if (names.length == 0) {
224
+ throw new commander.Error('Missing or conflicting option names');
225
+ }
226
+
227
+ const longName = names[names.length - 1];
228
+ const commanderProperty = (longName.length > 1)
229
+ ? toCamelCase(longName)
230
+ : longName;
231
+ if (!(context.properties instanceof Map)) {
232
+ context.properties = new Map;
233
+ }
234
+ context.properties.set (commanderProperty, option);
235
+
236
+ names.forEach(n => {
237
+ excludes.push (n);
238
+ });
239
+ if (!context.names) {
240
+ context.names = excludes;
241
+ }
242
+
243
+ const line = buildOptionLine(option, names);
244
+
245
+ let description = '';
246
+ if (option.documentation) {
247
+ description = option.documentation.toString();
248
+ }
249
+
250
+ const co = new commander.Option (line, description);
251
+ if (option.required === true) {
252
+ co.required = true;
253
+ }
254
+ if (typeof option.default !== 'undefined') {
255
+ co.default (option.default);
256
+ }
257
+ return co;
258
+ }
259
+
260
+ /**
261
+ * @description Creates a commander.Argument object from a resource-defined Positional argument.
262
+ * This handles both simple value arguments and variadic argument lists.
263
+ * @param {Positional} positional The internal Positional argument definition object.
264
+ * @returns {commander.Argument} A fully configured commander.js Argument instance.
265
+ */
266
+ export function createPositional (positional) {
267
+ let declaration;
268
+ if (positional.type == POSITIONAL_TYPE.VALUE) {
269
+ declaration = buildParameterDeclaration(positional.parameter, {
270
+ 'name': positional.name,
271
+ 'required': positional.required
272
+ });
273
+ } else if (positional.type == POSITIONAL_TYPE.VARIADIC) {
274
+ declaration = buildParameterDeclaration(positional.parameters, {
275
+ 'multiple': true,
276
+ 'name': positional.name,
277
+ 'required': positional.required
278
+ });
279
+ }
280
+ let description = '';
281
+ if (positional.documentation) {
282
+ description = positional.documentation.toString();
283
+ }
284
+ const a = new commander.Argument (declaration, description, positional.default);
285
+ return a;
286
+ }
287
+
288
+
289
+ /**
290
+ * @description Creates a complete commander.Command object from a resource-defined Command structure.
291
+ * This orchestrates the adding of options, positional arguments, and nested commands.
292
+ * @param {Command} command The internal Command object that defines the command's structure.
293
+ * @param {object} context The shared context required for building child commands and options.
294
+ * @returns {object} The resulting commander.js Command instance.
295
+ */
296
+ export function createCommand (command, context) {
297
+ if (!(typeof context == 'object' && context)) {
298
+ context = {};
299
+ }
300
+ const cmd = new commander.Command ();
301
+ cmd.name (command.name);
302
+ if (command instanceof Command) {
303
+ cmd.aliases (command.aliases);
304
+ } else if (command instanceof Program) {
305
+ cmd.version (command.version);
306
+ }
307
+
308
+ if (command.documentation) {
309
+ cmd.description (command.documentation.toString());
310
+ }
311
+
312
+ command.options.forEach (o => {
313
+ cmd.addOption (createOption(o, context));
314
+ });
315
+
316
+ command.positionals.forEach(a => {
317
+ cmd.addArgument( createPositional(a) );
318
+ });
319
+
320
+ command.commands.forEach (c => {
321
+ const ctx = merge ({'commands': []}, context);
322
+ ctx.commands.push (command);
323
+ cmd.addCommand( createCommand(c, ctx) );
324
+ });
325
+
326
+
327
+ const handler = command.handler;
328
+ if (typeof handler === 'function') {
329
+ const ctx = {
330
+ 'context': command,
331
+ handler,
332
+ 'options': context.properties,
333
+ 'commands': context.commands
334
+ };
335
+ cmd.action (commandHandler.bind (ctx));
336
+ }
337
+
338
+ return cmd;
339
+ }
340
+
341
+ export default {
342
+ createOption
343
+ };
@@ -0,0 +1,26 @@
1
+ import adapters, {
2
+ buildParameterDeclaration,
3
+ buildOptionLine,
4
+ createCommand,
5
+ createOption,
6
+ createPositional,
7
+ selectOptionNames
8
+ } from './adapters.mjs';
9
+ export {
10
+ buildParameterDeclaration,
11
+ buildOptionLine,
12
+ createCommand,
13
+ createOption,
14
+ createPositional,
15
+ selectOptionNames,
16
+ adapters
17
+ };
18
+ export default {
19
+ buildParameterDeclaration,
20
+ buildOptionLine,
21
+ createCommand,
22
+ createOption,
23
+ createPositional,
24
+ selectOptionNames,
25
+ adapters
26
+ };
@@ -0,0 +1 @@
1
+ {"name":"@noresources/cli/commander","type":"module","module":"./index.mjs","files":["adapters.mjs"],"exports":{".":[{"import":"./index.mjs"},"./index.mjs"]}}
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @constant {object} Verbosity - Configuration object defining the levels of logging detail.
5
+ * @description This object provides a counter-based system to control how verbose the application should be.
6
+ * It is typically used to increment or check logging levels.
7
+ * @example
8
+ * // Example usage:
9
+ * // let currentVerbosity = Verbosity.default;
10
+ * // if (currentVerbosity >= 1) { console.log("Detailed debug information."); }
11
+ * @property {string} type - The underlying type mechanism ('counter').
12
+ * @property {number} default - The starting default count, typically 0.
13
+ * @property {Array<string>} names - An array of common aliases for this setting (e.g., ['verbosity', 'v']).
14
+ * @type {object}
15
+ */
16
+ export const Verbosity = {
17
+ 'type': 'counter',
18
+ 'documentation': 'Verbosity level',
19
+ 'initial': 0,
20
+ 'names': ['verbosity', 'v']
21
+ };
22
+
23
+ /**
24
+ * @constant {object} Verbose - Configuration object defining a boolean flag for high detail logging.
25
+ * @description When this flag is active (true), the application should output maximum detail,
26
+ * including debug messages and advanced tracing information.
27
+ * @property {string} type - The underlying type mechanism ('switch').
28
+ * @property {Array<string>} names - An array of common aliases for this setting (e.g., ['verbose', 'v']).
29
+ * @type {object}
30
+ */
31
+ export const Verbose = {
32
+ 'type': 'switch',
33
+ 'documentation': 'Verbose mode',
34
+ 'names': ['verbose', 'v']
35
+ };
36
+
37
+ /**
38
+ * @constant {object} Quiet - Configuration object defining a boolean flag to suppress non-essential logging.
39
+ * @description When this flag is active (true), the application should run in minimal output mode.
40
+ * This setting is used to silence routine messages, only showing critical errors.
41
+ * @property {string} type - The underlying type mechanism ('switch').
42
+ * @property {Array<string>} names - An array of common aliases for this setting (e.g., ['quiet', 'q']).
43
+ * @type {object}
44
+ */
45
+ export const Quiet = {
46
+ 'type': 'switch',
47
+ 'documentation': 'Quiet mode',
48
+ 'names': ['quiet', 'q']
49
+ };
50
+
51
+ export default {
52
+ Verbosity,
53
+ Verbose,
54
+ Quiet
55
+ };
@@ -0,0 +1,15 @@
1
+ import {
2
+ Quiet,
3
+ Verbose,
4
+ Verbosity
5
+ } from './Verbosity.mjs';
6
+ export {
7
+ Quiet,
8
+ Verbose,
9
+ Verbosity
10
+ };
11
+ export default {
12
+ Quiet,
13
+ Verbose,
14
+ Verbosity
15
+ };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ import { Parameter, createParameter } from './Parameters.mjs';
4
+
5
+ /**
6
+ * @private
7
+ */
8
+ export function transformParameterProperty (object, property) {
9
+ if (!(typeof object === 'object' && object)) {
10
+ return;
11
+ }
12
+
13
+ if (!(property in object)) {
14
+ return false;
15
+ }
16
+
17
+ let a = object[property];
18
+ if (a instanceof Parameter) {
19
+ object[property] = a;
20
+ return true;
21
+ }
22
+
23
+ const factory = this.parameterFactory || createParameter;
24
+ let type;
25
+ let parameterOptions;
26
+
27
+ if (typeof a === 'string') {
28
+ type = a;
29
+ } else if (typeof a === 'object' && a !== null) {
30
+
31
+ type = a.type;
32
+ parameterOptions = a;
33
+ } else {
34
+
35
+ return false;
36
+ }
37
+
38
+ object[property] = factory(type, parameterOptions);
39
+ return true;
40
+ }