Status: informative, tiny, growingly unstable.
This library emphasizes simple maintainability for your tools:
This is not a library for the prettiest CLI's; it's a library for sane, maintainable tools.
| Section | Code | Comments | | ======= | ==== | ======== | | Runtime | 221 | 72 | | Types | 215 | 65 | | Tests | 659 | 61 |
Minimized (not that you'd use it that way): <4kb. This is a readable library.
If you just need a CLI with some help for a quick script, here you go:
// a one statement Hello World in Deno
import {assertEquals} from "@std/assert";
import { command, required, runCommand, stringFlag } from "@un-clever/cli-library";
const status = await runCommand(
command({
description: "hello command",
flags: { who: required("who", "who to say hello to", stringFlag, "World") },
run: async (args: { flags: { who: string } }, output) => {
await output(`Hello, ${args.flags.who}!`);
return 0;
},
}),
Deno.args,
Deno.stdout,
);
assertEquals(status, 0);
You can use that quick-and-dirty API for throw-together scripts, complete with help. Under the surface, though, there's strong typing and a set of composeable types. Here's the same example, exploded for type commentary:
// lets unpack the pieces and types a bit more explicitly
import { command, required, runCommand, stringFlag } from "@un-clever/cli-library";
import type {FlagsetReturn, StringOutput} from "@un-clever/cli-library";
import {assertEquals} from "@std/assert";
// a CLI begins with a set of flags that parse to an expected type
const helloFlags = {
who: required("who", "who to say hello to", stringFlag, "World"),
};
type HelloFlags = FlagsetReturn<typeof helloFlags>;
// then we have a function that implements our command and expects
// 1. flags like we've described
// 2. an async function that outputs strings (makes testing easier!)
// and returns an integer status code
async function helloHandler(
cliArgs: { flags: HelloFlags },
output: StringOutput,
) {
await output(JSON.stringify(cliArgs));
return 0; // The SHELL's idea of success!
}
// TODO: switch this to the simpler RUN interface
// we bundle those up into a Command
const helloCommand = command({
description: "Hello command",
flags: helloFlags,
run: helloHandler,
});
const status = await runCommand(helloCommand, ["--who", "Abe"], Deno.stdout);
assertEquals(status, 0);
Here's the basic concept. An un-clever CLI command has
string[]
, parses that string array as command line args into a simple structure.Such commands can easily be combined into an un-clever Multi-CLI that has can list or execute the subcommands.
The core of un-clever's CLI engine lies in its extensible command line parser.
string[]
.string[]
into named flags and positional args.Raw args are string[].
Flag parsers accept index + args and return n + value;
Flagsets are Record<string, Flag>
Flags are just parser, name, description, default.
Flagsets drive commandline parsers which produce args, flags, and dashdash
Command handlers accept {args, flags, dashdash} and a writer.
Writer are just (string)=>Promise
Zod, Typebox
Web interface
Multicommands
I find myself having to use Node, Bun, Deno, and CloudFlare workers. I want to write CLI tools to support my work without making it a major endeavor to switch runtimes, tweak a tool after not looking at it for a year, etc.
This library showcases some ways to do that.
-rf
in rm -rf
).In 2024 I've looked at:
class
keyword some misguided coders think is evil (while they proceed to write bad OOP without using the class
keyword).They all have their own tradeoffs and do a lot more than this package does. I would use them for my occasional CLI's except that: