gunshi 0.2.2 → 0.3.1
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 +330 -9
- package/lib/{context-DmZAeiph.js → context-B5z7lnoV.js} +23 -2
- package/lib/context.js +1 -2
- package/lib/index.js +190 -3
- package/package.json +5 -5
- package/lib/renderer-Bo0DibAK.js +0 -115
- package/lib/renderer.d.ts +0 -24
- package/lib/renderer.js +0 -4
- package/lib/utils-NHs5DuHk.js +0 -24
package/README.md
CHANGED
|
@@ -5,22 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
[![Version][npm-version-src]][npm-version-href]
|
|
7
7
|
[![CI][ci-src]][ci-href]
|
|
8
|
+
[![InstallSize][install-size-src]][install-size-src]
|
|
8
9
|
|
|
9
10
|
<!--
|
|
10
11
|
[![JSR][jsr-src]][jsr-href]
|
|
11
|
-
[![InstallSize][install-size-src]][install-size-src]
|
|
12
12
|
-->
|
|
13
13
|
|
|
14
14
|
Gunshi is a modern javascript command-line library
|
|
15
15
|
|
|
16
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
|
|
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
18
|
|
|
19
19
|
## ✨ Features
|
|
20
20
|
|
|
21
21
|
Gunshi is designed to simplify the creation of modern command-line interfaces:
|
|
22
22
|
|
|
23
|
-
- 📏 **
|
|
23
|
+
- 📏 **Simple**: Run the commands with a simple API.
|
|
24
24
|
- 🛡️ **Type Safe**: Arguments parsing and options value resolution type-safely by [args-tokens](https://github.com/kazupon/args-tokens)
|
|
25
25
|
- ⚙️ **Declarative configuration**: Configure the command modules declaratively.
|
|
26
26
|
- 🧩 **Composable**: Sub-commands that can be composed with modularized commands.
|
|
@@ -40,13 +40,338 @@ pnpm add gunshi
|
|
|
40
40
|
|
|
41
41
|
## yarn
|
|
42
42
|
yarn add gunshi
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 🚀 Usage
|
|
46
|
+
|
|
47
|
+
### Simple API
|
|
48
|
+
|
|
49
|
+
Gunshi has a simple API that is a facade:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { cli } from 'gunshi'
|
|
53
|
+
|
|
54
|
+
// Run a simple command
|
|
55
|
+
cli(process.argv.slice(2), () => {
|
|
56
|
+
console.log('Hello from Gunshi!')
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Type-Safe Arguments
|
|
61
|
+
|
|
62
|
+
Gunshi provides type-safe argument parsing with TypeScript:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { cli } from 'gunshi'
|
|
66
|
+
import type { ArgOptions, Command, CommandContext } from 'gunshi'
|
|
67
|
+
|
|
68
|
+
// Define interfaces for options and values
|
|
69
|
+
interface UserOptions extends ArgOptions {
|
|
70
|
+
name: { type: 'string'; short: 'n' }
|
|
71
|
+
age: { type: 'number'; short: 'a'; default: number }
|
|
72
|
+
verbose: { type: 'boolean'; short: 'v' }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface UserValues {
|
|
76
|
+
name?: string
|
|
77
|
+
age: number
|
|
78
|
+
verbose?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create a type-safe command
|
|
82
|
+
const command: Command<UserOptions> = {
|
|
83
|
+
name: 'type-safe',
|
|
84
|
+
options: {
|
|
85
|
+
name: { type: 'string', short: 'n' },
|
|
86
|
+
age: { type: 'number', short: 'a', default: 25 },
|
|
87
|
+
verbose: { type: 'boolean', short: 'v' }
|
|
88
|
+
},
|
|
89
|
+
run: (ctx: CommandContext<UserOptions, UserValues>) => {
|
|
90
|
+
const { name, age, verbose } = ctx.values
|
|
91
|
+
console.log(`Hello, ${name || 'World'}! You are ${age} years old.`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await cli(process.argv.slice(2), command)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For more detailed examples, check out the [playground/type-safe](https://github.com/kazupon/gunshi/tree/main/playground/type-safe) in the repository.
|
|
99
|
+
|
|
100
|
+
### Declarative Configuration
|
|
43
101
|
|
|
102
|
+
Configure commands declaratively:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { cli } from 'gunshi'
|
|
106
|
+
|
|
107
|
+
// Define a command with declarative configuration
|
|
108
|
+
const command = {
|
|
109
|
+
name: 'greet',
|
|
110
|
+
description: 'A greeting command',
|
|
111
|
+
options: {
|
|
112
|
+
name: { type: 'string', short: 'n' },
|
|
113
|
+
greeting: { type: 'string', short: 'g', default: 'Hello' },
|
|
114
|
+
times: { type: 'number', short: 't', default: 1 }
|
|
115
|
+
},
|
|
116
|
+
usage: {
|
|
117
|
+
options: {
|
|
118
|
+
name: 'Name to greet',
|
|
119
|
+
greeting: 'Greeting to use (default: "Hello")',
|
|
120
|
+
times: 'Number of times to repeat the greeting (default: 1)'
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
run: ctx => {
|
|
124
|
+
const { name = 'World', greeting, times } = ctx.values
|
|
125
|
+
for (let i = 0; i < times; i++) {
|
|
126
|
+
console.log(`${greeting}, ${name}!`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cli(process.argv.slice(2), command, {
|
|
132
|
+
name: 'my-app',
|
|
133
|
+
version: '1.0.0',
|
|
134
|
+
description: 'My CLI application'
|
|
135
|
+
})
|
|
44
136
|
```
|
|
45
137
|
|
|
138
|
+
For more detailed examples, check out the [playground/declarative](https://github.com/kazupon/gunshi/tree/main/playground/declarative) in the repository.
|
|
139
|
+
|
|
140
|
+
### Composable Sub-commands
|
|
141
|
+
|
|
142
|
+
Create a CLI with composable sub-commands:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import { cli } from 'gunshi'
|
|
146
|
+
|
|
147
|
+
// Define sub-commands
|
|
148
|
+
const createCommand = {
|
|
149
|
+
name: 'create',
|
|
150
|
+
description: 'Create a new resource',
|
|
151
|
+
options: {
|
|
152
|
+
name: { type: 'string', short: 'n' }
|
|
153
|
+
},
|
|
154
|
+
run: ctx => {
|
|
155
|
+
console.log(`Creating resource: ${ctx.values.name}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const listCommand = {
|
|
160
|
+
name: 'list',
|
|
161
|
+
description: 'List all resources',
|
|
162
|
+
run: () => {
|
|
163
|
+
console.log('Listing all resources...')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create a Map of sub-commands
|
|
168
|
+
const subCommands = new Map()
|
|
169
|
+
subCommands.set('create', createCommand)
|
|
170
|
+
subCommands.set('list', listCommand)
|
|
171
|
+
|
|
172
|
+
// Define the main command
|
|
173
|
+
const mainCommand = {
|
|
174
|
+
name: 'resource-manager',
|
|
175
|
+
description: 'Manage resources',
|
|
176
|
+
run: () => {
|
|
177
|
+
console.log('Use one of the sub-commands: create, list')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Run the CLI with composable sub-commands
|
|
182
|
+
cli(process.argv.slice(2), mainCommand, {
|
|
183
|
+
name: 'my-app',
|
|
184
|
+
version: '1.0.0',
|
|
185
|
+
subCommands
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
For more detailed examples, check out the [playground/composable](https://github.com/kazupon/gunshi/tree/main/playground/composable) in the repository.
|
|
190
|
+
|
|
191
|
+
### Lazy & Async Command Loading
|
|
192
|
+
|
|
193
|
+
Load commands lazily and execute them asynchronously:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
import { cli } from 'gunshi'
|
|
197
|
+
|
|
198
|
+
// Define a command that will be loaded lazily
|
|
199
|
+
const lazyCommand = async () => {
|
|
200
|
+
// Simulate async loading
|
|
201
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
202
|
+
|
|
203
|
+
// Return the actual command
|
|
204
|
+
return {
|
|
205
|
+
name: 'lazy',
|
|
206
|
+
description: 'A command that is loaded lazily',
|
|
207
|
+
run: async ctx => {
|
|
208
|
+
// Async execution
|
|
209
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
210
|
+
console.log('Command executed!')
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Create a Map of sub-commands with lazy-loaded commands
|
|
216
|
+
const subCommands = new Map()
|
|
217
|
+
subCommands.set('lazy', lazyCommand)
|
|
218
|
+
|
|
219
|
+
// Run the CLI with lazy-loaded commands
|
|
220
|
+
cli(
|
|
221
|
+
process.argv.slice(2),
|
|
222
|
+
{ name: 'main', run: () => {} },
|
|
223
|
+
{
|
|
224
|
+
name: 'my-app',
|
|
225
|
+
subCommands
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For more detailed examples, check out the [playground/lazy-async](https://github.com/kazupon/gunshi/tree/main/playground/lazy-async) in the repository.
|
|
231
|
+
|
|
232
|
+
### Auto Usage Generation
|
|
233
|
+
|
|
234
|
+
Gunshi automatically generates usage information:
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
import { cli } from 'gunshi'
|
|
238
|
+
|
|
239
|
+
const command = {
|
|
240
|
+
name: 'app',
|
|
241
|
+
description: 'My application',
|
|
242
|
+
options: {
|
|
243
|
+
path: { type: 'string', short: 'p' },
|
|
244
|
+
recursive: { type: 'boolean', short: 'r' },
|
|
245
|
+
operation: { type: 'string', short: 'o', required: true }
|
|
246
|
+
},
|
|
247
|
+
usage: {
|
|
248
|
+
options: {
|
|
249
|
+
path: 'File or directory path',
|
|
250
|
+
recursive: 'Operate recursively on directories',
|
|
251
|
+
operation: 'Operation to perform (list, copy, move, delete)'
|
|
252
|
+
},
|
|
253
|
+
examples: '# Example\n$ my-app --operation list --path ./src'
|
|
254
|
+
},
|
|
255
|
+
run: ctx => {
|
|
256
|
+
// Command implementation
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Run with --help to see the automatically generated usage information
|
|
261
|
+
cli(process.argv.slice(2), command, {
|
|
262
|
+
name: 'my-app',
|
|
263
|
+
version: '1.0.0'
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
For more detailed examples, check out the [playground/auto-usage](https://github.com/kazupon/gunshi/tree/main/playground/auto-usage) in the repository.
|
|
268
|
+
|
|
269
|
+
### Custom Usage Generation
|
|
270
|
+
|
|
271
|
+
Customize the usage message generation:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
import { cli } from 'gunshi'
|
|
275
|
+
|
|
276
|
+
// Custom header renderer
|
|
277
|
+
const customHeaderRenderer = ctx => {
|
|
278
|
+
return Promise.resolve(`
|
|
279
|
+
╔═══════════════════════╗
|
|
280
|
+
║ ${ctx.env.name.toUpperCase()} ║
|
|
281
|
+
╚═══════════════════════╝
|
|
282
|
+
${ctx.env.description}
|
|
283
|
+
Version: ${ctx.env.version}
|
|
284
|
+
`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Custom usage renderer
|
|
288
|
+
const customUsageRenderer = ctx => {
|
|
289
|
+
const lines = []
|
|
290
|
+
lines.push('USAGE:')
|
|
291
|
+
lines.push(` $ ${ctx.env.name} [options]`)
|
|
292
|
+
lines.push('')
|
|
293
|
+
lines.push('OPTIONS:')
|
|
294
|
+
|
|
295
|
+
for (const [key, option] of Object.entries(ctx.options || {})) {
|
|
296
|
+
const shortFlag = option.short ? `-${option.short}, ` : ' '
|
|
297
|
+
lines.push(` ${shortFlag}--${key.padEnd(10)} ${ctx.translation(key)}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return Promise.resolve(lines.join('\n'))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Run with custom renderers
|
|
304
|
+
cli(
|
|
305
|
+
process.argv.slice(2),
|
|
306
|
+
{ name: 'app', run: () => {} },
|
|
307
|
+
{
|
|
308
|
+
name: 'my-app',
|
|
309
|
+
version: '1.0.0',
|
|
310
|
+
description: 'My application',
|
|
311
|
+
renderHeader: customHeaderRenderer,
|
|
312
|
+
renderUsage: customUsageRenderer
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
For more detailed examples, check out the [playground/custom-usage](https://github.com/kazupon/gunshi/tree/main/playground/custom-usage) in the repository.
|
|
318
|
+
|
|
319
|
+
### Internationalization
|
|
320
|
+
|
|
321
|
+
Support internationalization:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
import { cli } from 'gunshi'
|
|
325
|
+
import enUS from './locales/en-US.json' with { type: 'json' }
|
|
326
|
+
|
|
327
|
+
const command = {
|
|
328
|
+
name: 'greeter',
|
|
329
|
+
options: {
|
|
330
|
+
name: { type: 'string', short: 'n' },
|
|
331
|
+
formal: { type: 'boolean', short: 'f' }
|
|
332
|
+
},
|
|
333
|
+
// Resource fetcher for translations
|
|
334
|
+
resource: async ctx => {
|
|
335
|
+
if (ctx.locale.toString() === 'ja-JP') {
|
|
336
|
+
const resource = await import('./locales/ja-JP.json', { with: { type: 'json' } })
|
|
337
|
+
return resource.default
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Default to English
|
|
341
|
+
return enUS
|
|
342
|
+
},
|
|
343
|
+
run: ctx => {
|
|
344
|
+
const { name = 'World', formal } = ctx.values
|
|
345
|
+
const greeting = formal ? ctx.translation('formal') : ctx.translation('informal')
|
|
346
|
+
console.log(`${greeting}, ${name}!`)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Run with locale support
|
|
351
|
+
cli(process.argv.slice(2), command, {
|
|
352
|
+
name: 'my-app',
|
|
353
|
+
version: '1.0.0',
|
|
354
|
+
// Set the locale via an environment variable
|
|
355
|
+
// If Node v21 or later is used, you can use the built-in `navigator.language` instead)
|
|
356
|
+
locale: new Intl.Locale(process.env.MY_LOCALE || 'en-US')
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
For more detailed examples, check out the [playground/i18n](https://github.com/kazupon/gunshi/tree/main/playground/i18n) in the repository.
|
|
361
|
+
|
|
46
362
|
## 🙌 Contributing guidelines
|
|
47
363
|
|
|
48
364
|
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.
|
|
49
365
|
|
|
366
|
+
## 💖 Credits
|
|
367
|
+
|
|
368
|
+
This project is inspired by:
|
|
369
|
+
|
|
370
|
+
- [`citty`](https://github.com/unjs/citty), created by UnJS team and contributors
|
|
371
|
+
- cline and claude 3.7 sonnet, examples and docs is generated
|
|
372
|
+
|
|
373
|
+
Thank you!
|
|
374
|
+
|
|
50
375
|
## ©️ License
|
|
51
376
|
|
|
52
377
|
[MIT](http://opensource.org/licenses/MIT)
|
|
@@ -57,11 +382,7 @@ If you are interested in contributing to `gunshi`, I highly recommend checking o
|
|
|
57
382
|
[npm-version-href]: https://npmjs.com/package/gunshi
|
|
58
383
|
[jsr-src]: https://jsr.io/badges/@kazupon/gunishi
|
|
59
384
|
[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
|
-
|
|
385
|
+
[install-size-src]: https://pkg-size.dev/badge/install/72346
|
|
386
|
+
[install-size-href]: https://pkg-size.dev/gunshi
|
|
66
387
|
[ci-src]: https://github.com/kazupon/gunshi/actions/workflows/ci.yml/badge.svg
|
|
67
388
|
[ci-href]: https://github.com/kazupon/gunshi/actions/workflows/ci.yml
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { create, deepFreeze, resolveLazyCommand } from "./utils-NHs5DuHk.js";
|
|
2
1
|
|
|
3
2
|
//#region locales/en-US.json
|
|
4
3
|
var COMMAND = "COMMAND";
|
|
@@ -57,6 +56,28 @@ const COMMAND_I18N_RESOURCE_KEYS = [
|
|
|
57
56
|
"FORMORE"
|
|
58
57
|
];
|
|
59
58
|
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/utils.ts
|
|
61
|
+
async function resolveLazyCommand(cmd, name, entry = false) {
|
|
62
|
+
const resolved = Object.assign(create(), typeof cmd == "function" ? await cmd() : cmd, { default: entry });
|
|
63
|
+
if (resolved.name == null && name) resolved.name = name;
|
|
64
|
+
return deepFreeze(resolved);
|
|
65
|
+
}
|
|
66
|
+
function create(obj = null) {
|
|
67
|
+
return Object.create(obj);
|
|
68
|
+
}
|
|
69
|
+
function log(...args) {
|
|
70
|
+
console.log(...args);
|
|
71
|
+
}
|
|
72
|
+
function deepFreeze(obj) {
|
|
73
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
74
|
+
for (const key of Object.keys(obj)) {
|
|
75
|
+
const value = obj[key];
|
|
76
|
+
if (typeof value === "object" && value !== null) deepFreeze(value);
|
|
77
|
+
}
|
|
78
|
+
return Object.freeze(obj);
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
//#endregion
|
|
61
82
|
//#region src/context.ts
|
|
62
83
|
const DEFAULT_LOCALE = "en-US";
|
|
@@ -175,4 +196,4 @@ async function loadCommandResource(ctx, command) {
|
|
|
175
196
|
}
|
|
176
197
|
|
|
177
198
|
//#endregion
|
|
178
|
-
export { COMMAND_OPTIONS_DEFAULT, COMMON_OPTIONS, DEFAULT_LOCALE, createCommandContext };
|
|
199
|
+
export { COMMAND_OPTIONS_DEFAULT, COMMON_OPTIONS, DEFAULT_LOCALE, create, createCommandContext, log, resolveLazyCommand };
|
package/lib/context.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,8 +1,195 @@
|
|
|
1
|
-
import { COMMAND_OPTIONS_DEFAULT, COMMON_OPTIONS, createCommandContext } from "./context-
|
|
2
|
-
import { create, log, resolveLazyCommand } from "./utils-NHs5DuHk.js";
|
|
3
|
-
import { renderHeader, renderUsage, renderValidationErrors } from "./renderer-Bo0DibAK.js";
|
|
1
|
+
import { COMMAND_OPTIONS_DEFAULT, COMMON_OPTIONS, create, createCommandContext, log, resolveLazyCommand } from "./context-B5z7lnoV.js";
|
|
4
2
|
import { parseArgs, resolveArgs } from "args-tokens";
|
|
5
3
|
|
|
4
|
+
//#region src/renderer/header.ts
|
|
5
|
+
function renderHeader(ctx) {
|
|
6
|
+
const title = ctx.env.description || ctx.env.name || "";
|
|
7
|
+
return Promise.resolve(title ? `${title} (${ctx.env.name || ""}${ctx.env.version ? ` v${ctx.env.version}` : ""})` : title);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/renderer/usage.ts
|
|
12
|
+
async function renderUsage(ctx) {
|
|
13
|
+
const messages = [];
|
|
14
|
+
if (!ctx.omitted && hasDescription(ctx)) messages.push(ctx.description, "");
|
|
15
|
+
messages.push(...await renderUsageSection(ctx), "");
|
|
16
|
+
if (ctx.omitted && await hasCommands(ctx)) messages.push(...await renderCommandsSection(ctx), "");
|
|
17
|
+
if (hasOptions(ctx)) messages.push(...await renderOptionsSection(ctx), "");
|
|
18
|
+
if (hasExamples(ctx)) messages.push(...renderExamplesSection(ctx), "");
|
|
19
|
+
return messages.join("\n");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Render the options section
|
|
23
|
+
* @param ctx A {@link CommandContext | command context}
|
|
24
|
+
* @returns A rendered options section
|
|
25
|
+
*/
|
|
26
|
+
async function renderOptionsSection(ctx) {
|
|
27
|
+
const messages = [];
|
|
28
|
+
messages.push(`${ctx.translation("OPTIONS")}:`);
|
|
29
|
+
const optionsPairs = getOptionsPairs(ctx);
|
|
30
|
+
messages.push(await generateOptionsUsage(ctx, optionsPairs));
|
|
31
|
+
return messages;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render the examples section
|
|
35
|
+
* @param ctx A {@link CommandContext | command context}
|
|
36
|
+
* @returns A rendered examples section
|
|
37
|
+
*/
|
|
38
|
+
function renderExamplesSection(ctx) {
|
|
39
|
+
const messages = [];
|
|
40
|
+
const examples = ctx.usage.examples.split("\n").map((example) => example.padStart(ctx.env.leftMargin + example.length));
|
|
41
|
+
messages.push(`${ctx.translation("EXAMPLES")}:`, ...examples);
|
|
42
|
+
return messages;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Render the usage section
|
|
46
|
+
* @param ctx A {@link CommandContext | command context}
|
|
47
|
+
* @returns A rendered usage section
|
|
48
|
+
*/
|
|
49
|
+
async function renderUsageSection(ctx) {
|
|
50
|
+
const messages = [`${ctx.translation("USAGE")}:`];
|
|
51
|
+
if (ctx.omitted) {
|
|
52
|
+
const defaultCommand = `${resolveEntry(ctx)}${await hasCommands(ctx) ? ` [${resolveSubCommand(ctx)}]` : ""} ${hasOptions(ctx) ? `<${ctx.translation("OPTIONS")}>` : ""} `;
|
|
53
|
+
messages.push(defaultCommand.padStart(ctx.env.leftMargin + defaultCommand.length));
|
|
54
|
+
if (await hasCommands(ctx)) {
|
|
55
|
+
const commandsUsage = `${resolveEntry(ctx)} <${ctx.translation("COMMANDS")}>`;
|
|
56
|
+
messages.push(commandsUsage.padStart(ctx.env.leftMargin + commandsUsage.length));
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
const usageStr = `${resolveEntry(ctx)} ${resolveSubCommand(ctx)} ${generateOptionsSymbols(ctx)}`;
|
|
60
|
+
messages.push(usageStr.padStart(ctx.env.leftMargin + usageStr.length));
|
|
61
|
+
}
|
|
62
|
+
return messages;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Render the commands section
|
|
66
|
+
* @param ctx A {@link CommandContext | command context}
|
|
67
|
+
* @returns A rendered commands section
|
|
68
|
+
*/
|
|
69
|
+
async function renderCommandsSection(ctx) {
|
|
70
|
+
const messages = [`${ctx.translation("COMMANDS")}:`];
|
|
71
|
+
const loadedCommands = await ctx.loadCommands();
|
|
72
|
+
const commandMaxLength = Math.max(...loadedCommands.map((cmd) => (cmd.name || "").length));
|
|
73
|
+
const commandsStr = await Promise.all(loadedCommands.map((cmd) => {
|
|
74
|
+
const key = cmd.name || "";
|
|
75
|
+
const desc = cmd.description || "";
|
|
76
|
+
const command = `${key.padEnd(commandMaxLength + ctx.env.middleMargin)}${desc} `;
|
|
77
|
+
return `${command.padStart(ctx.env.leftMargin + command.length)} `;
|
|
78
|
+
}));
|
|
79
|
+
messages.push(...commandsStr, "", ctx.translation("FORMORE"));
|
|
80
|
+
messages.push(...loadedCommands.map((cmd) => {
|
|
81
|
+
const commandHelp = `${ctx.env.name} ${cmd.name} --help`;
|
|
82
|
+
return `${commandHelp.padStart(ctx.env.leftMargin + commandHelp.length)}`;
|
|
83
|
+
}));
|
|
84
|
+
return messages;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the entry command name
|
|
88
|
+
* @param ctx A {@link CommandContext | command context}
|
|
89
|
+
* @returns The entry command name
|
|
90
|
+
*/
|
|
91
|
+
function resolveEntry(ctx) {
|
|
92
|
+
return ctx.env.name || ctx.translation("COMMAND");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the sub command name
|
|
96
|
+
* @param ctx A {@link CommandContext | command context}
|
|
97
|
+
* @returns The sub command name
|
|
98
|
+
*/
|
|
99
|
+
function resolveSubCommand(ctx) {
|
|
100
|
+
return ctx.name || ctx.translation("SUBCOMMAND");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if the command has a description
|
|
104
|
+
* @param ctx A {@link CommandContext | command context}
|
|
105
|
+
* @returns True if the command has a description
|
|
106
|
+
*/
|
|
107
|
+
function hasDescription(ctx) {
|
|
108
|
+
return !!ctx.description;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if the command has sub commands
|
|
112
|
+
* @param ctx A {@link CommandContext | command context}
|
|
113
|
+
* @returns True if the command has sub commands
|
|
114
|
+
*/
|
|
115
|
+
async function hasCommands(ctx) {
|
|
116
|
+
const loadedCommands = await ctx.loadCommands();
|
|
117
|
+
return loadedCommands.length > 1;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if the command has options
|
|
121
|
+
* @param ctx A {@link CommandContext | command context}
|
|
122
|
+
* @returns True if the command has options
|
|
123
|
+
*/
|
|
124
|
+
function hasOptions(ctx) {
|
|
125
|
+
return !!(ctx.options && Object.keys(ctx.options).length > 0);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check if the command has examples
|
|
129
|
+
* @param ctx A {@link CommandContext | command context}
|
|
130
|
+
* @returns True if the command has examples
|
|
131
|
+
*/
|
|
132
|
+
function hasExamples(ctx) {
|
|
133
|
+
return !!ctx.usage.examples;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if all options have default values
|
|
137
|
+
* @param ctx A {@link CommandContext | command context}
|
|
138
|
+
* @returns True if all options have default values
|
|
139
|
+
*/
|
|
140
|
+
function hasAllDefaultOptions(ctx) {
|
|
141
|
+
return !!(ctx.options && Object.values(ctx.options).every((opt) => opt.default));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Generate options symbols for usage
|
|
145
|
+
* @param ctx A {@link CommandContext | command context}
|
|
146
|
+
* @returns Options symbols for usage
|
|
147
|
+
*/
|
|
148
|
+
function generateOptionsSymbols(ctx) {
|
|
149
|
+
return hasOptions(ctx) ? hasAllDefaultOptions(ctx) ? `[${ctx.translation("OPTIONS")}]` : `<${ctx.translation("OPTIONS")}>` : "";
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get options pairs for usage
|
|
153
|
+
* @param ctx A {@link CommandContext | command context}
|
|
154
|
+
* @returns Options pairs for usage
|
|
155
|
+
*/
|
|
156
|
+
function getOptionsPairs(ctx) {
|
|
157
|
+
return Object.entries(ctx.options).reduce((acc, [name, value]) => {
|
|
158
|
+
let key = `--${name}`;
|
|
159
|
+
if (value.short) key = `-${value.short}, ${key}`;
|
|
160
|
+
if (value.type !== "boolean") key = value.default ? `${key} [${name}]` : `${key} <${name}>`;
|
|
161
|
+
acc[name] = key;
|
|
162
|
+
return acc;
|
|
163
|
+
}, create());
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Generate options usage
|
|
167
|
+
* @param ctx A {@link CommandContext | command context}
|
|
168
|
+
* @param optionsPairs Options pairs for usage
|
|
169
|
+
* @returns Generated options usage
|
|
170
|
+
*/
|
|
171
|
+
async function generateOptionsUsage(ctx, optionsPairs) {
|
|
172
|
+
const optionsMaxLength = Math.max(...Object.entries(optionsPairs).map(([_, value]) => value.length));
|
|
173
|
+
const optionSchemaMaxLength = ctx.env.usageOptionType ? Math.max(...Object.entries(optionsPairs).map(([key, _]) => ctx.options[key].type.length)) : 0;
|
|
174
|
+
const usages = await Promise.all(Object.entries(optionsPairs).map(([key, value]) => {
|
|
175
|
+
const rawDesc = ctx.translation(key);
|
|
176
|
+
const optionsSchema = ctx.env.usageOptionType ? `[${ctx.options[key].type}] ` : "";
|
|
177
|
+
const desc = `${optionsSchema ? optionsSchema.padEnd(optionSchemaMaxLength + 3) : ""}${rawDesc}`;
|
|
178
|
+
const option = `${value.padEnd(optionsMaxLength + ctx.env.middleMargin)}${desc}`;
|
|
179
|
+
return `${option.padStart(ctx.env.leftMargin + option.length)}`;
|
|
180
|
+
}));
|
|
181
|
+
return usages.join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/renderer/validation.ts
|
|
186
|
+
function renderValidationErrors(_ctx, error) {
|
|
187
|
+
const messages = [];
|
|
188
|
+
for (const err of error.errors) messages.push(err.message);
|
|
189
|
+
return Promise.resolve(messages.join("\n"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//#endregion
|
|
6
193
|
//#region src/cli.ts
|
|
7
194
|
async function cli(args, entry, opts = {}) {
|
|
8
195
|
const tokens = parseArgs(args);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gunshi",
|
|
3
3
|
"description": "Modern javascript command-line library",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "kazuya kawaguchi",
|
|
7
7
|
"email": "kawakazu80@gmail.com"
|
|
@@ -86,10 +86,10 @@
|
|
|
86
86
|
"gh-changelogen": "^0.2.8",
|
|
87
87
|
"knip": "^5.45.0",
|
|
88
88
|
"lint-staged": "^15.4.3",
|
|
89
|
-
"pkg-pr-new": "^0.0.
|
|
89
|
+
"pkg-pr-new": "^0.0.41",
|
|
90
90
|
"prettier": "^3.5.3",
|
|
91
91
|
"tsdown": "^0.6.4",
|
|
92
|
-
"typescript": "^5.
|
|
92
|
+
"typescript": "^5.4.2",
|
|
93
93
|
"typescript-eslint": "^8.26.0",
|
|
94
94
|
"vitest": "^3.0.7"
|
|
95
95
|
},
|
|
@@ -113,11 +113,11 @@
|
|
|
113
113
|
"clean": "git clean -df",
|
|
114
114
|
"dev": "pnpx @eslint/config-inspector --config eslint.config.ts",
|
|
115
115
|
"dev:eslint": "pnpx @eslint/config-inspector --config eslint.config.ts",
|
|
116
|
-
"fix": "pnpm run --
|
|
116
|
+
"fix": "pnpm run --stream --color \"/^fix:/\"",
|
|
117
117
|
"fix:eslint": "eslint . --fix",
|
|
118
118
|
"fix:knip": "knip --fix --no-exit-code",
|
|
119
119
|
"fix:prettier": "prettier . --write",
|
|
120
|
-
"lint": "pnpm run --
|
|
120
|
+
"lint": "pnpm run --stream --color \"/^lint:/\"",
|
|
121
121
|
"lint:eslint": "eslint .",
|
|
122
122
|
"lint:knip": "knip",
|
|
123
123
|
"lint:prettier": "prettier . --check",
|
package/lib/renderer-Bo0DibAK.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { create } from "./utils-NHs5DuHk.js";
|
|
2
|
-
|
|
3
|
-
//#region src/renderer.ts
|
|
4
|
-
function renderHeader(ctx) {
|
|
5
|
-
const title = ctx.env.description || ctx.env.name || "";
|
|
6
|
-
return Promise.resolve(title ? `${title} (${ctx.env.name || ""}${ctx.env.version ? ` v${ctx.env.version}` : ""})` : title);
|
|
7
|
-
}
|
|
8
|
-
async function renderUsage(ctx) {
|
|
9
|
-
const messages = [];
|
|
10
|
-
if (!ctx.omitted && hasDescription(ctx)) messages.push(ctx.description, "");
|
|
11
|
-
messages.push(...await renderUsageSection(ctx), "");
|
|
12
|
-
if (ctx.omitted && await hasCommands(ctx)) messages.push(...await renderCommandsSection(ctx), "");
|
|
13
|
-
if (hasOptions(ctx)) messages.push(...await renderOptionsSection(ctx), "");
|
|
14
|
-
if (hasExamples(ctx)) messages.push(...renderExamplesSection(ctx), "");
|
|
15
|
-
return messages.join("\n");
|
|
16
|
-
}
|
|
17
|
-
function renderValidationErrors(_ctx, error) {
|
|
18
|
-
const messages = [];
|
|
19
|
-
for (const err of error.errors) messages.push(err.message);
|
|
20
|
-
return Promise.resolve(messages.join("\n"));
|
|
21
|
-
}
|
|
22
|
-
async function renderOptionsSection(ctx) {
|
|
23
|
-
const messages = [];
|
|
24
|
-
messages.push(`${ctx.translation("OPTIONS")}:`);
|
|
25
|
-
const optionsPairs = getOptionsPairs(ctx);
|
|
26
|
-
messages.push(await generateOptionsUsage(ctx, optionsPairs));
|
|
27
|
-
return messages;
|
|
28
|
-
}
|
|
29
|
-
function renderExamplesSection(ctx) {
|
|
30
|
-
const messages = [];
|
|
31
|
-
const examples = ctx.usage.examples.split("\n").map((example) => example.padStart(ctx.env.leftMargin + example.length));
|
|
32
|
-
messages.push(`${ctx.translation("EXAMPLES")}:`, ...examples);
|
|
33
|
-
return messages;
|
|
34
|
-
}
|
|
35
|
-
async function renderUsageSection(ctx) {
|
|
36
|
-
const messages = [`${ctx.translation("USAGE")}:`];
|
|
37
|
-
if (ctx.omitted) {
|
|
38
|
-
const defaultCommand = `${resolveEntry(ctx)}${await hasCommands(ctx) ? ` [${resolveSubCommand(ctx)}]` : ""} ${hasOptions(ctx) ? `<${ctx.translation("OPTIONS")}>` : ""} `;
|
|
39
|
-
messages.push(defaultCommand.padStart(ctx.env.leftMargin + defaultCommand.length));
|
|
40
|
-
if (await hasCommands(ctx)) {
|
|
41
|
-
const commandsUsage = `${resolveEntry(ctx)} <${ctx.translation("COMMANDS")}>`;
|
|
42
|
-
messages.push(commandsUsage.padStart(ctx.env.leftMargin + commandsUsage.length));
|
|
43
|
-
}
|
|
44
|
-
} else {
|
|
45
|
-
const usageStr = `${resolveEntry(ctx)} ${resolveSubCommand(ctx)} ${generateOptionsSymbols(ctx)}`;
|
|
46
|
-
messages.push(usageStr.padStart(ctx.env.leftMargin + usageStr.length));
|
|
47
|
-
}
|
|
48
|
-
return messages;
|
|
49
|
-
}
|
|
50
|
-
async function renderCommandsSection(ctx) {
|
|
51
|
-
const messages = [`${ctx.translation("COMMANDS")}:`];
|
|
52
|
-
const loadedCommands = await ctx.loadCommands();
|
|
53
|
-
const commandMaxLength = Math.max(...loadedCommands.map((cmd) => (cmd.name || "").length));
|
|
54
|
-
const commandsStr = await Promise.all(loadedCommands.map((cmd) => {
|
|
55
|
-
const key = cmd.name || "";
|
|
56
|
-
const desc = cmd.description || "";
|
|
57
|
-
const command = `${key.padEnd(commandMaxLength + ctx.env.middleMargin)}${desc} `;
|
|
58
|
-
return `${command.padStart(ctx.env.leftMargin + command.length)} `;
|
|
59
|
-
}));
|
|
60
|
-
messages.push(...commandsStr, "", ctx.translation("FORMORE"));
|
|
61
|
-
messages.push(...loadedCommands.map((cmd) => {
|
|
62
|
-
const commandHelp = `${ctx.env.name} ${cmd.name} --help`;
|
|
63
|
-
return `${commandHelp.padStart(ctx.env.leftMargin + commandHelp.length)}`;
|
|
64
|
-
}));
|
|
65
|
-
return messages;
|
|
66
|
-
}
|
|
67
|
-
function resolveEntry(ctx) {
|
|
68
|
-
return ctx.env.name || ctx.translation("COMMAND");
|
|
69
|
-
}
|
|
70
|
-
function resolveSubCommand(ctx) {
|
|
71
|
-
return ctx.name || ctx.translation("SUBCOMMAND");
|
|
72
|
-
}
|
|
73
|
-
function hasDescription(ctx) {
|
|
74
|
-
return !!ctx.description;
|
|
75
|
-
}
|
|
76
|
-
async function hasCommands(ctx) {
|
|
77
|
-
const loadedCommands = await ctx.loadCommands();
|
|
78
|
-
return loadedCommands.length > 1;
|
|
79
|
-
}
|
|
80
|
-
function hasOptions(ctx) {
|
|
81
|
-
return !!(ctx.options && Object.keys(ctx.options).length > 0);
|
|
82
|
-
}
|
|
83
|
-
function hasExamples(ctx) {
|
|
84
|
-
return !!ctx.usage.examples;
|
|
85
|
-
}
|
|
86
|
-
function hasAllDefaultOptions(ctx) {
|
|
87
|
-
return !!(ctx.options && Object.values(ctx.options).every((opt) => opt.default));
|
|
88
|
-
}
|
|
89
|
-
function generateOptionsSymbols(ctx) {
|
|
90
|
-
return hasOptions(ctx) ? hasAllDefaultOptions(ctx) ? `[${ctx.translation("OPTIONS")}]` : `<${ctx.translation("OPTIONS")}>` : "";
|
|
91
|
-
}
|
|
92
|
-
function getOptionsPairs(ctx) {
|
|
93
|
-
return Object.entries(ctx.options).reduce((acc, [name, value]) => {
|
|
94
|
-
let key = `--${name}`;
|
|
95
|
-
if (value.short) key = `-${value.short}, ${key}`;
|
|
96
|
-
if (value.type !== "boolean") key = value.default ? `${key} [${name}]` : `${key} <${name}>`;
|
|
97
|
-
acc[name] = key;
|
|
98
|
-
return acc;
|
|
99
|
-
}, create());
|
|
100
|
-
}
|
|
101
|
-
async function generateOptionsUsage(ctx, optionsPairs) {
|
|
102
|
-
const optionsMaxLength = Math.max(...Object.entries(optionsPairs).map(([_, value]) => value.length));
|
|
103
|
-
const optionSchemaMaxLength = ctx.env.usageOptionType ? Math.max(...Object.entries(optionsPairs).map(([key, _]) => ctx.options[key].type.length)) : 0;
|
|
104
|
-
const usages = await Promise.all(Object.entries(optionsPairs).map(([key, value]) => {
|
|
105
|
-
const rawDesc = ctx.translation(key);
|
|
106
|
-
const optionsSchema = ctx.env.usageOptionType ? `[${ctx.options[key].type}] ` : "";
|
|
107
|
-
const desc = `${optionsSchema ? optionsSchema.padEnd(optionSchemaMaxLength + 3) : ""}${rawDesc}`;
|
|
108
|
-
const option = `${value.padEnd(optionsMaxLength + ctx.env.middleMargin)}${desc}`;
|
|
109
|
-
return `${option.padStart(ctx.env.leftMargin + option.length)}`;
|
|
110
|
-
}));
|
|
111
|
-
return usages.join("\n");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
//#endregion
|
|
115
|
-
export { renderHeader, renderUsage, renderValidationErrors };
|
package/lib/renderer.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { ArgOptions } from 'args-tokens';
|
|
2
|
-
import { b as CommandContext } from './types.d-00BVt8hZ.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Render the header
|
|
6
|
-
* @param ctx A {@link CommandContext | command context}
|
|
7
|
-
* @returns A rendered header
|
|
8
|
-
*/
|
|
9
|
-
declare function renderHeader<Options extends ArgOptions>(ctx: Readonly<CommandContext<Options>>): Promise<string>;
|
|
10
|
-
/**
|
|
11
|
-
* Render the usage
|
|
12
|
-
* @param ctx A {@link CommandContext | command context}
|
|
13
|
-
* @returns A rendered usage
|
|
14
|
-
*/
|
|
15
|
-
declare function renderUsage<Options extends ArgOptions>(ctx: Readonly<CommandContext<Options>>): Promise<string>;
|
|
16
|
-
/**
|
|
17
|
-
* Render the validation errors
|
|
18
|
-
* @param ctx A {@link CommandContext | command context}
|
|
19
|
-
* @param error An {@link AggregateError} of option in `args-token` validation
|
|
20
|
-
* @returns A rendered validation error
|
|
21
|
-
*/
|
|
22
|
-
declare function renderValidationErrors<Options extends ArgOptions>(_ctx: CommandContext<Options>, error: AggregateError): Promise<string>;
|
|
23
|
-
|
|
24
|
-
export { renderHeader, renderUsage, renderValidationErrors };
|
package/lib/renderer.js
DELETED
package/lib/utils-NHs5DuHk.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
//#region src/utils.ts
|
|
3
|
-
async function resolveLazyCommand(cmd, name, entry = false) {
|
|
4
|
-
const resolved = Object.assign(create(), typeof cmd == "function" ? await cmd() : cmd, { default: entry });
|
|
5
|
-
if (resolved.name == null && name) resolved.name = name;
|
|
6
|
-
return deepFreeze(resolved);
|
|
7
|
-
}
|
|
8
|
-
function create(obj = null) {
|
|
9
|
-
return Object.create(obj);
|
|
10
|
-
}
|
|
11
|
-
function log(...args) {
|
|
12
|
-
console.log(...args);
|
|
13
|
-
}
|
|
14
|
-
function deepFreeze(obj) {
|
|
15
|
-
if (obj === null || typeof obj !== "object") return obj;
|
|
16
|
-
for (const key of Object.keys(obj)) {
|
|
17
|
-
const value = obj[key];
|
|
18
|
-
if (typeof value === "object" && value !== null) deepFreeze(value);
|
|
19
|
-
}
|
|
20
|
-
return Object.freeze(obj);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
24
|
-
export { create, deepFreeze, log, resolveLazyCommand };
|