commandstruct
v0.3.0
Published
Type safe and modular CLIs with Sade
Downloads
16
Maintainers
Readme
Commandstruct
🛠️ 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 Hollywood DI.
Installation
Commandstruct has both sade and hollywood-di as peer-dependencies so make sure to install them along side it.
npm i commandstruct sade hollywood-di
Usage
A Regular Program
import { arg, createProgram, createCommand, flag } from "commandstruct";
const commitCmd = createCommand("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 = createCommand("push")
.describe("push changes")
.args({
repo: arg(),
branch: arg().optional(),
})
.action(({ args }) => {
console.log("pushing repo", args.repo, "at", args.branch || "HEAD");
});
const prog = createProgram("notgit")
.describe("not your regular git")
.flags({ verbose: flag("display extra information on command run") })
.commands(commitCmd, pushCmd)
.build();
prog.run();
A Single Program
import { arg, createSingleProgram, flag } from "commandstruct";
const prog = createSingleProgram("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"
);
if (restArgs.length) console.log("maybe print these", 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.
const prog = createProgram("notgit")
.describe("not your regular git")
.example("notgit commit -m 'my first commit'")
.example("notgit push origin main")
.version("v1.0.0")
.provide({
/** register tokens */
})
.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
.
const prog = createSingleProgram("mycat")
.describe("print contents of file to stdout")
.example("mycat")
.example("git push origin main")
.version("v1.0.0")
.provide({
/** register tokens */
})
.args({
/** program args */
})
.flags({
/** program flags */
})
.action(() => {
/** program action */
});
Command
A command defines its flags
, actions
and also additional subcommands
if needed.
const programFlags = {
/** program flags */
};
const cmd = createCommand("commit")
.describe("make a commit")
.alias("cm", "com")
.alias("comit")
.example("git commit -m 'my first commit'")
.example("git cm -m 'another commit")
.useFlags<typeof programFlags>()
.use<{
/** dependencies */
}>()
.provide({
/** register tokens */
})
.subcommands(/** subcommands */)
.args({
/** command args */
})
.flags({
/** command flags */
})
.action(({ args, flags, restArgs }, container) => {
/** command action */
});
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.
import { commitCmd, pushCmd } from "./commands";
const prog = createProgram("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.
createCommand("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()
.
createCommand("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.
createCommand("example")
.flags({
foo: flag("foo description").char("f"),
})
.action(({ flags }) => {
// flags.foo === flags.f
});
Flag Params
Accept values with flags.
createCommand("example")
.flags({
foo: flag("foo description").requiredParam("string"),
bar: flag("bar description").optionalParam("number"),
baz: flag("baz description").optionalParam("array", ["1"]), // with default
})
.action(({ flag }) => {
/**
flags: {
foo: string;
bar: number | undefined;
baz: string[];
};
*/
});
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.
// using withNegated
createCommand("example")
.flags({
foo: flag("foo description").withNegated("negated description"),
})
.action(({ flags }) => {
// flags.foo is true by default (default shown in description)
});
// only negated flag
createCommand("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
createCommand("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 Flags
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 = createCommand("example")
.useFlags<typeof programFlags>()
.flags({
message: flag("commit message"),
})
.action(({ flags }) => {
/**
flags: {
message: boolean;
"dry-run": boolean;
}
*/
});
const prog = createProgram("test").flags(programFlags).commands(cmd).build();
Use
Define command dependencies. The command can then only be added as a command or subcommand to a program/command that satisfies it's dependencies. The command dependencies can be accessed in the command action.
You can only call use
once and only before calling provide
.
interface Counter {
count: number;
increment(): void;
}
const cmd = createCommand("commit")
.use<{ counter: Counter }>()
.action((ctx, container) => {
/**
container: {
counter: Counter;
}
*/
});
class LinearCounter {
public count = 0;
public increment() {
this.count++;
}
}
const prog = createProgram("test")
.flags(programFlags)
.provide({ counter: LinearCounter })
.commands(cmd)
.build();
Provide
Provide creates a new child container. Registered tokens can then be used in the commmand action and in futher calls to provide
. See more about register tokens in Hollywood DI.
import { defineInit } from "hollywood-di";
import { createCommand } from "commandstruct";
class Foo {}
class Bar {
public static init = defineInit(Bar).args("foo");
constructor(public foo: Foo) {}
}
const cmd = createCommand("example")
.provide({
foo: Foo,
bar: Bar,
})
.action((ctx, container) => {
/**
container: {
foo: Foo;
bar: Bar;
}
*/
});
Run
Run executes any sade program with some useful options.
const prog = createProgram("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
);
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.
const prog = createProgram("test")
.flags(/** program flags */)
.commands(/** commands */)
.build();
prog.run();
Dependency Injection
At the core commandstruct is designed to work with Hollywood DI. Commands can define their dependencies using use
, programs and commands can register new tokens using provide
which creates a child container.
A root container can also be passed to the program.
import { Hollywood, defineInit } from "hollywood-di";
import { createCommand, createProgram } from "commandstruct";
class Foo {}
class Bar {
public static init = defineInit(Bar).args("foo");
constructor(public foo: Foo) {}
}
const cmd = createCommand("example")
.use<{ foo: Foo }>()
.provide({ bar: Bar })
.action((ctx, container) => {
/**
container: {
bar: Bar;
foo: Foo;
}
*/
});
const container = Hollywood.create({
foo: Foo,
});
const prog = createProgram("test", container).commands(cmd).build();
Incremental Adoption
Commandstruct can be added to an existing sade program.
import sade from "sade";
import { createCommand, 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 = createCommand("commit")
.describe("make a commit")
.flags({
message: flag("commit message").char("m").requiredParam("string"),
})
.action(({ flags }) => {
console.log("committing with message", flags.message);
});
cmd.command({ program: prog, programFlags: {}, container: undefined });
run(prog);
A commandstruct program can also be further modified just like a regular sade program.
import { createCommand, createProgram, flag, run } from "commandstruct";
const cmd = createCommand("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 = createProgram("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);