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 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
- - 📏 **Minimal**: Run the commands with a minimum API.
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
@@ -1,4 +1,3 @@
1
- import { DEFAULT_LOCALE, createCommandContext } from "./context-DmZAeiph.js";
2
- import "./utils-NHs5DuHk.js";
1
+ import { DEFAULT_LOCALE, createCommandContext } from "./context-B5z7lnoV.js";
3
2
 
4
3
  export { DEFAULT_LOCALE, createCommandContext };
package/lib/index.js CHANGED
@@ -1,8 +1,195 @@
1
- import { COMMAND_OPTIONS_DEFAULT, COMMON_OPTIONS, createCommandContext } from "./context-DmZAeiph.js";
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.2.2",
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.40",
89
+ "pkg-pr-new": "^0.0.41",
90
90
  "prettier": "^3.5.3",
91
91
  "tsdown": "^0.6.4",
92
- "typescript": "^5.8.2",
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 --parallel --color \"/^fix:/\"",
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 --parallel --color \"/^lint:/\"",
120
+ "lint": "pnpm run --stream --color \"/^lint:/\"",
121
121
  "lint:eslint": "eslint .",
122
122
  "lint:knip": "knip",
123
123
  "lint:prettier": "prettier . --check",
@@ -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
@@ -1,4 +0,0 @@
1
- import "./utils-NHs5DuHk.js";
2
- import { renderHeader, renderUsage, renderValidationErrors } from "./renderer-Bo0DibAK.js";
3
-
4
- export { renderHeader, renderUsage, renderValidationErrors };
@@ -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 };