🛠️ Typesafe and modular CLIs with 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.
Commandstruct has both sade and getbox as peer-dependencies.
npm i commandstructimport { 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();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();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.
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();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.
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 */
});A command defines its flags, actions and also additional subcommands if needed.
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 */
});You can make any command or subcommand the default command. This will execute that command when no command was passed to the program.
import { commitCmd, pushCmd } from "./commands";
const prog = program("nogit")
.commands(commitCmd, pushCmd)
.default(commitCmd) // commitCmd will be executed by default
.build();Positional arguments are declared in the order they should be provided. Optional arguments must be placed after required arguments.
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.
By default flags are boolean values and default to false. Flag keys are also converted to kebab-case except when explicitly disabled using .preserveCase().
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;
}
*/
});Allow passing flags with short character.
command("example")
.flags({
foo: flag("foo description").char("f"),
})
.action(({ flags }) => {
// flags.foo === flags.f
});Accept values with flags.
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:
# 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 item2Sade 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.
// using withNegated
command("example")
.flags({
foo: flag("foo description").withNegated("negated description"),
})
.action(({ flags }) => {
// flags.foo is true by default (default shown in description)
});// 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.
// 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)
});Use program flags from a command. The program flags can then be accessed in the command action.
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();Commandstruct is designed to work with getbox. Commands receive the Box instance as the second parameter to their action function, allowing them to retrieve dependencies.
npm i getboximport { 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 executes any sade program with some useful options.
const prog = program("test")
.flags(/** program flags */)
.commands(/** commands */)
.build();
run(prog.program());If you like to configure all available options.
run(
prog.program(),
{
errorOnUnknown(flag) {
return `flag not supported '${flag}'`;
},
onError(err) {
console.error(err);
},
},
process.argv,
);-
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.
const prog = program("test")
.flags(/** program flags */)
.commands(/** commands */)
.build();
prog.run();Commandstruct can be added to an existing sade program.
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.
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);