@forwardimpact/libcli 0.1.1 → 0.1.3
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/README.md +9 -0
- package/package.json +1 -1
- package/src/cli.js +48 -3
- package/src/help.js +80 -11
package/README.md
ADDED
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,6 +7,27 @@ export class Cli {
|
|
|
7
7
|
#helpRenderer;
|
|
8
8
|
|
|
9
9
|
constructor(definition, { process, helpRenderer }) {
|
|
10
|
+
if (definition.options) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`${definition.name}: "options" is no longer supported. ` +
|
|
13
|
+
`Use "globalOptions" for shared options and per-command "options" ` +
|
|
14
|
+
`for command-specific options.`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (definition.commands && definition.globalOptions) {
|
|
18
|
+
const globalNames = new Set(Object.keys(definition.globalOptions));
|
|
19
|
+
for (const cmd of definition.commands) {
|
|
20
|
+
if (!cmd.options) continue;
|
|
21
|
+
for (const name of Object.keys(cmd.options)) {
|
|
22
|
+
if (globalNames.has(name)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`${definition.name}: option "${name}" in command ` +
|
|
25
|
+
`"${cmd.name}" collides with a global option`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
10
31
|
this.#definition = definition;
|
|
11
32
|
this.#proc = process;
|
|
12
33
|
this.#helpRenderer = helpRenderer;
|
|
@@ -17,8 +38,14 @@ export class Cli {
|
|
|
17
38
|
}
|
|
18
39
|
|
|
19
40
|
parse(argv) {
|
|
41
|
+
const command = this.#findCommand(argv);
|
|
42
|
+
|
|
43
|
+
const globalOpts = this.#definition.globalOptions || {};
|
|
44
|
+
const commandOpts = command?.options || {};
|
|
45
|
+
const merged = { ...globalOpts, ...commandOpts };
|
|
46
|
+
|
|
20
47
|
const options = {};
|
|
21
|
-
for (const [name, opt] of Object.entries(
|
|
48
|
+
for (const [name, opt] of Object.entries(merged)) {
|
|
22
49
|
options[name] = { type: opt.type };
|
|
23
50
|
if (opt.short) options[name].short = opt.short;
|
|
24
51
|
if (opt.default !== undefined) options[name].default = opt.default;
|
|
@@ -33,9 +60,13 @@ export class Cli {
|
|
|
33
60
|
|
|
34
61
|
if (values.help) {
|
|
35
62
|
if (values.json) {
|
|
36
|
-
this.#helpRenderer.renderJson(
|
|
63
|
+
this.#helpRenderer.renderJson(
|
|
64
|
+
this.#definition,
|
|
65
|
+
this.#proc.stdout,
|
|
66
|
+
command,
|
|
67
|
+
);
|
|
37
68
|
} else {
|
|
38
|
-
this.#helpRenderer.render(this.#definition, this.#proc.stdout);
|
|
69
|
+
this.#helpRenderer.render(this.#definition, this.#proc.stdout, command);
|
|
39
70
|
}
|
|
40
71
|
return null;
|
|
41
72
|
}
|
|
@@ -48,6 +79,20 @@ export class Cli {
|
|
|
48
79
|
return { values, positionals };
|
|
49
80
|
}
|
|
50
81
|
|
|
82
|
+
#findCommand(argv) {
|
|
83
|
+
const commands = this.#definition.commands;
|
|
84
|
+
if (!commands || commands.length === 0) return null;
|
|
85
|
+
|
|
86
|
+
const positionals = argv.filter((a) => !a.startsWith("-"));
|
|
87
|
+
|
|
88
|
+
for (let len = Math.min(positionals.length, 3); len > 0; len--) {
|
|
89
|
+
const candidate = positionals.slice(0, len).join(" ");
|
|
90
|
+
const found = commands.find((c) => c.name === candidate);
|
|
91
|
+
if (found) return found;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
showHelp() {
|
|
52
97
|
this.#helpRenderer.render(this.#definition, this.#proc.stdout);
|
|
53
98
|
}
|
package/src/help.js
CHANGED
|
@@ -48,11 +48,11 @@ export class HelpRenderer {
|
|
|
48
48
|
return lines;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
#
|
|
52
|
-
if (!
|
|
53
|
-
const entries = Object.entries(
|
|
51
|
+
#renderOptionSection(options, title) {
|
|
52
|
+
if (!options) return [];
|
|
53
|
+
const entries = Object.entries(options);
|
|
54
54
|
if (entries.length === 0) return [];
|
|
55
|
-
const lines = [this.#sectionHeader(
|
|
55
|
+
const lines = [this.#sectionHeader(title)];
|
|
56
56
|
const optStrings = entries.map(([name, opt]) => {
|
|
57
57
|
let s = `--${name}`;
|
|
58
58
|
if (opt.type === "string") s += `=<${opt.type}>`;
|
|
@@ -69,30 +69,99 @@ export class HelpRenderer {
|
|
|
69
69
|
return lines;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
#
|
|
73
|
-
if (!
|
|
72
|
+
#renderExamplesArray(examples) {
|
|
73
|
+
if (!examples || examples.length === 0) return [];
|
|
74
74
|
const lines = [this.#sectionHeader("Examples:")];
|
|
75
|
-
for (const ex of
|
|
75
|
+
for (const ex of examples) {
|
|
76
76
|
lines.push(` ${ex}`);
|
|
77
77
|
}
|
|
78
78
|
lines.push("");
|
|
79
79
|
return lines;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
#renderExamples(definition) {
|
|
83
|
+
return this.#renderExamplesArray(definition.examples);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#renderHintLine(definition) {
|
|
87
|
+
if (!definition.commands || definition.commands.length === 0) return [];
|
|
88
|
+
return [
|
|
89
|
+
`Use ${definition.name} <command> --help for command-specific options.`,
|
|
90
|
+
"",
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#renderCommand(definition, stream, command) {
|
|
95
|
+
let header = `${definition.name} ${command.name}`;
|
|
96
|
+
if (command.args) header += ` ${command.args}`;
|
|
97
|
+
if (command.description) header += ` \u2014 ${command.description}`;
|
|
98
|
+
const formatted = supportsColor(this.#proc)
|
|
99
|
+
? formatHeader(header, this.#proc)
|
|
100
|
+
: header;
|
|
101
|
+
|
|
102
|
+
let usage = `Usage: ${definition.name} ${command.name}`;
|
|
103
|
+
if (command.args) usage += ` ${command.args}`;
|
|
104
|
+
usage += " [options]";
|
|
105
|
+
|
|
106
|
+
const globalWithoutVersion = definition.globalOptions
|
|
107
|
+
? Object.fromEntries(
|
|
108
|
+
Object.entries(definition.globalOptions).filter(
|
|
109
|
+
([name]) => name !== "version",
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
: null;
|
|
113
|
+
|
|
114
|
+
const lines = [
|
|
115
|
+
formatted,
|
|
116
|
+
"",
|
|
117
|
+
usage,
|
|
118
|
+
"",
|
|
119
|
+
...this.#renderOptionSection(command.options, "Options:"),
|
|
120
|
+
...this.#renderOptionSection(globalWithoutVersion, "Global options:"),
|
|
121
|
+
...this.#renderExamplesArray(command.examples),
|
|
122
|
+
];
|
|
123
|
+
stream.write(lines.join("\n"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
render(definition, stream, command) {
|
|
83
127
|
const out = stream || this.#proc.stdout;
|
|
128
|
+
if (command) {
|
|
129
|
+
this.#renderCommand(definition, out, command);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
84
132
|
const lines = [
|
|
85
133
|
...this.#renderHeader(definition),
|
|
86
134
|
...this.#renderUsage(definition),
|
|
87
135
|
...this.#renderCommands(definition),
|
|
88
|
-
...this.#
|
|
136
|
+
...this.#renderOptionSection(definition.globalOptions, "Options:"),
|
|
89
137
|
...this.#renderExamples(definition),
|
|
138
|
+
...this.#renderHintLine(definition),
|
|
90
139
|
];
|
|
91
140
|
out.write(lines.join("\n"));
|
|
92
141
|
}
|
|
93
142
|
|
|
94
|
-
renderJson(definition, stream) {
|
|
143
|
+
renderJson(definition, stream, command) {
|
|
95
144
|
const out = stream || this.#proc.stdout;
|
|
96
|
-
|
|
145
|
+
if (command) {
|
|
146
|
+
const globalWithoutVersion = definition.globalOptions
|
|
147
|
+
? Object.fromEntries(
|
|
148
|
+
Object.entries(definition.globalOptions).filter(
|
|
149
|
+
([name]) => name !== "version",
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
: undefined;
|
|
153
|
+
const obj = {
|
|
154
|
+
name: command.name,
|
|
155
|
+
args: command.args,
|
|
156
|
+
description: command.description,
|
|
157
|
+
parent: definition.name,
|
|
158
|
+
options: command.options,
|
|
159
|
+
globalOptions: globalWithoutVersion,
|
|
160
|
+
examples: command.examples,
|
|
161
|
+
};
|
|
162
|
+
out.write(JSON.stringify(obj, null, 2) + "\n");
|
|
163
|
+
} else {
|
|
164
|
+
out.write(JSON.stringify(definition, null, 2) + "\n");
|
|
165
|
+
}
|
|
97
166
|
}
|
|
98
167
|
}
|