Skip to content

Create A Command

The command feature helps you write CLIs that are simple to call and solid to maintain. You describe what the command accepts, the library reads process arguments, validates inputs, generates help, and calls your handler with already typed values.

Principle

A command is made of three parts:

  • options, such as --port 3000 or --verbose;
  • subjects, meaning either positional arguments after options, or subcommands;
  • a handler, called only when parsing succeeds.

Options and arguments rely on DataParser from @duplojs/utils. This turns CLI input, which always starts as text, into typed domain values.

A First Command

ts
import { 
SC
} from "@duplojs/server-utils";
import {
DP
} from "@duplojs/utils";
await
SC
.
exec
(
{
description
: "Search text in a file",
options
: [
SC
.
createBooleanOption
(
"ignore-case", {
aliases
: ["i"],
description
: "Ignore letter case",
}, ), ],
subjects
: [
SC
.
createArgument
("pattern",
DP
.
string
()),
SC
.
createArgument
("filePath",
DP
.
string
()),
], }, ({
options
,
args
: {
pattern
,
filePath
} }) => {
const
sensitivity
=
options
["ignore-case"] ? "case-insensitive" : "case-sensitive";
console
.
log
(`search "${
pattern
}" in ${
filePath
} (${
sensitivity
})`);
}, );

Here, exec creates the root command. The subjects list declares two positional arguments (pattern and filePath), and parsed values are exposed as args.pattern and args.filePath.

grep-like
$ grep-like "TODO" ./src/index.ts --ignore-case search "TODO" in ./src/index.ts (case-insensitive)

Add Options

createBooleanOption creates a flag. It is true when the option is present and false otherwise.

createOption parses a single value. With required: true, the handler always receives a value.

createArrayOption parses a list of values and can enforce a minimum, a maximum, or a custom separator.

ts
import { 
SC
} from "@duplojs/server-utils";
import {
DP
} from "@duplojs/utils";
await
SC
.
exec
(
{
options
: [
SC
.
createOption
(
"type",
DP
.
literal
(["file", "directory"]),
{
required
: true,
aliases
: ["t"],
description
: "Expected resource type",
}, ),
SC
.
createArrayOption
(
"name",
DP
.
string
(),
{
separator
: ",",
description
: "Accepted resource names",
}, ), ], }, ({
options
}) => {
console
.
log
(
options
.
type
);
console
.
log
(
options
.
name
?? []);
}, );

The type of options.type becomes "file" | "directory". If the received value does not match the parser, the command does not call the handler.

Understand Errors

When parsing fails, exec prints an interpreted error and exits with code 1. The error shows the command path, the failing option/argument, the received value, and what was expected.

find-like
$ find-like --type socketCommand failedCOMMAND: rootOPTION: --type expected file | directory but received "socket"

This is useful for internal tools: you can keep strict contracts without writing diagnostic messages yourself.

Subcommands

For a CLI with several actions, create commands with SC.create, then pass them as subjects of a parent command.

ts
import { 
SC
} from "@duplojs/server-utils";
import {
DP
} from "@duplojs/utils";
const
installCommand
=
SC
.
create
(
"install", {
description
: "Install a package",
options
: [
SC
.
createBooleanOption
(
"yes", {
aliases
: ["y"],
description
: "Answer yes to prompts",
}, ), ],
subjects
: [
SC
.
createArgument
("packageName",
DP
.
string
())],
}, ({
options
,
args
: {
packageName
} }) => {
console
.
log
(`install ${
packageName
}${
options
.
yes
? " without prompt" : ""}`);
}, ); await
SC
.
exec
(
{
description
: "Package manager",
subjects
: [
installCommand
],
}, () => {
console
.
log
("select a package command");
}, );
apt-like
$ apt-like install typescript --yes install typescript without prompt

Each subcommand can have its own description, options, argument subjects, and handler. Help follows the command tree.

Generated Help

--help and -h are automatically available. The output is built from declared descriptions, aliases, options, and subjects.

apt-like
$ apt-like --helpCOMMAND: rootDESCRIPTION:Package managerCOMMAND: installDESCRIPTION:Install a packageOPTIONS:- yes: -y, --yesAnswer yes to promptsARGUMENTS: <packageName>- packageName: string

This makes command and option descriptions important: they become the embedded documentation of your tool.

Lightweight Version With execOptions

When your script has no subcommands and no positional argument, execOptions is enough. It only parses process options and returns a typed object.

ts
import { 
SC
} from "@duplojs/server-utils";
import {
DP
} from "@duplojs/utils";
const
options
= await
SC
.
execOptions
(
SC
.
createOption
(
"port",
DP
.
coerce
.
number
(),
{
required
: true,
description
: "HTTP port",
}, ),
SC
.
createBooleanOption
(
"reload", {
aliases
: ["r"],
description
: "Restart on file change",
}, ), ); if (
options
.
reload
) {
console
.
log
(`server listens on ${
options
.
port
} with reload`);
}
server-script
$ server-script --port 3000 --reload server listens on 3000 with reload

Use execOptions for minimal scripts. Move to exec as soon as you have positional arguments, subcommands, or a real CLI experience to expose.

Released under the MIT license.