Skip to content

No Multiple Cliffy Commands

NewSuggestion
small-rules/no-multiple-cliffy-commands

Disallow defining more than one Cliffy command in a file.

Rationale

One command per file, please. When list, get, and their parent share a file, its imports load for all three even if you only ran --help, and the file only ever grows. Cliffy can already load a subcommand on demand with .command("list", async () => ...), but that only helps if the subcommand is in another file.

The first command in a file is fine, and every one after it gets reported. These count as a command:

  • new Command(), where Command comes from @cliffy/command or its jsr: or npm: forms, including through an alias or a namespace import.

  • A call to a function named in factories.

  • .command("name") and .command("name", "description"), which define a subcommand right there, even though there’s no second new Command() to see.

  • A class that extends Cliffy’s Command.

factories defaults to ["createBaseCommand"] and replaces that default when you set it. Factories match by name, because a project can import its factory from anywhere. If you set the option, include createBaseCommand yourself if you still use it.

Registering a command from elsewhere passes, so .command("list", listCommand) and .command("help", new HelpCommand()) are both fine. The second argument is why: anything that isn’t a string means the command wasn’t defined here. That also makes the rule miss a description held in a variable, such as .command("list", description), which is a fair trade.

A file that declares a factory and also calls it counts twice. Test files that build more than one command get reported too, so add an override for them if you need one.

Diagnostic Messages

multipleCommands
This file already defines a Cliffy command on line {{line}}. Move this one to its own file, and have the parent load it with `.command(name, async () => ...)`.

Configuration

This rule accepts one options object after the severity.

factoriesOptional

Names of functions that return a new Cliffy command. A call counts by name wherever the function is imported from.

{
"jsPlugins": [
"@pobammer-ts/small-rules"
],
"rules": {
"small-rules/no-multiple-cliffy-commands": [
"error",
{
"factories": [
"createBaseCommand"
]
}
]
}
}

Examples

A command and its subcommands in one file
import { createBaseCommand } from "@bin/cli/base-command";
const listCommand = createBaseCommand("list", undefined, "List LLMs.").action(() => {});
const getCommand = createBaseCommand("get", undefined, "Get LLMs.").action(() => {});
export const llmsCommand = createBaseCommand("llms", undefined, "Query LLMs.")
.command("list", listCommand)
.command("get", getCommand);
One command that loads its subcommands from their own files
import { createBaseCommand } from "@bin/cli/base-command";
export const llmsCommand = createBaseCommand("llms", undefined, "Query LLMs.")
.command("list", async () => {
const { listCommand } = await import("./commands/list-command");
return listCommand;
})
.command("get", async () => {
const { getCommand } = await import("./commands/get-command");
return getCommand;
});