Skip to content

Commit

Permalink
feat(args): add cliApp() runner
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Dec 13, 2023
1 parent 22e36fa commit b2248fa
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 0 deletions.
53 changes: 53 additions & 0 deletions packages/args/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Fn, IDeref, IObjectOf } from "@thi.ng/api";
import type { ILogger } from "@thi.ng/logger";

export interface ArgSpecBase {
/**
Expand Down Expand Up @@ -196,3 +197,55 @@ export class Tuple<T> implements IDeref<T[]> {
return this.value;
}
}

export interface CLIAppConfig<OPTS extends object> {
/**
* Shared args for all commands
*/
opts: Args<OPTS>;
/**
* Command spec registry
*/
commands: IObjectOf<Command<any, OPTS>>;
/**
* If true, the app will only use the single command entry in
* {@link CLIAppConfig.commands} and not expect the first CLI args to be a
* command name.
*/
single?: boolean;
/**
* Usage options, same as {@link UsageOpts}. Usage will be shown
* automatically in case of arg parse errors.
*/
usage: Partial<UsageOpts>;
/**
* Arguments vector to use for arg parsing. If omitted, uses
* `process.argv.slice(2)`
*/
argv?: string[];
}

export interface Command<T extends BASE, BASE extends object> {
/**
* Command description (short, single line)
*/
desc: string;
/**
* Command specific CLI arg specs
*/
opts: Args<Omit<T, keyof BASE>>;
/**
* Number of required rest input value (after all options)
*/
inputs?: number;
/**
* Actual command function/implementation.
*/
fn: Fn<CommandCtx<T, BASE>, Promise<void>>;
}

export interface CommandCtx<T extends BASE, BASE extends object> {
logger: ILogger;
opts: T;
inputs: string[];
}
67 changes: 67 additions & 0 deletions packages/args/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { illegalArgs } from "@thi.ng/errors";
import { ConsoleLogger } from "@thi.ng/logger/console";
import type { CLIAppConfig, Command, UsageOpts } from "./api.js";
import { parse } from "./parse.js";
import { usage } from "./usage.js";
import { padRight } from "@thi.ng/strings/pad-right";
import type { IObjectOf } from "@thi.ng/api";

export const cliApp = async <T extends object>(config: CLIAppConfig<T>) => {
const argv = config.argv || process.argv.slice(2);
let usageOpts = { prefix: "", ...config.usage };
try {
let cmdID: string;
let cmd: Command<any, T>;
let start = 0;
if (config.single) {
// single command mode, use 1st available name
cmdID = Object.keys(config.commands)[0];
if (!cmdID) illegalArgs("no command provided");
cmd = config.commands[cmdID];
} else {
start = 1;
cmdID = argv[0];
cmd = config.commands[cmdID];
usageOpts.prefix += __descriptions(config.commands);
if (!cmd) __usageAndExit(config, usageOpts);
}
const parsed = parse<T>({ ...config.opts, ...cmd.opts }, argv, {
showUsage: false,
usageOpts,
start,
});
const inputsOk =
parsed && cmd.inputs !== undefined
? cmd.inputs === parsed.rest.length
: true;
if (!(parsed && inputsOk)) {
process.stderr.write(`expected ${cmd.inputs || 0} input(s)\n`);
__usageAndExit(config, usageOpts);
}
await cmd.fn({
logger: new ConsoleLogger("app", "DEBUG"),
opts: parsed!.result,
inputs: parsed!.rest,
});
} catch (e) {
process.stderr.write((<Error>e).message + "\n\n");
__usageAndExit(config, usageOpts);
}
};

const __usageAndExit = <T extends object>(
config: CLIAppConfig<T>,
usageOpts: Partial<UsageOpts>
) => {
process.stderr.write(usage(config.opts, usageOpts));
process.exit(1);
};

const __descriptions = (commands: IObjectOf<Command<any, any>>) =>
[
"\nAvailable commands:\n",
...Object.keys(commands).map(
(x) => `${padRight(16)(x)}: ${commands[x].desc}`
),
"\n\n",
].join("\n");
1 change: 1 addition & 0 deletions packages/args/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./api.js";
export * from "./args.js";
export * from "./cli.js";
export * from "./coerce.js";
export * from "./parse.js";
export * from "./usage.js";

0 comments on commit b2248fa

Please sign in to comment.