commandstruct
Version:
Type safe and modular CLIs with Sade
443 lines (434 loc) • 13 kB
JavaScript
import sade from "sade";
import { Box } from "getbox";
//#region src/arg.ts
var Arg = class {
constructor(_type) {
this._type = _type;
}
optional() {
this._type = "optional";
return this;
}
static toObject(arg$1) {
return { type: arg$1._type };
}
static toString(arg$1, name) {
if (arg$1._type === "required") return `<${name}>`;
else return `[${name}]`;
}
};
function arg() {
return new Arg("required");
}
//#endregion
//#region src/errors.ts
var CommandError = class extends Error {
constructor(reason, message) {
super(message);
this.reason = reason;
this.message = message;
}
};
//#endregion
//#region src/flag.ts
var Flag = class Flag {
constructor(_desc, _char, _param, _preserveCase, _negate) {
this._desc = _desc;
this._char = _char;
this._param = _param;
this._preserveCase = _preserveCase;
this._negate = _negate;
}
char(char) {
this._char = char;
return this;
}
requiredParam(type) {
this._param = {
_type: "required",
type
};
return this;
}
optionalParam(type, defaultValue) {
this._param = {
_type: "optional",
type,
defaultValue
};
return this;
}
withNegated(description) {
this._negate = description;
return this;
}
preserveCase() {
this._preserveCase = true;
return this;
}
static toObject(flag$1) {
return {
desc: flag$1._desc,
char: flag$1._char,
param: flag$1._param,
negate: flag$1._negate,
preserveCase: flag$1._preserveCase
};
}
static toString(flag$1, name) {
let str = `--${flag$1._preserveCase ? name : Flag.toKebabCase(name)}`;
if (flag$1._char) str = `-${flag$1._char}, ${str}`;
return str;
}
static toNegatedString(flag$1, name) {
return `--no-${flag$1._preserveCase ? name : Flag.toKebabCase(name)}`;
}
static isNegated(str) {
return str.startsWith("no-");
}
static toKebabCase(str) {
return str.split("").map((char, index) => {
if (index === 0) return char.toLowerCase();
return char.toLowerCase() !== char ? `-${char.toLowerCase()}` : char;
}).join("");
}
};
function flag(description) {
return new Flag(description, void 0, void 0, false, void 0);
}
//#endregion
//#region src/utils.ts
function registerFlags(program$1, flags) {
for (const [name, flag$1] of Object.entries(flags)) {
const flagObj = Flag.toObject(flag$1);
if (flagObj.char !== void 0 && flagObj.char.length !== 1) throw new CommandError("invalid_flag", `option ${"`" + Flag.toString(flag$1, name) + "`"} char must be 1 character`);
if (flagObj.param && flagObj.negate !== void 0) throw new CommandError("invalid_flag", `negated option ${name} ${"`" + Flag.toString(flag$1, name) + "`"} cannot have param`);
const key = flagObj.preserveCase ? name : Flag.toKebabCase(name);
if (Flag.isNegated(key)) {
if (flagObj.param) throw new CommandError("invalid_flag", `negated option ${name} ${"`" + Flag.toString(flag$1, name) + "`"} cannot have param`);
if (flagObj.char !== void 0) throw new CommandError("invalid_flag", `negated option ${name} ${"`" + Flag.toString(flag$1, name) + "`"} cannot have char`);
if (flagObj.negate !== void 0) throw new CommandError("invalid_flag", `negated option ${name} ${"`" + Flag.toString(flag$1, name) + "`"} is already negated`);
const existingKey = flags[key.slice(3)];
if (existingKey && Flag.toObject(existingKey).param) throw new CommandError("invalid_flag", `negated option ${name} ${"`" + Flag.toString(flag$1, name) + "`"} can only be used with boolean flags`);
}
program$1.option(Flag.toString(flag$1, name), flagObj.desc, flagObj.negate && true);
if (flagObj.negate !== void 0) program$1.option(Flag.toNegatedString(flag$1, name), flagObj.negate);
}
}
function commandUsage(baseCmd, cmd, args) {
let usage = cmd;
if (baseCmd) usage = baseCmd + " " + cmd;
let hasOptional = false;
for (const [name, arg$1] of Object.entries(args)) {
const argObj = Arg.toObject(arg$1);
if (argObj.type === "required" && hasOptional) throw new CommandError("invalid_arg", `required positional argument ${"`" + Arg.toString(arg$1, name) + "`"} cannot appear after an optional argument`);
hasOptional ||= argObj.type === "optional";
usage += " " + Arg.toString(arg$1, name);
}
return usage;
}
function commandContext(args, flags, programFlags, fnArgs) {
const mergedFlags = {
...programFlags,
...flags
};
const _args = Object.keys(args).reduce((acc, name, index) => {
acc[name] = fnArgs[index];
return acc;
}, {});
const { _, ...opts } = fnArgs[fnArgs.length - 1];
for (const [name, flag$1] of Object.entries(mergedFlags)) {
const flagObj = Flag.toObject(flag$1);
const key = flagObj.preserveCase ? name : Flag.toKebabCase(name);
const value = opts[key];
if (!flagObj.param) {
if (flagObj.negate || Flag.isNegated(key)) {
const negatedKey = flagObj.negate ? `no-${key}` : key;
delete opts[negatedKey];
}
if (value === void 0) {
if (Flag.isNegated(key)) {
const negatedKey = key.slice(3);
if (opts[negatedKey] === void 0) opts[negatedKey] = true;
continue;
}
opts[key] = false;
if (flagObj.char) opts[flagObj.char] = false;
} else if (typeof value !== "boolean") {
opts[key] = true;
if (flagObj.char) opts[flagObj.char] = true;
}
continue;
}
if (value === void 0) {
if (flagObj.param._type === "optional") {
if (flagObj.param.defaultValue !== void 0) {
opts[key] = flagObj.param.defaultValue;
if (flagObj.char) opts[flagObj.char] = flagObj.param.defaultValue;
}
continue;
}
throw new CommandError("invalid_flag", `option ${"`" + Flag.toString(flag$1, name) + "`"} value is missing`);
}
if (flagObj.param.type === "string" && typeof value !== "string") if (typeof value !== "object") {
if (value === true || value === void 0) throw new CommandError("invalid_flag", `option ${"`" + Flag.toString(flag$1, name) + "`"} value is missing`);
const parsedValue = String(value);
opts[key] = parsedValue;
if (flagObj.char) opts[flagObj.char] = parsedValue;
} else if (Array.isArray(value)) {
const val = value[value.length - 1];
const parsedValue = val === true ? "" : String(val);
opts[key] = parsedValue;
if (flagObj.char) opts[flagObj.char] = parsedValue;
} else throw new CommandError("invalid_flag", `option ${"`" + Flag.toString(flag$1, name) + "`"} value is not a string`);
if (flagObj.param.type === "number" && typeof value !== "number") {
const parsedValue = Number(value);
if (typeof value !== "string" || Number.isNaN(parsedValue)) throw new CommandError("invalid_flag", `option ${"`" + Flag.toString(flag$1, name) + "`"} value is not a number`);
opts[key] = parsedValue;
if (flagObj.char) opts[flagObj.char] = parsedValue;
}
if (flagObj.param.type === "array") if (Array.isArray(value)) {
const parsedValue = value.map((val) => val === true ? "" : String(val));
opts[key] = parsedValue;
if (flagObj.char) opts[flagObj.char] = parsedValue;
} else {
if (typeof value === "object" || value === true) throw new CommandError("invalid_flag", `option ${"`" + Flag.toString(flag$1, name) + "`"} value is not an array`);
const parsedValue = [String(value)];
opts[key] = parsedValue;
if (flagObj.char) opts[flagObj.char] = parsedValue;
}
}
return {
args: _args,
flags: opts,
restArgs: _
};
}
//#endregion
//#region src/command.ts
var Command = class {
constructor(options) {
this.options = options;
}
command(box, options) {
const { program: program$1, programFlags, baseCmd, defaultCmd } = options;
const usage = commandUsage(baseCmd, this.options.name, this.options.args);
const command$1 = program$1.command(usage, this.options.description, { default: defaultCmd === this });
if (this.options.aliases.length) command$1.alias(...this.options.aliases);
for (const example of this.options.examples) command$1.example(example);
registerFlags(command$1, this.options.flags);
command$1.action((...fnArgs) => {
const ctx = commandContext(this.options.args, this.options.flags, programFlags, fnArgs);
return this.options.action(ctx, box);
});
for (const subcommand of this.options.subcommands) subcommand.command(box, {
program: program$1,
programFlags,
baseCmd: baseCmd ? baseCmd + " " + this.options.name : this.options.name,
defaultCmd: defaultCmd === this ? void 0 : defaultCmd
});
}
};
var CommandBuilder = class {
options;
constructor(name) {
this.options = {
name,
description: void 0,
aliases: [],
examples: [],
args: {},
flags: {},
subcommands: []
};
}
describe(description) {
this.options.description = description;
return this;
}
alias(...aliases) {
this.options.aliases.push(...aliases);
return this;
}
example(example) {
this.options.examples.push(example);
return this;
}
args(args) {
this.options.args = args;
return this;
}
flags(flags) {
this.options.flags = flags;
return this;
}
programFlags() {
return this;
}
subcommands(...commands) {
this.options.subcommands.push(...commands);
return this;
}
action(fn) {
return new Command({
...this.options,
action: fn
});
}
};
function command(name) {
return new CommandBuilder(name);
}
//#endregion
//#region src/run.ts
async function run(cli, options, argv = process.argv) {
try {
const output = cli.parse(argv, {
lazy: true,
unknown: options?.errorOnUnknown === true ? () => {} : options?.errorOnUnknown === false ? void 0 : options?.errorOnUnknown
});
if (!output) return;
return await output.handler.apply(null, output.args);
} catch (err) {
if (process.exitCode === void 0) process.exitCode = 1;
if (options?.onError) options.onError(err);
else if (err instanceof CommandError) console.error("error:", err.message);
else throw err;
}
}
//#endregion
//#region src/program.ts
var Program = class {
constructor(box, options) {
this.box = box;
this.options = options;
}
program() {
const program$1 = sade(this.options.name);
if (this.options.version) program$1.version(this.options.version);
if (this.options.description) program$1.describe(this.options.description);
for (const example of this.options.examples) program$1.example(example);
registerFlags(program$1, this.options.flags);
for (const command$1 of this.options.commands) command$1.command(this.box, {
program: program$1,
programFlags: this.options.flags,
defaultCmd: this.options.default
});
return program$1;
}
run(options, argv) {
return run(this.program(), options, argv);
}
};
var ProgramBuilder = class {
options;
constructor(box, name) {
this.box = box;
this.options = {
name,
version: void 0,
description: void 0,
examples: [],
flags: {},
commands: [],
default: void 0
};
}
version(version) {
this.options.version = version;
return this;
}
describe(description) {
this.options.description = description;
return this;
}
example(example) {
this.options.examples.push(example);
return this;
}
flags(flags) {
this.options.flags = flags;
return this;
}
commands(...commands) {
this.options.commands.push(...commands);
return this;
}
default(command$1) {
this.options.default = command$1;
return this;
}
build() {
return new Program(this.box, this.options);
}
};
function program(name, box = new Box()) {
return new ProgramBuilder(box, name);
}
//#endregion
//#region src/single.ts
var SingleProgram = class {
constructor(box, options) {
this.box = box;
this.options = options;
}
program() {
const program$1 = sade(commandUsage(void 0, this.options.name, this.options.args), true);
if (this.options.version) program$1.version(this.options.version);
if (this.options.description) program$1.describe(this.options.description);
for (const example of this.options.examples) program$1.example(example);
registerFlags(program$1, this.options.flags);
return program$1.action((...fnArgs) => {
const ctx = commandContext(this.options.args, this.options.flags, {}, fnArgs);
return this.options.action(ctx, this.box);
});
}
run(options, argv) {
return run(this.program(), options, argv);
}
};
var SingleProgramBuilder = class {
options;
constructor(box, name) {
this.box = box;
this.options = {
name,
version: void 0,
description: void 0,
examples: [],
args: {},
flags: {}
};
}
version(version) {
this.options.version = version;
return this;
}
describe(description) {
this.options.description = description;
return this;
}
example(example) {
this.options.examples.push(example);
return this;
}
args(args) {
this.options.args = args;
return this;
}
flags(flags) {
this.options.flags = flags;
return this;
}
action(fn) {
return new SingleProgram(this.box, {
...this.options,
action: fn
});
}
};
function singleProgram(name, box = new Box()) {
return new SingleProgramBuilder(box, name);
}
//#endregion
export { Arg, Command, CommandError, Flag, Program, SingleProgram, arg, command, flag, program, run, singleProgram };