gunshi 0.0.0 → 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/README.md +59 -5
- package/lib/cli.d.ts +9 -0
- package/lib/constants.d.ts +14 -0
- package/lib/context.d.ts +14 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +723 -2
- package/lib/renderer.d.ts +5 -0
- package/lib/types.d.ts +302 -0
- package/lib/utils.d.ts +6 -0
- package/locales/en-US.json +11 -0
- package/locales/ja-JP.json +11 -0
- package/package.json +32 -13
package/README.md
CHANGED
|
@@ -1,13 +1,67 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img width="
|
|
2
|
+
<img width="196" src="./assets/logo.webp">
|
|
3
3
|
</p>
|
|
4
|
-
<h1 align="center">🏯
|
|
4
|
+
<h1 align="center">🏯 Gunshi</h1>
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
[![Version][npm-version-src]][npm-version-href]
|
|
7
|
+
[![CI][ci-src]][ci-href]
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
<!--
|
|
10
|
+
[![JSR][jsr-src]][jsr-href]
|
|
11
|
+
[![InstallSize][install-size-src]][install-size-src]
|
|
12
|
+
-->
|
|
13
|
+
|
|
14
|
+
Gunshi is a modern javascript command-line library
|
|
15
|
+
|
|
16
|
+
> [!TIP]
|
|
17
|
+
> gunshi (軍師) is a position in ancient Japanese samurai battle in which a amurai devised strategies and gave orders. That name is inspired by the word "command.”
|
|
18
|
+
|
|
19
|
+
## ✨ Features
|
|
20
|
+
|
|
21
|
+
Gunshi is designed to simplify the creation of modern command-line interfaces:
|
|
22
|
+
|
|
23
|
+
- 📏 **Minimal**: Run the commands with a minimum API.
|
|
24
|
+
- 🛡️ **Type Safe**: Arguments parsing and options value resolution type-safely by [args-tokens](https://github.com/kazupon/args-tokens)
|
|
25
|
+
- ⚙️ **Declarative configuration**: Configure the command modules declaratively.
|
|
26
|
+
- 🧩 **Composable**: Sub-commands that can be composed with modularized commands.
|
|
27
|
+
- ⏳ **Lazy & Async**: Command modules lazy loading and asynchronously executing.
|
|
28
|
+
- 📜 **Auto usage generation**: Automatic usage message generation with modularized commands.
|
|
29
|
+
- 🎨 **Custom usage generation**: Usage message generation customizable.
|
|
30
|
+
- 🌍 **Internationalization**: I18n out of the box and locale resource lazy loading.
|
|
31
|
+
|
|
32
|
+
## 💿 Installation
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
# npm
|
|
36
|
+
npm install --save gunshi
|
|
37
|
+
|
|
38
|
+
## pnpm
|
|
39
|
+
pnpm add gunshi
|
|
40
|
+
|
|
41
|
+
## yarn
|
|
42
|
+
yarn add gunshi
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 🙌 Contributing guidelines
|
|
47
|
+
|
|
48
|
+
If you are interested in contributing to `gunshi`, I highly recommend checking out [the contributing guidelines](/CONTRIBUTING.md) here. You'll find all the relevant information such as [how to make a PR](/CONTRIBUTING.md#pull-request-guidelines), [how to setup development](/CONTRIBUTING.md#development-setup)) etc., there.
|
|
10
49
|
|
|
11
50
|
## ©️ License
|
|
12
51
|
|
|
13
52
|
[MIT](http://opensource.org/licenses/MIT)
|
|
53
|
+
|
|
54
|
+
<!-- Badges -->
|
|
55
|
+
|
|
56
|
+
[npm-version-src]: https://img.shields.io/npm/v/gunshi?style=flat
|
|
57
|
+
[npm-version-href]: https://npmjs.com/package/gunshi
|
|
58
|
+
[jsr-src]: https://jsr.io/badges/@kazupon/gunishi
|
|
59
|
+
[jsr-href]: https://jsr.io/@kazupon/gunshi
|
|
60
|
+
|
|
61
|
+
<!--
|
|
62
|
+
[install-size-src]: https://pkg-size.dev/badge/install/35082
|
|
63
|
+
[install-size-href]: https://pkg-size.dev/gunishi
|
|
64
|
+
-->
|
|
65
|
+
|
|
66
|
+
[ci-src]: https://github.com/kazupon/gunshi/actions/workflows/ci.yml/badge.svg
|
|
67
|
+
[ci-href]: https://github.com/kazupon/gunshi/actions/workflows/ci.yml
|
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ArgOptions } from "args-tokens";
|
|
2
|
+
import type { Command, CommandOptions, CommandRunner } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Run the command
|
|
5
|
+
* @param args - command line arguments
|
|
6
|
+
* @param entry - a {@link Command | entry command} or an {@link CommandRunner | inline command runner}
|
|
7
|
+
* @param opts - a {@link CommandOptions | command options}
|
|
8
|
+
*/
|
|
9
|
+
export declare function cli<Options extends ArgOptions>(args: string[], entry: Command<Options> | CommandRunner<Options>, opts?: CommandOptions<Options>): Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ArgOptions } from "args-tokens";
|
|
2
|
+
import type { CommandOptions } from "./types.js";
|
|
3
|
+
export declare const COMMON_OPTIONS: {
|
|
4
|
+
readonly help: {
|
|
5
|
+
readonly type: "boolean"
|
|
6
|
+
readonly short: "h"
|
|
7
|
+
}
|
|
8
|
+
readonly version: {
|
|
9
|
+
readonly type: "boolean"
|
|
10
|
+
readonly short: "v"
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export declare const COMMAND_OPTIONS_DEFAULT: CommandOptions<ArgOptions>;
|
|
14
|
+
export declare const COMMAND_I18N_RESOURCE_KEYS: readonly ["USAGE", "COMMAND", "SUBCOMMAND", "COMMANDS", "OPTIONS", "EXAMPLES", "FORMORE"];
|
package/lib/context.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ArgOptions, ArgValues } from "args-tokens";
|
|
2
|
+
import type { Command, CommandContext, CommandOptions } from "./types.js";
|
|
3
|
+
export declare const DEFAULT_LOCALE = "en-US";
|
|
4
|
+
export declare function createCommandContext<
|
|
5
|
+
Options extends ArgOptions,
|
|
6
|
+
Values = ArgValues<Options>
|
|
7
|
+
>({ options, values, positionals, command, commandOptions, omitted }: {
|
|
8
|
+
options: Options | undefined
|
|
9
|
+
values: Values
|
|
10
|
+
positionals: string[]
|
|
11
|
+
omitted: boolean
|
|
12
|
+
command: Command<Options>
|
|
13
|
+
commandOptions: CommandOptions<Options>
|
|
14
|
+
}): Promise<Readonly<CommandContext<Options, Values>>>;
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,2 +1,723 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
|
|
2
|
+
//#region node_modules/.pnpm/args-tokens@0.10.2/node_modules/args-tokens/lib/parser.js
|
|
3
|
+
const HYPHEN_CHAR = "-";
|
|
4
|
+
const HYPHEN_CODE = HYPHEN_CHAR.codePointAt(0);
|
|
5
|
+
const EQUAL_CHAR = "=";
|
|
6
|
+
const EQUAL_CODE = EQUAL_CHAR.codePointAt(0);
|
|
7
|
+
const TERMINATOR = "--";
|
|
8
|
+
const SHORT_OPTION_PREFIX = HYPHEN_CHAR;
|
|
9
|
+
const LONG_OPTION_PREFIX = "--";
|
|
10
|
+
function parseArgs(args, options = {}) {
|
|
11
|
+
const { allowCompatible = false } = options;
|
|
12
|
+
const tokens = [];
|
|
13
|
+
const remainings = [...args];
|
|
14
|
+
let index = -1;
|
|
15
|
+
let groupCount = 0;
|
|
16
|
+
let hasShortValueSeparator = false;
|
|
17
|
+
while (remainings.length > 0) {
|
|
18
|
+
const arg = remainings.shift();
|
|
19
|
+
if (arg == undefined) break;
|
|
20
|
+
const nextArg = remainings[0];
|
|
21
|
+
if (groupCount > 0) groupCount--;
|
|
22
|
+
else index++;
|
|
23
|
+
if (arg === TERMINATOR) {
|
|
24
|
+
tokens.push({
|
|
25
|
+
kind: "option-terminator",
|
|
26
|
+
index
|
|
27
|
+
});
|
|
28
|
+
const mapped = remainings.map((arg$1) => {
|
|
29
|
+
return {
|
|
30
|
+
kind: "positional",
|
|
31
|
+
index: ++index,
|
|
32
|
+
value: arg$1
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
tokens.push(...mapped);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
if (isShortOption(arg)) {
|
|
39
|
+
const shortOption = arg.charAt(1);
|
|
40
|
+
let value;
|
|
41
|
+
let inlineValue;
|
|
42
|
+
if (groupCount) {
|
|
43
|
+
tokens.push({
|
|
44
|
+
kind: "option",
|
|
45
|
+
name: shortOption,
|
|
46
|
+
rawName: arg,
|
|
47
|
+
index,
|
|
48
|
+
value,
|
|
49
|
+
inlineValue
|
|
50
|
+
});
|
|
51
|
+
if (groupCount === 1 && hasOptionValue(nextArg)) {
|
|
52
|
+
value = remainings.shift();
|
|
53
|
+
if (hasShortValueSeparator) {
|
|
54
|
+
inlineValue = true;
|
|
55
|
+
hasShortValueSeparator = false;
|
|
56
|
+
}
|
|
57
|
+
tokens.push({
|
|
58
|
+
kind: "option",
|
|
59
|
+
index,
|
|
60
|
+
value,
|
|
61
|
+
inlineValue
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} else tokens.push({
|
|
65
|
+
kind: "option",
|
|
66
|
+
name: shortOption,
|
|
67
|
+
rawName: arg,
|
|
68
|
+
index,
|
|
69
|
+
value,
|
|
70
|
+
inlineValue
|
|
71
|
+
});
|
|
72
|
+
if (value != null) ++index;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (isShortOptionGroup(arg)) {
|
|
76
|
+
const expanded = [];
|
|
77
|
+
let shortValue = "";
|
|
78
|
+
for (let i = 1; i < arg.length; i++) {
|
|
79
|
+
const shortableOption = arg.charAt(i);
|
|
80
|
+
if (hasShortValueSeparator) shortValue += shortableOption;
|
|
81
|
+
else if (!allowCompatible && shortableOption.codePointAt(0) === EQUAL_CODE) hasShortValueSeparator = true;
|
|
82
|
+
else expanded.push(`${SHORT_OPTION_PREFIX}${shortableOption}`);
|
|
83
|
+
}
|
|
84
|
+
if (shortValue) expanded.push(shortValue);
|
|
85
|
+
remainings.unshift(...expanded);
|
|
86
|
+
groupCount = expanded.length;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (isLongOption(arg)) {
|
|
90
|
+
const longOption = arg.slice(2);
|
|
91
|
+
tokens.push({
|
|
92
|
+
kind: "option",
|
|
93
|
+
name: longOption,
|
|
94
|
+
rawName: arg,
|
|
95
|
+
index,
|
|
96
|
+
value: undefined,
|
|
97
|
+
inlineValue: undefined
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (isLongOptionAndValue(arg)) {
|
|
102
|
+
const equalIndex = arg.indexOf(EQUAL_CHAR);
|
|
103
|
+
const longOption = arg.slice(2, equalIndex);
|
|
104
|
+
const value = arg.slice(equalIndex + 1);
|
|
105
|
+
tokens.push({
|
|
106
|
+
kind: "option",
|
|
107
|
+
name: longOption,
|
|
108
|
+
rawName: `${LONG_OPTION_PREFIX}${longOption}`,
|
|
109
|
+
index,
|
|
110
|
+
value,
|
|
111
|
+
inlineValue: true
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
tokens.push({
|
|
116
|
+
kind: "positional",
|
|
117
|
+
index,
|
|
118
|
+
value: arg
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return tokens;
|
|
122
|
+
}
|
|
123
|
+
function isShortOption(arg) {
|
|
124
|
+
return arg.length === 2 && arg.codePointAt(0) === HYPHEN_CODE && arg.codePointAt(1) !== HYPHEN_CODE;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if `arg` is a short option group (e.g. `-abc`)
|
|
128
|
+
* @param arg the argument to check
|
|
129
|
+
* @returns whether `arg` is a short option group
|
|
130
|
+
*/
|
|
131
|
+
function isShortOptionGroup(arg) {
|
|
132
|
+
if (arg.length <= 2) return false;
|
|
133
|
+
if (arg.codePointAt(0) !== HYPHEN_CODE) return false;
|
|
134
|
+
if (arg.codePointAt(1) === HYPHEN_CODE) return false;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check if `arg` is a long option (e.g. `--foo`)
|
|
139
|
+
* @param arg the argument to check
|
|
140
|
+
* @returns whether `arg` is a long option
|
|
141
|
+
*/
|
|
142
|
+
function isLongOption(arg) {
|
|
143
|
+
return hasLongOptionPrefix(arg) && !arg.includes(EQUAL_CHAR, 3);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if `arg` is a long option with value (e.g. `--foo=bar`)
|
|
147
|
+
* @param arg the argument to check
|
|
148
|
+
* @returns whether `arg` is a long option
|
|
149
|
+
*/
|
|
150
|
+
function isLongOptionAndValue(arg) {
|
|
151
|
+
return hasLongOptionPrefix(arg) && arg.includes(EQUAL_CHAR, 3);
|
|
152
|
+
}
|
|
153
|
+
function hasLongOptionPrefix(arg) {
|
|
154
|
+
return arg.length > 2 && ~arg.indexOf(LONG_OPTION_PREFIX);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if a `value` is an option value
|
|
158
|
+
* @param value a value to check
|
|
159
|
+
* @returns whether a `value` is an option value
|
|
160
|
+
*/
|
|
161
|
+
function hasOptionValue(value) {
|
|
162
|
+
return !(value == null) && value.codePointAt(0) !== HYPHEN_CODE;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region node_modules/.pnpm/args-tokens@0.10.2/node_modules/args-tokens/lib/resolver.js
|
|
167
|
+
function resolveArgs(options, tokens) {
|
|
168
|
+
const positionals = [];
|
|
169
|
+
const longOptionTokens = [];
|
|
170
|
+
const shortOptionTokens = [];
|
|
171
|
+
let currentLongOption;
|
|
172
|
+
let currentShortOption;
|
|
173
|
+
const expandableShortOptions = [];
|
|
174
|
+
function toShortValue() {
|
|
175
|
+
if (expandableShortOptions.length === 0) return undefined;
|
|
176
|
+
else {
|
|
177
|
+
const value = expandableShortOptions.map((token) => token.name).join("");
|
|
178
|
+
expandableShortOptions.length = 0;
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function applyLongOptionValue(value = undefined) {
|
|
183
|
+
if (currentLongOption) {
|
|
184
|
+
currentLongOption.value = value;
|
|
185
|
+
longOptionTokens.push({ ...currentLongOption });
|
|
186
|
+
currentLongOption = undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function applyShortOptionValue(value = undefined) {
|
|
190
|
+
if (currentShortOption) {
|
|
191
|
+
currentShortOption.value = value || toShortValue();
|
|
192
|
+
shortOptionTokens.push({ ...currentShortOption });
|
|
193
|
+
currentShortOption = undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* analyze phase to resolve value
|
|
198
|
+
* separate tokens into positionals, long and short options, after that resolve values
|
|
199
|
+
*/
|
|
200
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
201
|
+
const token = tokens[i];
|
|
202
|
+
if (token.kind === "positional") {
|
|
203
|
+
positionals.push(token.value);
|
|
204
|
+
applyLongOptionValue(token.value);
|
|
205
|
+
applyShortOptionValue(token.value);
|
|
206
|
+
} else if (token.kind === "option") if (token.rawName) {
|
|
207
|
+
if (hasLongOptionPrefix(token.rawName)) {
|
|
208
|
+
if (token.inlineValue) longOptionTokens.push({ ...token });
|
|
209
|
+
else currentLongOption = { ...token };
|
|
210
|
+
applyShortOptionValue();
|
|
211
|
+
} else if (isShortOption(token.rawName)) if (currentShortOption) {
|
|
212
|
+
if (currentShortOption.index === token.index) expandableShortOptions.push({ ...token });
|
|
213
|
+
else {
|
|
214
|
+
currentShortOption.value = toShortValue();
|
|
215
|
+
shortOptionTokens.push({ ...currentShortOption });
|
|
216
|
+
currentShortOption = { ...token };
|
|
217
|
+
}
|
|
218
|
+
applyLongOptionValue();
|
|
219
|
+
} else {
|
|
220
|
+
currentShortOption = { ...token };
|
|
221
|
+
applyLongOptionValue();
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
if (currentShortOption && currentShortOption.index == token.index && token.inlineValue) {
|
|
225
|
+
currentShortOption.value = token.value;
|
|
226
|
+
shortOptionTokens.push({ ...currentShortOption });
|
|
227
|
+
currentShortOption = undefined;
|
|
228
|
+
}
|
|
229
|
+
applyLongOptionValue();
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
applyLongOptionValue();
|
|
233
|
+
applyShortOptionValue();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* check if the last long or short option is not resolved
|
|
238
|
+
*/
|
|
239
|
+
applyLongOptionValue();
|
|
240
|
+
applyShortOptionValue();
|
|
241
|
+
/**
|
|
242
|
+
* resolve values
|
|
243
|
+
*/
|
|
244
|
+
const values = Object.create(null);
|
|
245
|
+
const errors = [];
|
|
246
|
+
for (const [option, schema] of Object.entries(options)) {
|
|
247
|
+
if (schema.required) {
|
|
248
|
+
const found = longOptionTokens.find((token) => token.name === option) || schema.short && shortOptionTokens.find((token) => token.name === schema.short);
|
|
249
|
+
if (!found) {
|
|
250
|
+
errors.push(createRequireError(option, schema));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (let i = 0; i < longOptionTokens.length; i++) {
|
|
255
|
+
const token = longOptionTokens[i];
|
|
256
|
+
if (option === token.name && token.rawName != null && hasLongOptionPrefix(token.rawName)) {
|
|
257
|
+
const invalid = validateRequire(token, option, schema);
|
|
258
|
+
if (invalid) {
|
|
259
|
+
errors.push(invalid);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (schema.type === "boolean") token.value = undefined;
|
|
263
|
+
else {
|
|
264
|
+
const invalid$1 = validateValue(token, option, schema);
|
|
265
|
+
if (invalid$1) {
|
|
266
|
+
errors.push(invalid$1);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
values[option] = resolveOptionValue(token, schema);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (let i = 0; i < shortOptionTokens.length; i++) {
|
|
275
|
+
const token = shortOptionTokens[i];
|
|
276
|
+
if (schema.short === token.name && token.rawName != null && isShortOption(token.rawName)) {
|
|
277
|
+
const invalid = validateRequire(token, option, schema);
|
|
278
|
+
if (invalid) {
|
|
279
|
+
errors.push(invalid);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (schema.type === "boolean") token.value = undefined;
|
|
283
|
+
else {
|
|
284
|
+
const invalid$1 = validateValue(token, option, schema);
|
|
285
|
+
if (invalid$1) {
|
|
286
|
+
errors.push(invalid$1);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
values[option] = resolveOptionValue(token, schema);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (values[option] == null && schema.default != null) values[option] = schema.default;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
values,
|
|
298
|
+
positionals,
|
|
299
|
+
error: errors.length > 0 ? new AggregateError(errors) : undefined
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function createRequireError(option, schema) {
|
|
303
|
+
return new Error(`Option '--${option}' ${schema.short ? `or '-${schema.short}' ` : ""}is required`);
|
|
304
|
+
}
|
|
305
|
+
function validateRequire(token, option, schema) {
|
|
306
|
+
if (schema.required && schema.type !== "boolean" && !token.value) return createRequireError(option, schema);
|
|
307
|
+
}
|
|
308
|
+
function validateValue(token, option, schema) {
|
|
309
|
+
switch (schema.type) {
|
|
310
|
+
case "number": {
|
|
311
|
+
if (!isNumeric(token.value)) return createTypeError(option, schema);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "string": {
|
|
315
|
+
if (typeof token.value !== "string") return createTypeError(option, schema);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function isNumeric(str) {
|
|
321
|
+
return str.trim() !== "" && !isNaN(str);
|
|
322
|
+
}
|
|
323
|
+
function createTypeError(option, schema) {
|
|
324
|
+
return new TypeError(`Option '--${option}' ${schema.short ? `or '-${schema.short}' ` : ""}should be '${schema.type}'`);
|
|
325
|
+
}
|
|
326
|
+
function resolveOptionValue(token, schema) {
|
|
327
|
+
if (token.value) return schema.type === "number" ? +token.value : token.value;
|
|
328
|
+
if (schema.type === "boolean") return true;
|
|
329
|
+
return schema.type === "number" ? +(schema.default || "") : schema.default;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/constants.ts
|
|
334
|
+
const COMMON_OPTIONS = {
|
|
335
|
+
help: {
|
|
336
|
+
type: "boolean",
|
|
337
|
+
short: "h"
|
|
338
|
+
},
|
|
339
|
+
version: {
|
|
340
|
+
type: "boolean",
|
|
341
|
+
short: "v"
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const COMMAND_OPTIONS_DEFAULT = {
|
|
345
|
+
name: undefined,
|
|
346
|
+
description: undefined,
|
|
347
|
+
version: undefined,
|
|
348
|
+
cwd: undefined,
|
|
349
|
+
subCommands: undefined,
|
|
350
|
+
leftMargin: 2,
|
|
351
|
+
middleMargin: 10,
|
|
352
|
+
usageOptionType: false,
|
|
353
|
+
renderHeader: undefined,
|
|
354
|
+
renderUsage: undefined,
|
|
355
|
+
renderValidationErrors: undefined
|
|
356
|
+
};
|
|
357
|
+
const COMMAND_I18N_RESOURCE_KEYS = [
|
|
358
|
+
"USAGE",
|
|
359
|
+
"COMMAND",
|
|
360
|
+
"SUBCOMMAND",
|
|
361
|
+
"COMMANDS",
|
|
362
|
+
"OPTIONS",
|
|
363
|
+
"EXAMPLES",
|
|
364
|
+
"FORMORE"
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region locales/en-US.json
|
|
369
|
+
var COMMAND = "COMMAND";
|
|
370
|
+
var COMMANDS = "COMMANDS";
|
|
371
|
+
var SUBCOMMAND = "SUBCOMMAND";
|
|
372
|
+
var USAGE = "USAGE";
|
|
373
|
+
var OPTIONS = "OPTIONS";
|
|
374
|
+
var EXAMPLES = "EXAMPLES";
|
|
375
|
+
var FORMORE = "For more info, run any command with the `--help` flag:";
|
|
376
|
+
var help = "Display this help message";
|
|
377
|
+
var version = "Display this version";
|
|
378
|
+
var en_US_default = {
|
|
379
|
+
COMMAND,
|
|
380
|
+
COMMANDS,
|
|
381
|
+
SUBCOMMAND,
|
|
382
|
+
USAGE,
|
|
383
|
+
OPTIONS,
|
|
384
|
+
EXAMPLES,
|
|
385
|
+
FORMORE,
|
|
386
|
+
help,
|
|
387
|
+
version
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/utils.ts
|
|
392
|
+
async function resolveLazyCommand(cmd, name, entry = false) {
|
|
393
|
+
const resolved = Object.assign(create(), typeof cmd == "function" ? await cmd() : cmd, { default: entry });
|
|
394
|
+
if (resolved.name == null && name) resolved.name = name;
|
|
395
|
+
return deepFreeze(resolved);
|
|
396
|
+
}
|
|
397
|
+
function create(obj = null) {
|
|
398
|
+
return Object.create(obj);
|
|
399
|
+
}
|
|
400
|
+
function log(...args) {
|
|
401
|
+
console.log(...args);
|
|
402
|
+
}
|
|
403
|
+
function deepFreeze(obj) {
|
|
404
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
405
|
+
for (const key of Object.keys(obj)) {
|
|
406
|
+
const value = obj[key];
|
|
407
|
+
if (typeof value === "object" && value !== null) deepFreeze(value);
|
|
408
|
+
}
|
|
409
|
+
return Object.freeze(obj);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/context.ts
|
|
414
|
+
const DEFAULT_LOCALE = "en-US";
|
|
415
|
+
async function createCommandContext({ options, values, positionals, command, commandOptions, omitted = false }) {
|
|
416
|
+
/**
|
|
417
|
+
* tweak the options and values
|
|
418
|
+
*/
|
|
419
|
+
const _options = options == null ? undefined : Object.entries(options).reduce((acc, [key, value]) => {
|
|
420
|
+
acc[key] = Object.assign(create(), value);
|
|
421
|
+
return acc;
|
|
422
|
+
}, create());
|
|
423
|
+
const _values = Object.assign(create(), values);
|
|
424
|
+
/**
|
|
425
|
+
* normalize the usage
|
|
426
|
+
*/
|
|
427
|
+
const usage = Object.assign(create(), command.usage);
|
|
428
|
+
const { help: help$1, version: version$1 } = en_US_default;
|
|
429
|
+
usage.options = Object.assign(create(), usage.options, {
|
|
430
|
+
help: help$1,
|
|
431
|
+
version: version$1
|
|
432
|
+
});
|
|
433
|
+
/**
|
|
434
|
+
* setup the environment
|
|
435
|
+
*/
|
|
436
|
+
const env = Object.assign(create(), COMMAND_OPTIONS_DEFAULT, commandOptions);
|
|
437
|
+
const locale = resolveLocale(commandOptions.locale);
|
|
438
|
+
const localeResources = new Map();
|
|
439
|
+
const commandResources = new Map();
|
|
440
|
+
let builtInLoadedResources;
|
|
441
|
+
/**
|
|
442
|
+
* load the built-in locale resources
|
|
443
|
+
*/
|
|
444
|
+
localeResources.set(DEFAULT_LOCALE, en_US_default);
|
|
445
|
+
if (DEFAULT_LOCALE !== locale.toString()) try {
|
|
446
|
+
builtInLoadedResources = await import(`../locales/${locale.toString()}.json`, { with: { type: "json" } });
|
|
447
|
+
localeResources.set(locale.toString(), builtInLoadedResources);
|
|
448
|
+
} catch {}
|
|
449
|
+
/**
|
|
450
|
+
* define the translation function
|
|
451
|
+
*/
|
|
452
|
+
function translation(key) {
|
|
453
|
+
if (COMMAND_I18N_RESOURCE_KEYS.includes(key)) {
|
|
454
|
+
const resource = localeResources.get(locale.toString()) || localeResources.get(DEFAULT_LOCALE);
|
|
455
|
+
return resource[key] || key;
|
|
456
|
+
} else {
|
|
457
|
+
const resource = commandResources.get(locale.toString()) || commandResources.get(DEFAULT_LOCALE);
|
|
458
|
+
return resource[key] || "";
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* load the sub commands
|
|
463
|
+
*/
|
|
464
|
+
let cachedCommands;
|
|
465
|
+
async function loadCommands() {
|
|
466
|
+
if (cachedCommands) return cachedCommands;
|
|
467
|
+
const subCommands = [...env.subCommands || []];
|
|
468
|
+
return cachedCommands = await Promise.all(subCommands.map(async ([name, cmd]) => await resolveLazyCommand(cmd, name)));
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* create the context
|
|
472
|
+
*/
|
|
473
|
+
const ctx = deepFreeze(Object.assign(create(), {
|
|
474
|
+
name: command.name,
|
|
475
|
+
description: command.description,
|
|
476
|
+
omitted,
|
|
477
|
+
locale,
|
|
478
|
+
env,
|
|
479
|
+
options: _options,
|
|
480
|
+
values: _values,
|
|
481
|
+
positionals,
|
|
482
|
+
usage,
|
|
483
|
+
loadCommands,
|
|
484
|
+
translation
|
|
485
|
+
}));
|
|
486
|
+
/**
|
|
487
|
+
* load the command resources
|
|
488
|
+
*/
|
|
489
|
+
const loadedOptionsResources = Object.entries(usage.options || create()).map(([key, _]) => {
|
|
490
|
+
const option = usage.options[key];
|
|
491
|
+
return [key, option];
|
|
492
|
+
});
|
|
493
|
+
const defaultCommandResource = loadedOptionsResources.reduce((res, [key, value]) => {
|
|
494
|
+
res[key] = value;
|
|
495
|
+
return res;
|
|
496
|
+
}, create());
|
|
497
|
+
defaultCommandResource.description = command.description || "";
|
|
498
|
+
defaultCommandResource.examples = usage.examples || "";
|
|
499
|
+
commandResources.set(DEFAULT_LOCALE, defaultCommandResource);
|
|
500
|
+
const originalResource = await loadCommandResource(ctx, command);
|
|
501
|
+
if (originalResource) {
|
|
502
|
+
const resource = Object.entries(originalResource.options).reduce((res, [key, value]) => {
|
|
503
|
+
res[key] = value;
|
|
504
|
+
return res;
|
|
505
|
+
}, Object.assign(create(), {
|
|
506
|
+
description: originalResource.description,
|
|
507
|
+
examples: originalResource.examples
|
|
508
|
+
}));
|
|
509
|
+
if (builtInLoadedResources) {
|
|
510
|
+
resource.help = builtInLoadedResources.help;
|
|
511
|
+
resource.version = builtInLoadedResources.version;
|
|
512
|
+
}
|
|
513
|
+
commandResources.set(locale.toString(), resource);
|
|
514
|
+
}
|
|
515
|
+
return ctx;
|
|
516
|
+
}
|
|
517
|
+
function resolveLocale(locale) {
|
|
518
|
+
return locale instanceof Intl.Locale ? locale : typeof locale === "string" ? new Intl.Locale(locale) : new Intl.Locale(DEFAULT_LOCALE);
|
|
519
|
+
}
|
|
520
|
+
async function loadCommandResource(ctx, command) {
|
|
521
|
+
let resource;
|
|
522
|
+
try {
|
|
523
|
+
resource = await command.resource?.(ctx);
|
|
524
|
+
} catch {}
|
|
525
|
+
return resource;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/renderer.ts
|
|
530
|
+
function renderHeader(ctx) {
|
|
531
|
+
const title = ctx.env.description || ctx.env.name || "";
|
|
532
|
+
return Promise.resolve(title ? `${title} (${ctx.env.name || ""}${ctx.env.version ? ` v${ctx.env.version}` : ""})` : title);
|
|
533
|
+
}
|
|
534
|
+
async function renderUsage(ctx) {
|
|
535
|
+
const messages = [];
|
|
536
|
+
if (!ctx.omitted && hasDescription(ctx)) messages.push(ctx.description, "");
|
|
537
|
+
messages.push(...await renderUsageSection(ctx), "");
|
|
538
|
+
if (ctx.omitted && await hasCommands(ctx)) messages.push(...await renderCommandsSection(ctx), "");
|
|
539
|
+
if (hasOptions(ctx)) messages.push(...await renderOptionsSection(ctx), "");
|
|
540
|
+
if (hasExamples(ctx)) messages.push(...renderExamplesSection(ctx), "");
|
|
541
|
+
return messages.join("\n");
|
|
542
|
+
}
|
|
543
|
+
function renderValidationErrors(_ctx, error) {
|
|
544
|
+
const messages = [];
|
|
545
|
+
for (const err of error.errors) messages.push(err.message);
|
|
546
|
+
return Promise.resolve(messages.join("\n"));
|
|
547
|
+
}
|
|
548
|
+
async function renderOptionsSection(ctx) {
|
|
549
|
+
const messages = [];
|
|
550
|
+
messages.push(`${ctx.translation("OPTIONS")}:`);
|
|
551
|
+
const optionsPairs = getOptionsPairs(ctx);
|
|
552
|
+
messages.push(await generateOptionsUsage(ctx, optionsPairs));
|
|
553
|
+
return messages;
|
|
554
|
+
}
|
|
555
|
+
function renderExamplesSection(ctx) {
|
|
556
|
+
const messages = [];
|
|
557
|
+
const examples = ctx.usage.examples.split("\n").map((example) => example.padStart(ctx.env.leftMargin + example.length));
|
|
558
|
+
messages.push(`${ctx.translation("EXAMPLES")}:`, ...examples);
|
|
559
|
+
return messages;
|
|
560
|
+
}
|
|
561
|
+
async function renderUsageSection(ctx) {
|
|
562
|
+
const messages = [`${ctx.translation("USAGE")}:`];
|
|
563
|
+
if (ctx.omitted) {
|
|
564
|
+
const defaultCommand = `${resolveEntry(ctx)}${await hasCommands(ctx) ? ` [${resolveSubCommand(ctx)}]` : ""} ${hasOptions(ctx) ? `<${ctx.translation("OPTIONS")}>` : ""} `;
|
|
565
|
+
messages.push(defaultCommand.padStart(ctx.env.leftMargin + defaultCommand.length));
|
|
566
|
+
if (await hasCommands(ctx)) {
|
|
567
|
+
const commandsUsage = `${resolveEntry(ctx)} <${ctx.translation("COMMANDS")}>`;
|
|
568
|
+
messages.push(commandsUsage.padStart(ctx.env.leftMargin + commandsUsage.length));
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
const usageStr = `${resolveEntry(ctx)} ${resolveSubCommand(ctx)} ${generateOptionsSymbols(ctx)}`;
|
|
572
|
+
messages.push(usageStr.padStart(ctx.env.leftMargin + usageStr.length));
|
|
573
|
+
}
|
|
574
|
+
return messages;
|
|
575
|
+
}
|
|
576
|
+
async function renderCommandsSection(ctx) {
|
|
577
|
+
const messages = [`${ctx.translation("COMMANDS")}:`];
|
|
578
|
+
const loadedCommands = await ctx.loadCommands();
|
|
579
|
+
const commandMaxLength = Math.max(...loadedCommands.map((cmd) => (cmd.name || "").length));
|
|
580
|
+
const commandsStr = await Promise.all(loadedCommands.map((cmd) => {
|
|
581
|
+
const key = cmd.name || "";
|
|
582
|
+
const desc = cmd.description || "";
|
|
583
|
+
const command = `${key.padEnd(commandMaxLength + ctx.env.middleMargin)}${desc} `;
|
|
584
|
+
return `${command.padStart(ctx.env.leftMargin + command.length)} `;
|
|
585
|
+
}));
|
|
586
|
+
messages.push(...commandsStr, "", ctx.translation("FORMORE"));
|
|
587
|
+
messages.push(...loadedCommands.map((cmd) => {
|
|
588
|
+
const commandHelp = `${ctx.env.name} ${cmd.name} --help`;
|
|
589
|
+
return `${commandHelp.padStart(ctx.env.leftMargin + commandHelp.length)}`;
|
|
590
|
+
}));
|
|
591
|
+
return messages;
|
|
592
|
+
}
|
|
593
|
+
function resolveEntry(ctx) {
|
|
594
|
+
return ctx.env.name || ctx.translation("COMMAND");
|
|
595
|
+
}
|
|
596
|
+
function resolveSubCommand(ctx) {
|
|
597
|
+
return ctx.name || ctx.translation("SUBCOMMAND");
|
|
598
|
+
}
|
|
599
|
+
function hasDescription(ctx) {
|
|
600
|
+
return !!ctx.description;
|
|
601
|
+
}
|
|
602
|
+
async function hasCommands(ctx) {
|
|
603
|
+
const loadedCommands = await ctx.loadCommands();
|
|
604
|
+
return loadedCommands.length > 1;
|
|
605
|
+
}
|
|
606
|
+
function hasOptions(ctx) {
|
|
607
|
+
return !!(ctx.options && Object.keys(ctx.options).length > 0);
|
|
608
|
+
}
|
|
609
|
+
function hasExamples(ctx) {
|
|
610
|
+
return !!ctx.usage.examples;
|
|
611
|
+
}
|
|
612
|
+
function hasAllDefaultOptions(ctx) {
|
|
613
|
+
return !!(ctx.options && Object.values(ctx.options).every((opt) => opt.default));
|
|
614
|
+
}
|
|
615
|
+
function generateOptionsSymbols(ctx) {
|
|
616
|
+
return hasOptions(ctx) ? hasAllDefaultOptions(ctx) ? `[${ctx.translation("OPTIONS")}]` : `<${ctx.translation("OPTIONS")}>` : "";
|
|
617
|
+
}
|
|
618
|
+
function getOptionsPairs(ctx) {
|
|
619
|
+
return Object.entries(ctx.options).reduce((acc, [name, value]) => {
|
|
620
|
+
let key = `--${name}`;
|
|
621
|
+
if (value.short) key = `-${value.short}, ${key}`;
|
|
622
|
+
if (value.type !== "boolean") key = value.default ? `${key} [${name}]` : `${key} <${name}>`;
|
|
623
|
+
acc[name] = key;
|
|
624
|
+
return acc;
|
|
625
|
+
}, create());
|
|
626
|
+
}
|
|
627
|
+
async function generateOptionsUsage(ctx, optionsPairs) {
|
|
628
|
+
const optionsMaxLength = Math.max(...Object.entries(optionsPairs).map(([_, value]) => value.length));
|
|
629
|
+
const optionSchemaMaxLength = ctx.env.usageOptionType ? Math.max(...Object.entries(optionsPairs).map(([key, _]) => ctx.options[key].type.length)) : 0;
|
|
630
|
+
const usages = await Promise.all(Object.entries(optionsPairs).map(([key, value]) => {
|
|
631
|
+
const rawDesc = ctx.translation(key);
|
|
632
|
+
const optionsSchema = ctx.env.usageOptionType ? `[${ctx.options[key].type}] ` : "";
|
|
633
|
+
const desc = `${optionsSchema ? optionsSchema.padEnd(optionSchemaMaxLength + 3) : ""}${rawDesc}`;
|
|
634
|
+
const option = `${value.padEnd(optionsMaxLength + ctx.env.middleMargin)}${desc}`;
|
|
635
|
+
return `${option.padStart(ctx.env.leftMargin + option.length)}`;
|
|
636
|
+
}));
|
|
637
|
+
return usages.join("\n");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/cli.ts
|
|
642
|
+
async function cli(args, entry, opts = {}) {
|
|
643
|
+
const tokens = parseArgs(args);
|
|
644
|
+
const subCommand = getSubCommand(tokens);
|
|
645
|
+
const resolvedCommandOptions = resolveCommandOptions(opts);
|
|
646
|
+
const [name, command] = await resolveCommand(subCommand, entry, resolvedCommandOptions);
|
|
647
|
+
if (!command) throw new Error(`Command not found: ${name || ""}`);
|
|
648
|
+
if (command.name && !resolvedCommandOptions.subCommands.has(command.name)) resolvedCommandOptions.subCommands.set(command.name, command);
|
|
649
|
+
const options = resolveArgOptions(command.options);
|
|
650
|
+
const { values, positionals, error } = resolveArgs(options, tokens);
|
|
651
|
+
const omitted = !subCommand;
|
|
652
|
+
const ctx = await createCommandContext({
|
|
653
|
+
options,
|
|
654
|
+
values,
|
|
655
|
+
positionals,
|
|
656
|
+
omitted,
|
|
657
|
+
command,
|
|
658
|
+
commandOptions: opts
|
|
659
|
+
});
|
|
660
|
+
if (values.version) {
|
|
661
|
+
showVersion(ctx);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
await showHeader(ctx);
|
|
665
|
+
if (values.help) {
|
|
666
|
+
await showUsage(ctx);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (error) {
|
|
670
|
+
await showValidationErrors(ctx, error);
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
await command.run(ctx);
|
|
674
|
+
}
|
|
675
|
+
function resolveArgOptions(options) {
|
|
676
|
+
return Object.assign(create(), options, COMMON_OPTIONS);
|
|
677
|
+
}
|
|
678
|
+
function resolveCommandOptions(options) {
|
|
679
|
+
const subCommands = new Map(options.subCommands);
|
|
680
|
+
return Object.assign(create(), COMMAND_OPTIONS_DEFAULT, { subCommands }, options);
|
|
681
|
+
}
|
|
682
|
+
function getSubCommand(tokens) {
|
|
683
|
+
const firstToken = tokens[0];
|
|
684
|
+
return firstToken && firstToken.kind === "positional" && firstToken.index === 0 && firstToken.value ? firstToken.value : "";
|
|
685
|
+
}
|
|
686
|
+
async function showUsage(ctx) {
|
|
687
|
+
if (ctx.env.renderUsage === null) return;
|
|
688
|
+
const render = ctx.env.renderUsage || renderUsage;
|
|
689
|
+
log(await render(ctx));
|
|
690
|
+
}
|
|
691
|
+
function showVersion(ctx) {
|
|
692
|
+
log(ctx.env.version);
|
|
693
|
+
}
|
|
694
|
+
async function showHeader(ctx) {
|
|
695
|
+
if (ctx.env.renderHeader === null) return;
|
|
696
|
+
const header = await (ctx.env.renderHeader || renderHeader)(ctx);
|
|
697
|
+
if (header) {
|
|
698
|
+
log(header);
|
|
699
|
+
log();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function showValidationErrors(ctx, error) {
|
|
703
|
+
if (ctx.env.renderValidationErrors === null) return;
|
|
704
|
+
const render = ctx.env.renderValidationErrors || renderValidationErrors;
|
|
705
|
+
log(await render(ctx, error));
|
|
706
|
+
}
|
|
707
|
+
async function resolveCommand(sub, entry, options) {
|
|
708
|
+
const omitted = !sub;
|
|
709
|
+
if (typeof entry === "function") return [undefined, {
|
|
710
|
+
run: entry,
|
|
711
|
+
default: true
|
|
712
|
+
}];
|
|
713
|
+
else if (omitted) return typeof entry === "object" ? [entry.name, await resolveLazyCommand(entry, undefined, true)] : [undefined, undefined];
|
|
714
|
+
else {
|
|
715
|
+
if (options.subCommands == null) return [sub, undefined];
|
|
716
|
+
const cmd = options.subCommands?.get(sub);
|
|
717
|
+
if (cmd == null) return [sub, undefined];
|
|
718
|
+
return [sub, await resolveLazyCommand(cmd, sub)];
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
//#endregion
|
|
723
|
+
export { cli };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ArgOptions } from "args-tokens";
|
|
2
|
+
import type { CommandContext } from "./types.js";
|
|
3
|
+
export declare function renderHeader<Options extends ArgOptions>(ctx: Readonly<CommandContext<Options>>): Promise<string>;
|
|
4
|
+
export declare function renderUsage<Options extends ArgOptions>(ctx: Readonly<CommandContext<Options>>): Promise<string>;
|
|
5
|
+
export declare function renderValidationErrors<Options extends ArgOptions>(_ctx: CommandContext<Options>, error: AggregateError): Promise<string>;
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { ArgOptions, ArgValues } from "args-tokens";
|
|
2
|
+
/**
|
|
3
|
+
* Define a promise type that can be await from T
|
|
4
|
+
*/
|
|
5
|
+
type Awaitable<T> = T | Promise<T>;
|
|
6
|
+
/**
|
|
7
|
+
* The command i18n built-in options keys
|
|
8
|
+
* @experimental
|
|
9
|
+
*/
|
|
10
|
+
export type CommandBuiltinOptionsKeys = keyof (typeof import("./constants"))["COMMON_OPTIONS"];
|
|
11
|
+
/**
|
|
12
|
+
* The command i18n built-in resource keys
|
|
13
|
+
* @experimental
|
|
14
|
+
*/
|
|
15
|
+
export type CommandBuiltinResourceKeys = (typeof import("./constants"))["COMMAND_I18N_RESOURCE_KEYS"][number];
|
|
16
|
+
/**
|
|
17
|
+
* The command i18n built-in keys
|
|
18
|
+
* @description The command i18n built-in keys are used to {@link CommandContext.translation | translate} function
|
|
19
|
+
* @experimental
|
|
20
|
+
*/
|
|
21
|
+
export type CommandBuiltinKeys = CommandBuiltinOptionsKeys | CommandBuiltinResourceKeys | "description" | "examples";
|
|
22
|
+
/**
|
|
23
|
+
* The command environment
|
|
24
|
+
*/
|
|
25
|
+
export interface CommandEnvironment<Options extends ArgOptions = ArgOptions> {
|
|
26
|
+
/**
|
|
27
|
+
* The current working directory
|
|
28
|
+
* @see {@link CommandOptions.cwd}
|
|
29
|
+
*/
|
|
30
|
+
cwd: string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The command name
|
|
33
|
+
* @see {@link CommandOptions.name}
|
|
34
|
+
*/
|
|
35
|
+
name: string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The command description
|
|
38
|
+
* @see {@link CommandOptions.description}
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
description: string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The command version
|
|
44
|
+
* @see {@link CommandOptions.version}
|
|
45
|
+
*/
|
|
46
|
+
version: string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The left margin of the command output
|
|
49
|
+
* @default 2
|
|
50
|
+
* @see {@link CommandOptions.leftMargin}
|
|
51
|
+
*/
|
|
52
|
+
leftMargin: number;
|
|
53
|
+
/**
|
|
54
|
+
* The middle margin of the command output
|
|
55
|
+
* @default 10
|
|
56
|
+
* @see {@link CommandOptions.middleMargin}
|
|
57
|
+
*/
|
|
58
|
+
middleMargin: number;
|
|
59
|
+
/**
|
|
60
|
+
* Whether to display the usage option type
|
|
61
|
+
* @default false
|
|
62
|
+
* @see {@link CommandOptions.usageOptionType}
|
|
63
|
+
*/
|
|
64
|
+
usageOptionType: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The sub commands
|
|
67
|
+
* @see {@link CommandOptions.subCommands}
|
|
68
|
+
*/
|
|
69
|
+
subCommands: Map<string, Command<Options> | LazyCommand<Options>> | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Render function the command usage
|
|
72
|
+
*/
|
|
73
|
+
renderUsage: ((ctx: CommandContext<Options>) => Promise<string>) | null | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Render function the header section in the command usage
|
|
76
|
+
*/
|
|
77
|
+
renderHeader: ((ctx: CommandContext<Options>) => Promise<string>) | null | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Render function the validation errors
|
|
80
|
+
*/
|
|
81
|
+
renderValidationErrors: ((ctx: CommandContext<Options>, error: AggregateError) => Promise<string>) | null | undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The command options
|
|
85
|
+
*/
|
|
86
|
+
export interface CommandOptions<Options extends ArgOptions> {
|
|
87
|
+
/**
|
|
88
|
+
* The current working directory
|
|
89
|
+
* @description This is the current working directory path passed in the context of the run command. This is useful if you need your command about the current execution directory.
|
|
90
|
+
*/
|
|
91
|
+
cwd?: string;
|
|
92
|
+
/**
|
|
93
|
+
* The command name
|
|
94
|
+
* @description Please specify the name of the command that was executed. If you would specify it, gunshi will be displayed in the usage.
|
|
95
|
+
*/
|
|
96
|
+
name?: string;
|
|
97
|
+
/**
|
|
98
|
+
* The command description
|
|
99
|
+
* @description Please specify the description (summary) of the command that was executed. If you would specify it, gunshi will be displayed in the usage.
|
|
100
|
+
*
|
|
101
|
+
*/
|
|
102
|
+
description?: string;
|
|
103
|
+
/**
|
|
104
|
+
* The command version
|
|
105
|
+
* @description Please specify the version of the command that was executed. If you would specify it, gunshi will be displayed in the usage.
|
|
106
|
+
*/
|
|
107
|
+
version?: string;
|
|
108
|
+
/**
|
|
109
|
+
* The locale of the command
|
|
110
|
+
* @description The locale of the command that was executed. If you would specify it, gunshi command usage will be localized.
|
|
111
|
+
*/
|
|
112
|
+
locale?: string | Intl.Locale;
|
|
113
|
+
/**
|
|
114
|
+
* The sub commands
|
|
115
|
+
*/
|
|
116
|
+
subCommands?: Map<string, Command<Options> | LazyCommand<Options>>;
|
|
117
|
+
/**
|
|
118
|
+
* The left margin of the command output
|
|
119
|
+
*/
|
|
120
|
+
leftMargin?: number;
|
|
121
|
+
/**
|
|
122
|
+
* The middle margin of the command output
|
|
123
|
+
*/
|
|
124
|
+
middleMargin?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Whether to display the usage option type
|
|
127
|
+
*/
|
|
128
|
+
usageOptionType?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Render function the command usage
|
|
131
|
+
*/
|
|
132
|
+
renderUsage?: ((ctx: Readonly<CommandContext<Options>>) => Promise<string>) | null;
|
|
133
|
+
/**
|
|
134
|
+
* Render function the header section in the command usage
|
|
135
|
+
*/
|
|
136
|
+
renderHeader?: ((ctx: Readonly<CommandContext<Options>>) => Promise<string>) | null;
|
|
137
|
+
/**
|
|
138
|
+
* Render function the validation errors
|
|
139
|
+
*/
|
|
140
|
+
renderValidationErrors?: ((ctx: Readonly<CommandContext<Options>>, error: AggregateError) => Promise<string>) | null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The command context
|
|
144
|
+
* @description The command context is the context of the command execution
|
|
145
|
+
*/
|
|
146
|
+
export interface CommandContext<
|
|
147
|
+
Options extends ArgOptions,
|
|
148
|
+
Values = ArgValues<Options>
|
|
149
|
+
> {
|
|
150
|
+
/**
|
|
151
|
+
* The command name, that is the command that is executed
|
|
152
|
+
* @description The command name is same {@link CommandEnvironment.name}
|
|
153
|
+
*/
|
|
154
|
+
name: string | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* The command description, that is the description of the command that is executed
|
|
157
|
+
* @description The command description is same {@link CommandEnvironment.description}
|
|
158
|
+
*/
|
|
159
|
+
description: string | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* The command locale, that is the locale of the command that is executed
|
|
162
|
+
*/
|
|
163
|
+
locale: Intl.Locale;
|
|
164
|
+
/**
|
|
165
|
+
* The command environment, that is the environment of the command that is executed
|
|
166
|
+
* @description The command environment is same {@link CommandEnvironment}
|
|
167
|
+
*/
|
|
168
|
+
env: CommandEnvironment<Options>;
|
|
169
|
+
/**
|
|
170
|
+
* The command options, that is the options of the command that is executed
|
|
171
|
+
* @description The command options is same {@link Command.options}
|
|
172
|
+
*/
|
|
173
|
+
options: Options | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* The command values, that is the values of the command that is executed
|
|
176
|
+
* @description Resolve values with `resolveArgs` from command arguments and {@link Command.options}
|
|
177
|
+
*/
|
|
178
|
+
values: Values;
|
|
179
|
+
/**
|
|
180
|
+
* The command positionals, that is the positionals of the command that is executed
|
|
181
|
+
* @description Resolve positionals with `resolveArgs` from command arguments
|
|
182
|
+
*/
|
|
183
|
+
positionals: string[];
|
|
184
|
+
/**
|
|
185
|
+
* Whether the currently executing command has been executed with the sub-command name omitted
|
|
186
|
+
*/
|
|
187
|
+
omitted: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* The usage of the command
|
|
190
|
+
* @description The usage of the command is same {@link Command.usage}, and more has `--help` and `--version` options
|
|
191
|
+
*/
|
|
192
|
+
usage: CommandUsage<Options>;
|
|
193
|
+
/**
|
|
194
|
+
* Load the sub-commands
|
|
195
|
+
* @description The loaded commands are cached and returned when called again
|
|
196
|
+
* @returns loaded commands
|
|
197
|
+
*/
|
|
198
|
+
loadCommands: () => Promise<Command<Options>[]>;
|
|
199
|
+
/**
|
|
200
|
+
* The translation function
|
|
201
|
+
* @param key {CommandBuiltinKeys | T} - The key to be translated
|
|
202
|
+
* @returns The translated string, if the key is not found, the key itself is returned
|
|
203
|
+
* @experimental
|
|
204
|
+
*/
|
|
205
|
+
translation: <
|
|
206
|
+
T = CommandBuiltinKeys,
|
|
207
|
+
Key = CommandBuiltinKeys | T
|
|
208
|
+
>(key: Key) => string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The command usage render
|
|
212
|
+
* @description if the render function is async, it should return a promise
|
|
213
|
+
*/
|
|
214
|
+
export type CommandUsageRender<Options extends ArgOptions> = ((ctx: Readonly<CommandContext<Options>>) => Promise<string>) | string;
|
|
215
|
+
/**
|
|
216
|
+
* The command usage
|
|
217
|
+
*/
|
|
218
|
+
interface CommandUsage<Options extends ArgOptions> {
|
|
219
|
+
/**
|
|
220
|
+
* The options usage
|
|
221
|
+
*/
|
|
222
|
+
options?: { [Option in keyof Options] : string };
|
|
223
|
+
/**
|
|
224
|
+
* The examples usage
|
|
225
|
+
*/
|
|
226
|
+
examples?: string;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The command interface
|
|
230
|
+
*/
|
|
231
|
+
export interface Command<Options extends ArgOptions> {
|
|
232
|
+
/**
|
|
233
|
+
* The command name
|
|
234
|
+
* @description
|
|
235
|
+
* The command name is used to find command line arguments to execute from sub commands, so it's recommended to specify.
|
|
236
|
+
*/
|
|
237
|
+
name?: string;
|
|
238
|
+
/**
|
|
239
|
+
* The command description
|
|
240
|
+
* @description
|
|
241
|
+
* The command description is used to describe the command in usage, so it's recommended to specify.
|
|
242
|
+
*/
|
|
243
|
+
description?: string;
|
|
244
|
+
/**
|
|
245
|
+
* whether the command is default or not
|
|
246
|
+
* @description if the command is default, it is executed when no sub-command is specified
|
|
247
|
+
*/
|
|
248
|
+
default?: boolean;
|
|
249
|
+
/**
|
|
250
|
+
* The command options
|
|
251
|
+
*/
|
|
252
|
+
options?: Options;
|
|
253
|
+
/**
|
|
254
|
+
* The command usage
|
|
255
|
+
* @description
|
|
256
|
+
* The command usage is used to describe the command in usage, so it's recommended to specify.
|
|
257
|
+
*/
|
|
258
|
+
usage?: CommandUsage<Options>;
|
|
259
|
+
/**
|
|
260
|
+
* The command runner, that's the command to be executed
|
|
261
|
+
*/
|
|
262
|
+
run: CommandRunner<Options>;
|
|
263
|
+
/**
|
|
264
|
+
* The command resource fetcher
|
|
265
|
+
* @experimental
|
|
266
|
+
*/
|
|
267
|
+
resource?: CommandResourceFetcher<Options>;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* The command resource
|
|
271
|
+
* @experimental
|
|
272
|
+
*/
|
|
273
|
+
export interface CommandResource<Options extends ArgOptions> {
|
|
274
|
+
/**
|
|
275
|
+
* The command description resource
|
|
276
|
+
*/
|
|
277
|
+
description: string;
|
|
278
|
+
/**
|
|
279
|
+
* The options usage resources
|
|
280
|
+
*/
|
|
281
|
+
options: { [Option in keyof Options] : string };
|
|
282
|
+
/**
|
|
283
|
+
* The examples usage resources
|
|
284
|
+
*/
|
|
285
|
+
examples: string;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The command resource fetcher
|
|
289
|
+
* @experimental
|
|
290
|
+
*/
|
|
291
|
+
export type CommandResourceFetcher<Options extends ArgOptions> = (ctx: Readonly<CommandContext<Options>>) => Promise<CommandResource<Options>>;
|
|
292
|
+
/**
|
|
293
|
+
* The command runner interface
|
|
294
|
+
* @param ctx - The {@link CommandContext | command context}
|
|
295
|
+
*/
|
|
296
|
+
export type CommandRunner<Options extends ArgOptions> = (ctx: Readonly<CommandContext<Options>>) => Awaitable<void>;
|
|
297
|
+
/**
|
|
298
|
+
* The lazy command interface
|
|
299
|
+
* @description The lazy command that's not loaded until it is executed
|
|
300
|
+
*/
|
|
301
|
+
export type LazyCommand<Options extends ArgOptions> = () => Awaitable<Command<Options>>;
|
|
302
|
+
export {};
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ArgOptions } from "args-tokens";
|
|
2
|
+
import type { Command, LazyCommand } from "./types.js";
|
|
3
|
+
export declare function resolveLazyCommand<Options extends ArgOptions>(cmd: Command<Options> | LazyCommand<Options>, name: string | undefined, entry?: boolean): Promise<Command<Options>>;
|
|
4
|
+
export declare function create<T>(obj?: object | null): T;
|
|
5
|
+
export declare function log(...args: unknown[]): void;
|
|
6
|
+
export declare function deepFreeze<T extends Record<string, any>>(obj: T): Readonly<T>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"COMMAND": "COMMAND",
|
|
3
|
+
"COMMANDS": "COMMANDS",
|
|
4
|
+
"SUBCOMMAND": "SUBCOMMAND",
|
|
5
|
+
"USAGE": "USAGE",
|
|
6
|
+
"OPTIONS": "OPTIONS",
|
|
7
|
+
"EXAMPLES": "EXAMPLES",
|
|
8
|
+
"FORMORE": "For more info, run any command with the `--help` flag:",
|
|
9
|
+
"help": "Display this help message",
|
|
10
|
+
"version": "Display this version"
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gunshi",
|
|
3
|
-
"description": "Modern command-line
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Modern javascript command-line library",
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "kazuya kawaguchi",
|
|
7
7
|
"email": "kawakazu80@gmail.com"
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"type": "module",
|
|
33
33
|
"files": [
|
|
34
|
-
"lib"
|
|
34
|
+
"lib",
|
|
35
|
+
"locales"
|
|
35
36
|
],
|
|
36
37
|
"module": "lib/index.js",
|
|
37
38
|
"exports": {
|
|
@@ -41,6 +42,18 @@
|
|
|
41
42
|
"require": "./lib/index.js",
|
|
42
43
|
"default": "./lib/index.js"
|
|
43
44
|
},
|
|
45
|
+
"./context": {
|
|
46
|
+
"types": "./lib/context.d.ts",
|
|
47
|
+
"import": "./lib/context.js",
|
|
48
|
+
"require": "./lib/context.js",
|
|
49
|
+
"default": "./lib/context.js"
|
|
50
|
+
},
|
|
51
|
+
"./renderer": {
|
|
52
|
+
"types": "./lib/renderer.d.ts",
|
|
53
|
+
"import": "./lib/renderer.js",
|
|
54
|
+
"require": "./lib/renderer.js",
|
|
55
|
+
"default": "./lib/renderer.js"
|
|
56
|
+
},
|
|
44
57
|
"./package.json": "./package.json",
|
|
45
58
|
"./*": "./*"
|
|
46
59
|
},
|
|
@@ -53,28 +66,33 @@
|
|
|
53
66
|
]
|
|
54
67
|
}
|
|
55
68
|
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"args-tokens": "^0.10.2"
|
|
71
|
+
},
|
|
56
72
|
"devDependencies": {
|
|
57
73
|
"@eslint/markdown": "^6.2.2",
|
|
58
74
|
"@kazupon/eslint-config": "^0.22.0",
|
|
59
75
|
"@kazupon/prettier-config": "^0.1.1",
|
|
60
|
-
"@types/node": "^22.13.
|
|
61
|
-
"@vitest/eslint-plugin": "^1.1.
|
|
76
|
+
"@types/node": "^22.13.9",
|
|
77
|
+
"@vitest/eslint-plugin": "^1.1.36",
|
|
62
78
|
"bumpp": "^10.0.3",
|
|
63
79
|
"eslint": "^9.21.0",
|
|
64
|
-
"eslint-config-prettier": "^10.0.
|
|
80
|
+
"eslint-config-prettier": "^10.0.2",
|
|
65
81
|
"eslint-plugin-jsonc": "^2.19.1",
|
|
66
82
|
"eslint-plugin-promise": "^7.2.1",
|
|
67
83
|
"eslint-plugin-regexp": "^2.7.0",
|
|
68
84
|
"eslint-plugin-unicorn": "^57.0.0",
|
|
69
85
|
"eslint-plugin-yml": "^1.17.0",
|
|
70
86
|
"gh-changelogen": "^0.2.8",
|
|
71
|
-
"knip": "^5.
|
|
87
|
+
"knip": "^5.45.0",
|
|
72
88
|
"lint-staged": "^15.4.3",
|
|
73
|
-
"pkg-pr-new": "^0.0.
|
|
74
|
-
"prettier": "^3.5.
|
|
75
|
-
"
|
|
76
|
-
"typescript
|
|
77
|
-
"
|
|
89
|
+
"pkg-pr-new": "^0.0.40",
|
|
90
|
+
"prettier": "^3.5.3",
|
|
91
|
+
"rolldown": "1.0.0-beta.3",
|
|
92
|
+
"typescript": "^5.8.2",
|
|
93
|
+
"typescript-eslint": "^8.26.0",
|
|
94
|
+
"unplugin-isolated-decl": "^0.13.1",
|
|
95
|
+
"vitest": "^3.0.7"
|
|
78
96
|
},
|
|
79
97
|
"prettier": "@kazupon/prettier-config",
|
|
80
98
|
"lint-staged": {
|
|
@@ -91,8 +109,9 @@
|
|
|
91
109
|
]
|
|
92
110
|
},
|
|
93
111
|
"scripts": {
|
|
94
|
-
"build": "
|
|
112
|
+
"build": "rolldown -c rolldown.config.ts",
|
|
95
113
|
"changelog": "gh-changelogen --repo=kazupon/gunshi",
|
|
114
|
+
"clean": "git clean -df",
|
|
96
115
|
"dev": "pnpx @eslint/config-inspector --config eslint.config.ts",
|
|
97
116
|
"dev:eslint": "pnpx @eslint/config-inspector --config eslint.config.ts",
|
|
98
117
|
"fix": "pnpm run --parallel --color \"/^fix:/\"",
|