diff --git a/src/usage.ts b/src/usage.ts index cb4a794..b8d4ae5 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -4,10 +4,10 @@ import { formatLineColumns, resolveValue } from "./_utils"; import type { ArgsDef, CommandDef } from "./types"; import { resolveArgs } from "./args"; -export async function showUsage( - cmd: CommandDef, - parent?: CommandDef, -) { +export async function showUsage< + T extends ArgsDef = ArgsDef, + U extends ArgsDef = ArgsDef, +>(cmd: CommandDef, parent?: CommandDef) { try { consola.log((await renderUsage(cmd, parent)) + "\n"); } catch (error) { @@ -15,10 +15,10 @@ export async function showUsage( } } -export async function renderUsage( - cmd: CommandDef, - parent?: CommandDef, -) { +export async function renderUsage< + T extends ArgsDef = ArgsDef, + U extends ArgsDef = ArgsDef, +>(cmd: CommandDef, parent?: CommandDef) { const cmdMeta = await resolveValue(cmd.meta || {}); const cmdArgs = resolveArgs(await resolveValue(cmd.args || {})); const parentMeta = await resolveValue(parent?.meta || {}); diff --git a/test/usage.test.ts b/test/usage.test.ts new file mode 100644 index 0000000..104385b --- /dev/null +++ b/test/usage.test.ts @@ -0,0 +1,306 @@ +import { expect, it, describe } from "vitest"; +import { renderUsage } from "../src/usage"; +import { defineCommand } from "../src"; + +describe("usage", () => { + it("renders arguments", async () => { + const command = defineCommand({ + meta: { + name: "Commander", + description: "A command", + }, + args: { + foo: { + type: "string", + required: true, + description: "A foo", + }, + bar: { + alias: ["b"], + type: "string", + description: "A bar", + }, + pos: { + type: "positional", + name: "pos", + description: "A pos", + }, + enum: { + type: "enum", + name: "enum", + options: ["a", "b"], + description: "An enum", + }, + boolean: { + type: "boolean", + name: "boolean", + description: "A boolean", + }, + }, + }); + + const usage = await renderUsage(command); + + expect(usage).toMatchInlineSnapshot(` + "A command (Commander) + + USAGE \`Commander [OPTIONS] --foo \` + + ARGUMENTS + + \`POS\` A pos + + OPTIONS + + \`--foo (required)\` A foo + \`-b, --bar\` A bar + \`--enum=\` An enum + \`--boolean\` A boolean + " + `); + }); + + it("renders the negative description when a boolean default is true", async () => { + const command = defineCommand({ + meta: { + name: "Commander", + description: "A command", + }, + args: { + boolean: { + type: "boolean", + name: "boolean", + default: true, + description: "A boolean", + negativeDescription: "A negative boolean", + }, + }, + }); + + const usage = await renderUsage(command); + + expect(usage).toMatchInlineSnapshot(` + "A command (Commander) + + USAGE \`Commander [OPTIONS] \` + + OPTIONS + + \`--no-boolean\` A negative boolean + " + `); + }); + + it('renders arguments hints when "valueHint" is provided', async () => { + const command = defineCommand({ + meta: { + name: "Commander", + description: "A command", + }, + args: { + foo: { + type: "string", + description: "A foo", + valueHint: "FOO", + }, + }, + }); + + const usage = await renderUsage(command); + + expect(usage).toMatchInlineSnapshot(` + "A command (Commander) + + USAGE \`Commander [OPTIONS] \` + + OPTIONS + + \`--foo=\` A foo + " + `); + }); + + it("renders the default value when provided", async () => { + const command = defineCommand({ + meta: { + name: "Commander", + description: "A command", + }, + args: { + foo: { + type: "string", + description: "A foo", + default: "bar", + }, + }, + }); + + const usage = await renderUsage(command); + + expect(usage).toMatchInlineSnapshot(` + "A command (Commander) + + USAGE \`Commander [OPTIONS] \` + + OPTIONS + + \`--foo="bar"\` A foo + " + `); + }); + + it("renders subcommands", async () => { + const command = defineCommand({ + meta: { + name: "Commander", + description: "A command", + }, + subCommands: { + sub: defineCommand({ + meta: { + name: "Subcommander", + description: "A subcommand", + }, + }), + }, + }); + + const usage = await renderUsage(command); + + expect(usage).toMatchInlineSnapshot(` + "A command (Commander) + + USAGE \`Commander sub\` + + COMMANDS + + \`sub\` A subcommand + + Use \`Commander --help\` for more information about a command." + `); + }); + + it("renders both arguments and subcommands", async () => { + const command = defineCommand({ + meta: { + name: "Commander", + description: "A command", + }, + args: { + foo: { + required: true, + description: "A foo", + }, + }, + subCommands: { + sub: defineCommand({ + meta: { + name: "Subcommander", + description: "A subcommand", + }, + }), + }, + }); + + const usage = await renderUsage(command); + + expect(usage).toMatchInlineSnapshot(` + "A command (Commander) + + USAGE \`Commander [OPTIONS] --foo sub\` + + OPTIONS + + \`--foo (required)\` A foo + + COMMANDS + + \`sub\` A subcommand + + Use \`Commander --help\` for more information about a command." + `); + }); + + it("does not render hidden subcommands", async () => { + const command = defineCommand({ + meta: { + name: "Commander", + description: "A command", + }, + subCommands: { + sub: defineCommand({ + meta: { + name: "Subcommander", + description: "A subcommand", + hidden: true, + }, + }), + start: defineCommand({ + meta: { + name: "Start", + description: "A start", + }, + }), + }, + }); + + const usage = await renderUsage(command); + + expect(usage).toMatchInlineSnapshot(` + "A command (Commander) + + USAGE \`Commander start\` + + COMMANDS + + \`start\` A start + + Use \`Commander --help\` for more information about a command." + `); + }); + + it("uses parents meta to explain how to run sub commands", async () => { + const childCommand = defineCommand({ + meta: { + name: "child-command", + description: "A child command", + }, + args: { + foo: { + type: "string", + description: "A foo", + }, + }, + subCommands: { + "sub-command": defineCommand({}), + }, + }); + + const parentCommand = defineCommand({ + meta: { + name: "parent-command", + }, + subCommands: { + sub: childCommand, + }, + }); + + const usage = await renderUsage(childCommand, parentCommand); + + expect(usage).toMatchInlineSnapshot(` + "A child command (parent-command child-command) + + USAGE \`parent-command child-command [OPTIONS] sub-command\` + + OPTIONS + + \`--foo\` A foo + + COMMANDS + + \`sub-command\` + + Use \`parent-command child-command --help\` for more information about a command." + `); + }); +});