Skip to content

No Cliffy Action Wrapper

NewSuggestion
small-rules/no-cliffy-action-wrapper

Disallow Cliffy .action handlers that only forward their arguments to another function.

Rationale

A handler like .action(async (flags, search) => { await runListAsync(search, flags); }) does nothing. It also costs you more than the three lines it takes up, because Cliffy infers the option and argument types only inside the handler. Move the work out and you end up hand-writing a ListFlags interface that restates your .option calls, and now the two can drift apart. Put the implementation in the handler and all of that goes away.

The rule reports a handler whose whole body is one call to a named function, where every argument comes from the handler’s own parameters. That covers reordering, rest parameters, destructuring, rebuilding an object or array from them, and reading properties off them. An await, return, or void in front of the call doesn’t change that, and the target can be local or imported.

These all pass on purpose.

  • Lazy loading: const { load } = await import("./load"); await load(flags); is two statements, and it’s doing a real job.

  • A bare reference: .action(run) has no wrapper to remove. It still needs a hand-written flags type, but that’s a different complaint.

  • Method calls: .action((options) => console.log(options)) and .action(function () { this.showHelp(); }) are the whole command.

  • Added values: .action((flags) => run(flags, "list")) and .action(() => exit(0)) pass something the handler didn’t receive, so the function is probably shared for a reason.

  • Globals and parameters: this file has no function body to move into the handler.

Only commands the rule can recognize count, so it checks .action on a chain that starts from new Command() from @cliffy/command, from a function named in factories, or from a const holding either. A command that arrives as a function parameter or an import isn’t checked. factories defaults to ["createBaseCommand"] and replaces that default when you set it.

There’s no autofix. Inlining a function means renaming its parameters to match the handler and dealing with early returns. That’s better done by hand than by a fixer that gets it subtly wrong.

Diagnostic Messages

actionWrapper
This handler only forwards to `{{name}}`. Put the implementation in the handler, where Cliffy already infers the option and argument types.

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-cliffy-action-wrapper": [
"error",
{
"factories": [
"createBaseCommand"
]
}
]
}
}

Examples

A handler that only forwards to a function
import { createBaseCommand } from "@bin/cli/base-command";
async function runGetAsync(references, { json, refresh }) {
const models = await loadModelsAsync(references, refresh);
printModels(models, json);
}
export const getCommand = createBaseCommand("get", undefined, "Get models.")
.arguments("<model...:model>")
.action(async (flags, ...models) => {
await runGetAsync(models, flags);
});
The implementation lives in the handler
import { createBaseCommand } from "@bin/cli/base-command";
export const getCommand = createBaseCommand("get", undefined, "Get models.")
.arguments("<model...:model>")
.action(async ({ json, refresh }, ...references) => {
const models = await loadModelsAsync(references, refresh);
printModels(models, json);
});