Skip to content

Commit

Permalink
feat(args): update cliApp() to support command context extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Dec 14, 2023
1 parent 72077ad commit 61d9fb8
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 23 deletions.
35 changes: 24 additions & 11 deletions packages/args/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,22 @@ export class Tuple<T> implements IDeref<T[]> {
}
}

export interface CLIAppConfig<OPTS extends object> {
export interface CLIAppConfig<
OPTS extends object,
CTX extends CommandCtx<OPTS, OPTS> = CommandCtx<OPTS, OPTS>
> {
/**
* App (CLI command) short name.
*/
name: string;
/**
* Shared args for all commands
*/
opts: Args<OPTS>;
/**
* Command spec registry
*/
commands: IObjectOf<Command<any, OPTS>>;
commands: IObjectOf<Command<any, OPTS, CTX>>;
/**
* 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
Expand All @@ -228,26 +235,32 @@ export interface CLIAppConfig<OPTS extends object> {
*/
argv?: string[];
/**
* Lifecycle hook. Function which will be called just before the actual
* command handler, e.g. for setup/config purposes.
* {@link CommandCtx} augmentation handler, i.e. an async function which
* will be called just before the actual command for additional setup/config
* purposes. The context object returned will be the one passed to the
* command.
*/
pre?: Fn2<CommandCtx<OPTS, OPTS>, Command<any, OPTS>, Promise<void>>;
ctx: Fn2<CommandCtx<OPTS, OPTS>, Command<any, OPTS, CTX>, Promise<CTX>>;
/**
* Lifecycle hook. Function which will be called just after the actual
* command handler, e.g. for teardown purposes.
*/
post?: Fn2<CommandCtx<OPTS, OPTS>, Command<any, OPTS>, Promise<void>>;
post?: Fn2<CTX, Command<any, OPTS, CTX>, Promise<void>>;
}

export interface Command<T extends BASE, BASE extends object> {
export interface Command<
OPTS extends BASE,
BASE extends object,
CTX extends CommandCtx<OPTS, BASE> = CommandCtx<OPTS, BASE>
> {
/**
* Command description (short, single line)
*/
desc: string;
/**
* Command specific CLI arg specs
*/
opts: Args<Omit<T, keyof BASE>>;
opts: Args<Omit<OPTS, keyof BASE>>;
/**
* Number of required rest input value (after all parsed options). Leave
* unset to allow any number.
Expand All @@ -256,10 +269,10 @@ export interface Command<T extends BASE, BASE extends object> {
/**
* Actual command function/implementation.
*/
fn: Fn<CommandCtx<T, BASE>, Promise<void>>;
fn: Fn<CTX, Promise<void>>;
}

export interface CommandCtx<T extends BASE, BASE extends object> {
export interface CommandCtx<OPTS extends BASE, BASE extends object> {
/**
* Logger to be used by all commands. By default uses a console logger with
* log level INFO. Can be customized via {@link CLIAppConfig.pre}.
Expand All @@ -268,7 +281,7 @@ export interface CommandCtx<T extends BASE, BASE extends object> {
/**
* Parsed CLI args (according to provided command spec)
*/
opts: T;
opts: OPTS;
/**
* Array of remaining CLI args (after parsed options). Individual commands
* can specify the number of items required via {@link Command.inputs}.
Expand Down
31 changes: 19 additions & 12 deletions packages/args/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ 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>) => {
export const cliApp = async <
OPTS extends object,
CTX extends CommandCtx<OPTS, OPTS>
>(
config: CLIAppConfig<OPTS, CTX>
) => {
const argv = config.argv || process.argv.slice(2);
let usageOpts = {
prefix: "",
Expand All @@ -15,7 +20,7 @@ export const cliApp = async <T extends object>(config: CLIAppConfig<T>) => {
};
try {
let cmdID: string;
let cmd: Command<any, T>;
let cmd: Command<any, OPTS, CTX>;
let start = 0;
if (config.single) {
// single command mode, use 1st available name
Expand All @@ -29,7 +34,7 @@ export const cliApp = async <T extends object>(config: CLIAppConfig<T>) => {
usageOpts.prefix += __descriptions(config.commands);
if (!cmd) __usageAndExit(config, usageOpts);
}
const parsed = parse<T>({ ...config.opts, ...cmd.opts }, argv, {
const parsed = parse<OPTS>({ ...config.opts, ...cmd.opts }, argv, {
showUsage: false,
usageOpts,
start,
Expand All @@ -39,12 +44,14 @@ export const cliApp = async <T extends object>(config: CLIAppConfig<T>) => {
process.stderr.write(`expected ${cmd.inputs || 0} input(s)\n`);
__usageAndExit(config, usageOpts);
}
const ctx: CommandCtx<any, T> = {
logger: new ConsoleLogger("app", "INFO"),
opts: parsed.result,
inputs: parsed.rest,
};
if (config.pre) await config.pre(ctx, cmd);
const ctx: CTX = await config.ctx(
{
logger: new ConsoleLogger(config.name, "INFO"),
opts: parsed.result,
inputs: parsed.rest,
},
cmd
);
await cmd.fn(ctx);
if (config.post) await config.post(ctx, cmd);
} catch (e) {
Expand All @@ -53,15 +60,15 @@ export const cliApp = async <T extends object>(config: CLIAppConfig<T>) => {
}
};

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

const __descriptions = (commands: IObjectOf<Command<any, any>>) =>
const __descriptions = (commands: IObjectOf<Command<any, any, any>>) =>
[
"\nAvailable commands:\n",
...Object.keys(commands).map(
Expand Down

0 comments on commit 61d9fb8

Please sign in to comment.