Leave CLI as broken

This commit is contained in:
Jesse Wierzbinski 2024-03-10 19:30:26 -10:00
parent b69f20ccf4
commit f4fd16179c
No known key found for this signature in database
6 changed files with 350 additions and 77 deletions

View file

@ -1,10 +1,23 @@
export interface CliParameter {
name: string;
// If not positioned, the argument will need to be called with --name value instead of just value
/* Like -v for --version */
shortName?: string;
/**
* If not positioned, the argument will need to be called with --name value instead of just value
* @default true
*/
positioned?: boolean;
// Whether the argument needs a value (requires positioned to be false)
/* Whether the argument needs a value (requires positioned to be false) */
needsValue?: boolean;
optional?: true;
type: "string" | "number" | "boolean" | "array";
type: CliParameterType;
description?: string;
}
export enum CliParameterType {
STRING = "string",
NUMBER = "number",
BOOLEAN = "boolean",
ARRAY = "array",
EMPTY = "empty",
}

View file

@ -1,4 +1,4 @@
import type { CliParameter } from "./cli-builder.type";
import { CliParameterType, type CliParameter } from "./cli-builder.type";
import chalk from "chalk";
export function startsWithArray(fullArray: any[], startArray: any[]) {
@ -193,21 +193,21 @@ export class CliBuilder {
if (value instanceof CliCommand) {
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}|${chalk.underline(value.description)}\n`;
const positionedArgs = value.argTypes.filter(
arg => arg.positioned
arg => arg.positioned ?? true
);
const unpositionedArgs = value.argTypes.filter(
arg => !arg.positioned
arg => !(arg.positioned ?? true)
);
for (const arg of unpositionedArgs) {
for (const arg of positionedArgs) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.green(
arg.name
)}|${
arg.description ?? "(no description)"
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
}
for (const arg of positionedArgs) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}|${
for (const arg of unpositionedArgs) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}${arg.shortName ? ", " + chalk.yellow("-" + arg.shortName) : ""}|${
arg.description ?? "(no description)"
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
}
@ -253,15 +253,38 @@ export class CliBuilder {
}
}
/* type CliParametersToType<T extends CliParameter[]> = {
[K in T[number]["name"]]: T[number]["type"] extends CliParameterType.STRING
? string
: T[number]["type"] extends CliParameterType.NUMBER
? number
: T[number]["type"] extends CliParameterType.BOOLEAN
? boolean
: T[number]["type"] extends CliParameterType.ARRAY
? string[]
: T[number]["type"] extends CliParameterType.EMPTY
? never
: never;
};
type ExecuteFunction<T extends CliParameter[]> = (
args: CliParametersToType<T>
) => void; */
type ExecuteFunction<T> = (
instance: CliCommand,
args: Partial<T>
) => Promise<void> | void;
/**
* A command that can be executed from the command line
* @param categories Example: `["user", "create"]` for the command `./cli user create --name John`
*/
export class CliCommand {
export class CliCommand<T = any> {
constructor(
public categories: string[],
public argTypes: CliParameter[],
private execute: (args: Record<string, any>) => void,
private execute: ExecuteFunction<T>,
public description?: string,
public example?: string
) {}
@ -271,13 +294,17 @@ export class CliCommand {
* formatted with Chalk and with emojis
*/
displayHelp() {
const positionedArgs = this.argTypes.filter(arg => arg.positioned);
const unpositionedArgs = this.argTypes.filter(arg => !arg.positioned);
const positionedArgs = this.argTypes.filter(
arg => arg.positioned ?? true
);
const unpositionedArgs = this.argTypes.filter(
arg => !(arg.positioned ?? true)
);
const helpMessage = `
${chalk.green("📚 Command:")} ${chalk.yellow(this.categories.join(" "))}
${this.description ? `${chalk.cyan(this.description)}\n` : ""}
${chalk.magenta("🔧 Arguments:")}
${unpositionedArgs
${positionedArgs
.map(
arg =>
`${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${
@ -285,10 +312,10 @@ ${unpositionedArgs
}`
)
.join("\n")}
${positionedArgs
${unpositionedArgs
.map(
arg =>
`--${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${
`--${chalk.bold(arg.name)}${arg.shortName ? `, -${arg.shortName}` : ""}: ${chalk.blue(arg.description ?? "(no description)")} ${
arg.optional ? chalk.gray("(optional)") : ""
}`
)
@ -328,6 +355,20 @@ ${positionedArgs
i++;
currentParameter = null;
}
} else if (arg.startsWith("-")) {
const shortName = arg.substring(1);
const argType = this.argTypes.find(
argType => argType.shortName === shortName
);
if (argType && !argType.needsValue) {
parsedArgs[argType.name] = true;
} else if (argType && argType.needsValue) {
parsedArgs[argType.name] = this.castArgValue(
argsWithoutCategories[i + 1],
argType.type
);
i++;
}
} else if (currentParameter) {
parsedArgs[currentParameter.name] = this.castArgValue(
arg,
@ -352,13 +393,13 @@ ${positionedArgs
private castArgValue(value: string, type: CliParameter["type"]): any {
switch (type) {
case "string":
case CliParameterType.STRING:
return value;
case "number":
case CliParameterType.NUMBER:
return Number(value);
case "boolean":
case CliParameterType.BOOLEAN:
return value === "true";
case "array":
case CliParameterType.ARRAY:
return value.split(",");
default:
return value;
@ -370,6 +411,6 @@ ${positionedArgs
*/
run(argsWithoutCategories: string[]) {
const args = this.parseArgs(argsWithoutCategories);
this.execute(args);
void this.execute(this, args as any);
}
}

View file

@ -2,6 +2,7 @@
import { CliCommand, CliBuilder, startsWithArray } from "..";
import { describe, beforeEach, it, expect, jest, spyOn } from "bun:test";
import stripAnsi from "strip-ansi";
import { CliParameterType } from "../cli-builder.type";
describe("startsWithArray", () => {
it("should return true when fullArray starts with startArray", () => {
@ -36,10 +37,27 @@ describe("CliCommand", () => {
cliCommand = new CliCommand(
["category1", "category2"],
[
{ name: "arg1", type: "string", needsValue: true },
{ name: "arg2", type: "number", needsValue: true },
{ name: "arg3", type: "boolean", needsValue: false },
{ name: "arg4", type: "array", needsValue: true },
{
name: "arg1",
type: CliParameterType.STRING,
needsValue: true,
},
{
name: "arg2",
shortName: "a",
type: CliParameterType.NUMBER,
needsValue: true,
},
{
name: "arg3",
type: CliParameterType.BOOLEAN,
needsValue: false,
},
{
name: "arg4",
type: CliParameterType.ARRAY,
needsValue: true,
},
],
() => {
// Do nothing
@ -65,13 +83,34 @@ describe("CliCommand", () => {
});
});
it("should cast argument values correctly", () => {
expect(cliCommand["castArgValue"]("42", "number")).toBe(42);
expect(cliCommand["castArgValue"]("true", "boolean")).toBe(true);
expect(cliCommand["castArgValue"]("value1,value2", "array")).toEqual([
it("should parse short names for arguments too", () => {
const args = cliCommand["parseArgs"]([
"--arg1",
"value1",
"value2",
"-a",
"42",
"--arg3",
"--arg4",
"value1,value2",
]);
expect(args).toEqual({
arg1: "value1",
arg2: 42,
arg3: true,
arg4: ["value1", "value2"],
});
});
it("should cast argument values correctly", () => {
expect(cliCommand["castArgValue"]("42", CliParameterType.NUMBER)).toBe(
42
);
expect(
cliCommand["castArgValue"]("true", CliParameterType.BOOLEAN)
).toBe(true);
expect(
cliCommand["castArgValue"]("value1,value2", CliParameterType.ARRAY)
).toEqual(["value1", "value2"]);
});
it("should run the execute function with the parsed parameters", () => {
@ -79,10 +118,26 @@ describe("CliCommand", () => {
cliCommand = new CliCommand(
["category1", "category2"],
[
{ name: "arg1", type: "string", needsValue: true },
{ name: "arg2", type: "number", needsValue: true },
{ name: "arg3", type: "boolean", needsValue: false },
{ name: "arg4", type: "array", needsValue: true },
{
name: "arg1",
type: CliParameterType.STRING,
needsValue: true,
},
{
name: "arg2",
type: CliParameterType.NUMBER,
needsValue: true,
},
{
name: "arg3",
type: CliParameterType.BOOLEAN,
needsValue: false,
},
{
name: "arg4",
type: CliParameterType.ARRAY,
needsValue: true,
},
],
mockExecute
);
@ -109,13 +164,29 @@ describe("CliCommand", () => {
cliCommand = new CliCommand(
["category1", "category2"],
[
{ name: "arg1", type: "string", needsValue: true },
{ name: "arg2", type: "number", needsValue: true },
{ name: "arg3", type: "boolean", needsValue: false },
{ name: "arg4", type: "array", needsValue: true },
{
name: "arg1",
type: CliParameterType.STRING,
needsValue: true,
},
{
name: "arg2",
type: CliParameterType.NUMBER,
needsValue: true,
},
{
name: "arg3",
type: CliParameterType.BOOLEAN,
needsValue: false,
},
{
name: "arg4",
type: CliParameterType.ARRAY,
needsValue: true,
},
{
name: "arg5",
type: "string",
type: CliParameterType.STRING,
needsValue: true,
positioned: true,
},
@ -153,31 +224,31 @@ describe("CliCommand", () => {
[
{
name: "arg1",
type: "string",
type: CliParameterType.STRING,
needsValue: true,
description: "Argument 1",
optional: true,
},
{
name: "arg2",
type: "number",
type: CliParameterType.NUMBER,
needsValue: true,
description: "Argument 2",
},
{
name: "arg3",
type: "boolean",
type: CliParameterType.BOOLEAN,
needsValue: false,
description: "Argument 3",
optional: true,
positioned: true,
positioned: false,
},
{
name: "arg4",
type: "array",
type: CliParameterType.ARRAY,
needsValue: true,
description: "Argument 4",
positioned: true,
positioned: false,
},
],
() => {
@ -260,7 +331,7 @@ describe("CliBuilder", () => {
[
{
name: "arg1",
type: "string",
type: CliParameterType.STRING,
needsValue: true,
positioned: false,
},
@ -352,20 +423,28 @@ describe("CliBuilder", () => {
[
{
name: "name",
type: "string",
type: CliParameterType.STRING,
needsValue: true,
description: "Name of new item",
},
{
name: "delete-previous",
type: "number",
type: CliParameterType.NUMBER,
needsValue: false,
positioned: true,
positioned: false,
optional: true,
description: "Also delete the previous item",
},
{ name: "arg3", type: "boolean", needsValue: false },
{ name: "arg4", type: "array", needsValue: true },
{
name: "arg3",
type: CliParameterType.BOOLEAN,
needsValue: false,
},
{
name: "arg4",
type: CliParameterType.ARRAY,
needsValue: true,
},
],
() => {
// Do nothing