UNPKG

commandstruct

Version:
483 lines (384 loc) 11.4 kB
# Commandstruct 🛠️ Typesafe and modular CLIs with [sade](https://github.com/lukeed/sade). Commandstruct is a simple and powerful tool for building fast and typesafe command-line applications. It supports CLI commands, sub-commands, positional arguments and flags. It also supports dependency injection using [getbox](https://github.com/eriicafes/getbox). ## Installation Commandstruct has both `sade` and `getbox` as peer-dependencies. ```sh npm i commandstruct ``` ## Usage ### A Regular Program ```ts import { arg, program, command, flag } from "commandstruct"; const commitCmd = command("commit") .describe("make a commit") .flags({ message: flag("commit message").char("m").requiredParam("string"), }) .action(({ flags }) => { console.log("committing with message", flags.message); }); const pushCmd = command("push") .describe("push changes") .args({ repo: arg(), branch: arg().optional(), }) .action(({ args }) => { console.log("pushing repo", args.repo, "at", args.branch || "HEAD"); }); const prog = program("notgit") .describe("not your regular git") .flags({ verbose: flag("display extra information on command run") }) .commands(commitCmd, pushCmd) .build(); prog.run(); ``` ### A Single Program ```ts import { arg, singleProgram, flag } from "commandstruct"; const prog = singleProgram("mycat") .describe("print contents of file to stdout") .args({ file: arg(), }) .flags({ number: flag("number output lines").char("n"), }) .action(({ args, flags, restArgs }) => { console.log( "printing contents of file", args.file, flags.n ? "with lines" : "without lines", restArgs, ); }); prog.run(); ``` ## Program A program defines it's commands and an optional default command. When running the program, one of it's commands must be executed otherwise you get a `"No command specified"` error if no default command is set. Program wide flags can be defined on the program. ```ts const prog = program("notgit") .describe("not your regular git") .example("notgit commit -m 'my first commit'") .example("notgit push origin main") .version("v1.0.0") .flags({ /** program flags */ }) .commands(/** commands */) .default(/** default command */) .build(); ``` ## Single Program A single program unlike a regular program does not have any commands, the entire program executes as one command. A single program supports `args` and `action`. ```ts const prog = singleProgram("mycat") .describe("print contents of file to stdout") .example("mycat") .example("git push origin main") .version("v1.0.0") .args({ /** program args */ }) .flags({ /** program flags */ }) .action(() => { /** program action */ }); ``` ## Command A command defines its `flags`, `actions` and also additional `subcommands` if needed. ```ts const programFlags = { /** program flags */ }; const cmd = command("commit") .describe("make a commit") .alias("cm", "com") .alias("comit") .example("git commit -m 'my first commit'") .example("git cm -m 'another commit") .programFlags<typeof programFlags>() .subcommands(/** subcommands */) .args({ /** command args */ }) .flags({ /** command flags */ }) .action(({ args, flags, restArgs }, box) => { /** command action - box is the getbox Box instance */ }); ``` ## Default Command You can make any command or subcommand the default command. This will execute that command when no command was passed to the program. ```ts import { commitCmd, pushCmd } from "./commands"; const prog = program("nogit") .commands(commitCmd, pushCmd) .default(commitCmd) // commitCmd will be executed by default .build(); ``` ## Args Positional arguments are declared in the order they should be provided. Optional arguments must be placed after required arguments. ```ts command("example") .args({ foo: arg(), bar: arg(), baz: arg().optional(), }) .action(({ args, restArgs }) => { /** args: { foo: string; bar: string; baz: string | undefined; } restArgs: string[] */ }); ``` Variadic arguments can be accessed using restArgs. ## Flags By default flags are boolean values and default to false. Flag keys are also converted to kebab-case except when explicitly disabled using `.preserveCase()`. ```ts command("example") .flags({ foo: flag("foo description"), anotherFoo: flag("another foo description"), lastFoo: flag("last foo description").preserveCase(), }) .action(({ flags }) => { /** flags: { foo: boolean; "another-foo": boolean; lastFoo: boolean; } */ }); ``` ### Char Flag Allow passing flags with short character. ```ts command("example") .flags({ foo: flag("foo description").char("f"), }) .action(({ flags }) => { // flags.foo === flags.f }); ``` ### Flag Params Accept values with flags. ```ts command("example") .flags({ foo: flag("foo description").requiredParam("string"), bar: flag("bar description").optionalParam("number"), baz: flag("baz description").optionalParam("array", ["1"]), // with default }) .action(({ flags }) => { /** flags: { foo: string; bar: number | undefined; baz: string[]; }; */ }); ``` CLI usage examples: ```sh # String params example --foo "hello world" example --foo hello # Number params example --foo hello --bar 42 example --foo hello --bar 3.14 example --foo hello --bar -10 # Array params (can be specified multiple times) example --foo hello --baz one example --foo hello --baz one --baz two --baz three # Mixed params example --foo hello --bar 100 --baz item1 --baz item2 ``` ### Negated Flags Sade supports negating flags by supplying `--no-xxx` where `xxx` is the name of the flag. Flag values are `false` by default and passing `--no-xxx` would set the flag value to `false`, hence, this has no effect by default. However, many usecases of negated flags are for flags that should be defaulted to `true`. There are 3 ways to use negated flags in commandstruct. All these 3 methods adds the `--no-xxx` option to the command description but they differ in the default value of the flag. ```ts // using withNegated command("example") .flags({ foo: flag("foo description").withNegated("negated description"), }) .action(({ flags }) => { // flags.foo is true by default (default shown in description) }); ``` ```ts // only negated flag command("example") .flags({ noFoo: flag("turn off foo description"), "no-bar": flag("turn off bar description"), }) .action(({ flags }) => { // flags.foo is true by default (default not shown in description) // flags.bar is true by default (default not shown in description) }); // NOTE: noFoo is converted to kebab-case // if it's case was preserved it would not count as a negated flag. ``` You may not be able to use only negated flag if you enable the `errorOnUnknown` run option. ```ts // both existing flag and negated flag command("example") .flags({ foo: flag("foo description"), noFoo: flag("turn off bar description"), }) .action(({ flags }) => { // flags.foo is false by default (default not shown in description) }); ``` ## Program Flags Use program flags from a command. The program flags can then be accessed in the command action. ```ts const programFlags = { dryRun: flag("run command but do not commit results"), }; const cmd = command("example") .programFlags<typeof programFlags>() .flags({ message: flag("commit message"), }) .action(({ flags }) => { /** flags: { message: boolean; "dry-run": boolean; } */ }); const prog = program("test").flags(programFlags).commands(cmd).build(); ``` ## Dependency Injection Commandstruct is designed to work with [getbox](https://github.com/eriicafes/getbox). Commands receive the Box instance as the second parameter to their action function, allowing them to retrieve dependencies. ```sh npm i getbox ``` ### Example ```ts import { arg, command, program } from "commandstruct"; import { Box } from "getbox"; class Logger { log(message: string) { console.log(`[LOG] ${message}`); } } class UserService { constructor(private logger: Logger) {} static init = Box.init(UserService).get(Logger); createUser(name: string) { this.logger.log(`Creating user: ${name}`); } } const createCmd = command("create") .args({ name: arg() }) .action(({ args }, box) => { // UserService is auto-initialized with Logger dependency const service = box.get(UserService); service.createUser(args.name); }); // The program creates a default Box instance const prog = program("user-utils").commands(createCmd).build(); // You can provide your own Box instance const box = new Box(); const prog2 = program("user-utils", box).commands(createCmd).build(); ``` ## Run Run executes any sade program with some useful options. ```ts const prog = program("test") .flags(/** program flags */) .commands(/** commands */) .build(); run(prog.program()); ``` If you like to configure all available options. ```ts run( prog.program(), { errorOnUnknown(flag) { return `flag not supported '${flag}'`; }, onError(err) { console.error(err); }, }, process.argv, ); ``` ### Run Options - errorOnUnknown: by default unknown options/flags are allowed, set to true to return a default error message or pass a function that returns a custom error message when an unknown option/flag is encountered. - onError: catch error thrown in the executing command before the program terminates. As a convenience, a run methods exists on the commandstruct program itself. ```ts const prog = program("test") .flags(/** program flags */) .commands(/** commands */) .build(); prog.run(); ``` ## Incremental Adoption Commandstruct can be added to an existing sade program. ```ts import sade from "sade"; import { Box } from "getbox"; import { command, flag, run } from "commandstruct"; const prog = sade("notgit") .command("push <repo> [branch]") .describe("push changes") .action((repo, branch, opts) => { console.log("pushing repo", repo, "at", branch || "HEAD"); }); const cmd = command("commit") .describe("make a commit") .flags({ message: flag("commit message").char("m").requiredParam("string"), }) .action(({ flags }) => { console.log("committing with message", flags.message); }); const box = new Box(); cmd.command(box, { program: prog, programFlags: {} }); run(prog); ``` A commandstruct program can also be further modified just like a regular sade program. ```ts import { command, program, flag, run } from "commandstruct"; const cmd = command("commit") .describe("make a commit") .flags({ message: flag("commit message").char("m").requiredParam("string"), }) .action(({ flags }) => { console.log("committing with message", flags.message); }); const prog = program("notgit").commands(cmd).build().program(); // returns a new sade instance prog .command("push <repo> [branch]") .describe("push changes") .action((repo, branch, opts) => { console.log("pushing repo", repo, "at", branch || "HEAD"); }); run(prog); ```