Skip to content

Commit 2b5a0cd

Browse files
committed
find/replace Task -> Command
1 parent 786d99f commit 2b5a0cd

File tree

6 files changed

+91
-60
lines changed

6 files changed

+91
-60
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ git clone https://github.com/trvswgnr/cmdctr.git
3535

3636
## Usage
3737

38-
Command Center provides three main functions: `CmdCtr`, `Data`, and `Task`.
38+
Command Center provides three main functions: `CmdCtr`, `Data`, and `Command`.
3939

4040
### CmdCtr
4141

@@ -73,13 +73,13 @@ const task1Data = Data({
7373
});
7474
```
7575

76-
### Task
76+
### Command
7777

78-
`Task` creates a new task. It takes a data object and an action function as arguments. The action
78+
`Command` creates a new task. It takes a data object and an action function as arguments. The action
7979
function is what will be executed when the task is run.
8080

8181
```ts
82-
const task1 = Task(task1Data, (opts) => {
82+
const task1 = Command(task1Data, (opts) => {
8383
const { input, output } = opts;
8484
console.log(`input: ${input}`);
8585
console.log(`output: ${output}`);
@@ -88,7 +88,7 @@ const task1 = Task(task1Data, (opts) => {
8888

8989
A nice feature here is the options passed to the action function (`opts` here) are validated from the CLI and their types are known at compile-time. This means you get meaningfull type hints and code completion in your editor and can be sure that the arguments are the types you're expecting.
9090

91-
### Registering and Running Tasks
91+
### Registering and Running Commands
9292

9393
After creating tasks, you can register them to the command center using the `register` method. Then,
9494
you can run the tasks using the `run` method.
@@ -116,7 +116,7 @@ Here is a complete example of how to use Command Center:
116116

117117
```ts
118118
// @ts-check
119-
import { CmdCtr, Data, Task } from "cmdctr";
119+
import { CmdCtr, Data, Command } from "cmdctr";
120120
import ora from "ora"; // loading spinner (for funzies)
121121

122122
const cmdCtr = CmdCtr("example"); // or new CmdCtr(), if that's your thing
@@ -140,7 +140,7 @@ const task1Data = Data({
140140
},
141141
});
142142

143-
const task1 = Task(task1Data, (opts) => {
143+
const task1 = Command(task1Data, (opts) => {
144144
const { input, output } = opts;
145145
console.log(`input: ${input}`);
146146
console.log(`output: ${output}`);
@@ -165,7 +165,7 @@ const task2Data = Data({
165165
},
166166
});
167167

168-
const task2 = Task(task2Data, async (opts) => {
168+
const task2 = Command(task2Data, async (opts) => {
169169
const { message, loud } = opts;
170170
const loadingMsg = "...what was i saying again?";
171171
const spinner = ora(loadingMsg).start();

cmdctr.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
import type {
22
CmdCtrInstance,
33
DataInstance,
4-
TaskInstance,
4+
CommandInstance,
55
CmdCtrConstructor,
66
DataConstructor,
7-
TaskConstructor,
7+
CommandConstructor,
88
Strict,
99
Action,
1010
CmdCtrFn,
11-
RegisteredTasks,
11+
RegisteredCommands,
1212
DataFn,
13-
TaskFn,
13+
CommandFn,
1414
} from "./types";
1515
import { errExit, getCliArgs, getValidatedOpts } from "./lib";
1616
import { DEFAULT_TASK, TASK_NAME } from "./constants";
1717

1818
const _CmdCtr: CmdCtrFn = (baseCommand) => {
19-
const tasks: RegisteredTasks = new Map();
19+
const tasks: RegisteredCommands = new Map();
2020
let data = typeof baseCommand === "string" ? tasks.get(baseCommand) : baseCommand;
2121
if (!data) return errExit`unknown task "${baseCommand}"`;
2222
tasks.set(DEFAULT_TASK, { ...data, isDefault: true });
2323
const name = typeof baseCommand === "string" ? baseCommand : data.name;
2424
const self: CmdCtrInstance = {
25-
register: (task: TaskInstance) => tasks.set(task.name, task),
25+
register: (task: CommandInstance) => tasks.set(task.name, task),
2626
run: (_args?: string[]) => {
2727
if (tasks.size === 0) return errExit`no tasks registered`;
2828
const args = getCliArgs(tasks, name, _args);
29-
const taskName = args.usingDefaultTask ? DEFAULT_TASK : args[TASK_NAME];
29+
const taskName = args.usingDefaultCommand ? DEFAULT_TASK : args[TASK_NAME];
3030
const data = tasks.get(taskName);
3131
if (!data) return errExit`unknown task "${taskName}"`;
3232
data.action(getValidatedOpts(data, args));
@@ -37,11 +37,16 @@ const _CmdCtr: CmdCtrFn = (baseCommand) => {
3737

3838
const _Data: DataFn = <const D extends DataInstance>(data: Strict<D, DataInstance>) => data as D;
3939

40-
const _Task: TaskFn = <const D extends DataInstance>(data: D, action: Action<D>) => ({
41-
...data,
42-
action: (validatedOpts: any) => action(validatedOpts),
43-
});
40+
const _Command: CommandFn = <const D extends DataInstance>(data: D, action: Action<D>) => {
41+
const registeredCommands: RegisteredCommands = new Map();
42+
return {
43+
...data,
44+
action: (validatedOpts: any) => action(validatedOpts),
45+
register: (cmd: CommandInstance) => registeredCommands.set(cmd.name, cmd),
46+
registeredCommands,
47+
};
48+
};
4449

4550
export const CmdCtr = _CmdCtr as CmdCtrConstructor;
4651
export const Data = _Data as DataConstructor;
47-
export const Task = _Task as TaskConstructor;
52+
export const Command = _Command as CommandConstructor;

examples/basic.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @ts-check
2-
import { CmdCtr, Data, Task } from "cmdctr";
2+
import { CmdCtr, Data, Command } from "cmdctr";
33
import ora from "ora"; // loading spinner (for funzies)
44

55
const cmdCtr = CmdCtr("example"); // or new CmdCtr(), if that's your thing
@@ -23,7 +23,7 @@ const task1Data = Data({
2323
},
2424
});
2525

26-
const task1 = Task(task1Data, (opts) => {
26+
const task1 = Command(task1Data, (opts) => {
2727
const { input, output } = opts;
2828
console.log(`input: ${input}`);
2929
console.log(`output: ${output}`);
@@ -48,7 +48,7 @@ const task2Data = Data({
4848
},
4949
});
5050

51-
const task2 = Task(task2Data, async (opts) => {
51+
const task2 = Command(task2Data, async (opts) => {
5252
const { message, loud } = opts;
5353
const loadingMsg = "...what was i saying again?";
5454
const spinner = ora(loadingMsg).start();

index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ export type {
22
CmdCtrConstructor,
33
DataInstance,
44
DataConstructor,
5-
TaskInstance,
6-
TaskConstructor,
5+
CommandInstance,
6+
CommandConstructor,
77
Action,
8-
RegisteredTasks,
9-
TaskOption,
8+
RegisteredCommands,
9+
CommandOption,
1010
} from "./types";
1111
export * from "./cmdctr";

lib.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
11
import { parseArgs } from "node:util";
2-
import type { CliArgs, RegisteredTasks, TaskOption } from "./types";
2+
import type { CliArgs, RegisteredCommands, CommandOption } from "./types";
33
import { DEFAULT_TASK, ParseError, TASK_NAME } from "./constants";
44

5-
export function getCliArgs(tasks: RegisteredTasks, name: string, _args?: string[]): CliArgs {
5+
export function getCliArgs(tasks: RegisteredCommands, name: string, _args?: string[]): CliArgs {
66
const rawArgs = _args ?? process.argv.slice(2);
7-
let usageBase = `\nUsage: ${name} <task> <options>\nTasks:\n`;
7+
let usageBase = `\nUsage: ${name} <task> <options>\nCommands:\n`;
88
const tasksList = [...tasks.values()]
99
.map((task) => ` ${task.name}: ${task.description}`)
1010
.join("\n");
1111
let usage = usageBase + tasksList;
12-
let taskNameRaw = rawArgs[0];
13-
let usingDefaultTask = false;
12+
let taskNameRaw = tryToGetTaskName(rawArgs, tasks);
13+
let usingDefaultCommand = false;
1414
if (!taskNameRaw) {
1515
if (!tasks.has(DEFAULT_TASK)) {
1616
return errExit`missing task\n${usage}`;
1717
}
1818
taskNameRaw = tasks.get(DEFAULT_TASK)!.name;
19-
usingDefaultTask = true;
19+
usingDefaultCommand = true;
2020
}
2121
let taskName = taskNameRaw ?? "";
2222
if (!tasks.has(taskName)) {
2323
if (!tasks.has(DEFAULT_TASK)) {
2424
return errExit`missing task\n${usage}`;
2525
}
2626
taskName = tasks.get(DEFAULT_TASK)!.name;
27-
usingDefaultTask = true;
27+
usingDefaultCommand = true;
2828
}
2929
let task = tasks.get(taskName);
3030
if (!task) {
3131
if (!tasks.has(DEFAULT_TASK)) {
3232
return errExit`missing task\n${usage}`;
3333
}
3434
task = tasks.get(DEFAULT_TASK);
35-
usingDefaultTask = true;
35+
usingDefaultCommand = true;
3636
}
3737
if (!task) {
3838
return errExit`missing task\n${usage}`;
@@ -51,11 +51,11 @@ export function getCliArgs(tasks: RegisteredTasks, name: string, _args?: string[
5151
return usageOption;
5252
})
5353
.join("\n");
54-
const nameAndTaskName = usingDefaultTask ? name : `${name} ${taskName}`;
55-
usage = `\nUsage: ${nameAndTaskName} <options>\nOptions:\n`;
54+
const nameAndCommandName = usingDefaultCommand ? name : `${name} ${taskName}`;
55+
usage = `\nUsage: ${nameAndCommandName} <options>\nOptions:\n`;
5656
usage += usageOptions;
5757

58-
const taskArgs = usingDefaultTask ? rawArgs : rawArgs.slice(1);
58+
const taskArgs = usingDefaultCommand ? rawArgs : rawArgs.slice(1);
5959
const taskConfig = {
6060
options,
6161
args: taskArgs,
@@ -64,14 +64,14 @@ export function getCliArgs(tasks: RegisteredTasks, name: string, _args?: string[
6464
try {
6565
parsed = parseArgs(taskConfig);
6666
} catch (e) {
67-
const err = ParseError.from(e, usingDefaultTask ? name : taskName);
67+
const err = ParseError.from(e, usingDefaultCommand ? name : taskName);
6868
return errExit`${err.message}\n${usage}`;
6969
}
7070
const args = parsed.values as Record<PropertyKey, unknown>;
7171
const errors: string[] = [];
7272
for (const _key in options) {
7373
const key = _key as keyof typeof options;
74-
const option = options[key] ?? ({} as TaskOption);
74+
const option = options[key] ?? ({} as CommandOption);
7575
if (!("required" in option) || !option.required) {
7676
args[key] ??= option.default;
7777
}
@@ -84,7 +84,23 @@ export function getCliArgs(tasks: RegisteredTasks, name: string, _args?: string[
8484
return errExit`missing required option${s} ${listify(errors)}\n${usage}`;
8585
}
8686

87-
return Object.assign(args, { [TASK_NAME]: taskName, usingDefaultTask });
87+
return Object.assign(args, { [TASK_NAME]: taskName, usingDefaultCommand });
88+
}
89+
90+
// task name could be the first argument, or it could be a series of commands and subcommands
91+
function tryToGetTaskName(args: string[], tasks: RegisteredCommands) {
92+
if (args.length === 0) return null;
93+
const firstArg = args[0] ?? "";
94+
if (tasks.has(firstArg)) return firstArg;
95+
const taskNames = [...tasks.keys()];
96+
let taskName = "";
97+
for (const arg of args) {
98+
taskName += arg;
99+
if (taskNames.includes(taskName)) return taskName;
100+
taskName += " ";
101+
}
102+
103+
return null;
88104
}
89105

90106
/** validates the options passed to a task */

types.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import { TASK_NAME } from "./constants";
1+
import { DEFAULT_TASK, TASK_NAME } from "./constants";
22

33
export type CmdCtrInstance = {
4-
register: (task: TaskInstance) => RegisteredTasks;
4+
register: (task: CommandInstance) => RegisteredCommands;
55
run: (args?: string[]) => void | Promise<void>;
66
};
77

88
export type CmdCtrConstructor = CmdCtrFn & CmdCtrClass;
9-
export type CmdCtrFn = (baseCommand?: TaskInstance | string) => CmdCtrInstance;
10-
type CmdCtrClass = new (baseCommand?: TaskInstance | string) => CmdCtrInstance;
9+
export type CmdCtrFn = (baseCommand?: CommandInstance | string) => CmdCtrInstance;
10+
type CmdCtrClass = new (baseCommand?: CommandInstance | string) => CmdCtrInstance;
1111

1212
/** data for a task including its options and information about it */
1313
export type DataInstance = {
1414
name: StartsWithAlpha;
1515
description: string;
16-
options: TaskOptions;
16+
options: CommandOptions;
1717
};
1818

1919
/** the constructor of a data object, which can be called with `new` or without */
@@ -34,22 +34,32 @@ type Alpha = AlphaLower | AlphaUpper;
3434
type StartsWithAlpha = Explicit<`${Alpha}${string}`>;
3535

3636
/** a task that can be registered and run */
37-
export type TaskInstance = DataInstance & { action: (validatedOpts: any) => void };
37+
export type CommandInstance = DataInstance & {
38+
action: (validatedOpts: any) => void;
39+
register: (cmd: CommandInstance) => RegisteredCommands;
40+
};
3841

3942
/** the constructor of a task, which can be called with `new` or without */
40-
export type TaskConstructor = TaskFn & TaskClass;
41-
export type TaskFn = <const D extends DataInstance>(data: D, action: Action<D>) => TaskInstance;
42-
type TaskClass = new <const D extends DataInstance>(data: D, action: Action<D>) => TaskInstance;
43+
export type CommandConstructor = CommandFn & CommandClass;
44+
export type CommandFn = <const D extends DataInstance>(
45+
data: D,
46+
action: Action<D>,
47+
) => CommandInstance;
48+
type CommandClass = new <const D extends DataInstance>(
49+
data: D,
50+
action: Action<D>,
51+
) => CommandInstance;
4352

4453
/** the action function of a task */
4554
export type Action<T extends DataInstance> = (
4655
args: MaskOpts<ValidatedOpts<T>>,
4756
) => void | Promise<void>;
4857

49-
export type RegisteredTasks = Map<string | symbol, TaskInstance & { isDefault?: boolean }>;
58+
type CommandInstanceWithDefault = CommandInstance & { isDefault?: boolean };
59+
export type RegisteredCommands = Map<string | DEFAULT_TASK, CommandInstanceWithDefault>;
5060

5161
/** the possible options for a task */
52-
export type TaskOptions = { [long: string]: TaskOption };
62+
export type CommandOptions = { [long: string]: CommandOption };
5363

5464
type TypeLiteral = "string" | "boolean";
5565
type TypeLiteralToNative<T extends TypeLiteral> = {
@@ -64,14 +74,14 @@ type OptionItemDescriptor<T extends TypeLiteral> = {
6474
short?: Alpha;
6575
description: string;
6676
};
67-
type TaskOptionItem<T extends TypeLiteral, R extends boolean> = OptionItemDescriptor<T> &
77+
type CommandOptionItem<T extends TypeLiteral, R extends boolean> = OptionItemDescriptor<T> &
6878
OptionItemRequirement<T, R>;
6979

70-
export type TaskOption =
71-
| TaskOptionItem<"string", false>
72-
| TaskOptionItem<"boolean", false>
73-
| TaskOptionItem<"string", true>
74-
| TaskOptionItem<"boolean", true>;
80+
export type CommandOption =
81+
| CommandOptionItem<"string", false>
82+
| CommandOptionItem<"boolean", false>
83+
| CommandOptionItem<"string", true>
84+
| CommandOptionItem<"boolean", true>;
7585

7686
/** the validated options passed to the task action */
7787
type ValidatedOpts<T extends DataInstance> = OptionsFromData<T>;
@@ -83,7 +93,7 @@ type OptionsFromData<T extends DataInstance> = {
8393

8494
/** the arguments passed to the CLI */
8595
export type CliArgs = Record<PropertyKey, unknown> & { [K in TASK_NAME]: string } & {
86-
usingDefaultTask: boolean;
96+
usingDefaultCommand: boolean;
8797
};
8898

8999
/** widens the type `T` to be compatible with the type `U` if it has the same keys */
@@ -93,7 +103,7 @@ type Widen<T, U> = { [K in keyof T]: K extends keyof U ? U[K] : T[K] };
93103
export type Strict<T, U> = StrictHelper<T, U> & StrictHelper<U, T>;
94104
type StrictHelper<T, U> = U extends Widen<T, U> ? T : `ERROR: only known properties are allowed`;
95105

96-
/** removes the `TaskNameKey` property from the type `T` */
106+
/** removes the `CommandNameKey` property from the type `T` */
97107
type MaskOpts<T> = T extends infer U
98108
? { [K in keyof U as K extends TASK_NAME ? never : K]: U[K] }
99109
: never;

0 commit comments

Comments
 (0)