An Artisan-style command runner for Node.
Write commands as small TypeScript classes and run them anywhere on your machine,
or scoped to a project.
Globally, so your commands work anywhere:
pnpm add -g @madinco/smithOr into a project, for commands that belong to it:
pnpm add -D @madinco/smithsmith init # creates the command directory for wherever you are
smith init --global # forces the global one, even inside a project
smith init --shim # also writes ./smith so "node smith" works in the projectTwo directories, loaded in this order:
~/.config/smith/commands(override withSMITH_HOME, orXDG_CONFIG_HOME)<project>/src/console/commands, when apackage.jsonis found at or above the current directory
A project command shadows a global one with the same name. Discovery recurses into
subdirectories and skips anything starting with . or _, plus *.test.*.
Point a project somewhere else in its package.json:
{ "smith": { "commands": "console/commands" } }smith make:command greet --description "Say hello to someone"import { Command, argument, flag } from '@madinco/smith';
export class GreetCommand extends Command {
readonly name = 'greet';
readonly description = 'Say hello to someone';
readonly arguments = {
name: argument('Who to greet'),
};
readonly options = {
loud: flag('Shout it'),
};
handle(): number {
const greeting = `Hello, ${this.argument('name')}`;
this.info(this.option('loud') ? greeting.toUpperCase() : greeting);
return Command.SUCCESS;
}
}Within a command, this.project contains the project root, or null when you are not
inside a project. this.cwd contains the directory you ran the command from, which will
differ from the project root when you run smith from a subdirectory.
You may declare the arguments and options a command accepts using the arguments and
options properties. Smith uses these declarations to type the values you read back, and
to build the command's help output.
readonly arguments = {
target: argument('Required'),
branch: optional('main', 'Optional, with a default'),
comment: maybe('Optional, undefined when absent'),
files: rest('Collects the remainder, and must come last'),
};
readonly options = {
quality: number(75, 'A number', 'q'),
name: option('world', 'A string'),
dry: flag('True when present'),
};Each value is typed from its declaration, so this.argument('files') returns a string[]
and this.option('quality') returns a number. Requesting a name you have not declared
will not compile.
Any argument you have not declared is rejected. A mistyped argument, or a glob that matched more files than you expected, will fail rather than being silently ignored.
smith greet world --loudHere, world is this.argument('name') and --loud is this.option('loud').
Running smith on its own lists every command it found. Running smith greet --help
prints that command's description, usage, arguments and options.
line, info, comment, warn, error, newLine, sections for text.
The detail method prints a label and a value separated by a line of dots. The details
method groups several of those rows beneath a heading, and each row may carry a tone of
good or warn to colour its value. When the output is piped, both fall back to
label: value.
spin(label, task) for indeterminate work, progress(total) for a bar. Both write to
stderr and degrade to plain lines when the output is not a TTY.
Return one from handle. Command.SUCCESS (0), Command.FAILURE (1),
Command.INVALID (2). A thrown error becomes FAILURE, an unknown option becomes
INVALID.
examples/ holds runnable commands covering each of these. See its
README.
See CONTRIBUTING.md. Bugs and ideas go in issues. For anything security related, email oss@madinco.com rather than opening an issue.
MIT. See LICENSE.