Skip to content

Commit 76ce04c

Browse files
committed
cli: add pre/post hooks
1 parent 7f827fc commit 76ce04c

File tree

2 files changed

+105
-7
lines changed

2 files changed

+105
-7
lines changed

packages/alepha/src/command/primitives/$command.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,61 @@ export interface CommandPrimitiveOptions<T extends TObject, A extends TSchema> {
8282
* Equivalent to setting name to an empty string "".
8383
*/
8484
root?: boolean;
85+
86+
/**
87+
* Run this command's handler BEFORE the specified target command.
88+
*
89+
* Pre-hooks are not listed in help and cannot be called directly.
90+
* They receive the same parsed flags and args as the target command.
91+
*
92+
* @example
93+
* ```ts
94+
* class BuildCommands {
95+
* prebuild = $command({
96+
* pre: "build",
97+
* handler: async ({ run }) => {
98+
* await run("cleaning dist folder...", () => fs.rm("dist"));
99+
* }
100+
* });
101+
*
102+
* build = $command({
103+
* name: "build",
104+
* handler: async () => { ... }
105+
* });
106+
* }
107+
* ```
108+
*/
109+
pre?: string;
110+
111+
/**
112+
* Run this command's handler AFTER the specified target command.
113+
*
114+
* Post-hooks are not listed in help and cannot be called directly.
115+
* They receive the same parsed flags and args as the target command.
116+
*
117+
* @example
118+
* ```ts
119+
* class BuildCommands {
120+
* build = $command({
121+
* name: "build",
122+
* handler: async () => { ... }
123+
* });
124+
*
125+
* postbuild = $command({
126+
* post: "build",
127+
* handler: async ({ run }) => {
128+
* await run("generating checksums...", generateChecksums);
129+
* }
130+
* });
131+
* }
132+
* ```
133+
*/
134+
post?: string;
135+
136+
/**
137+
* If true, this command will be hidden from the help output.
138+
*/
139+
hide?: boolean;
85140
}
86141

87142
// ---------------------------------------------------------------------------------------------------------------------
@@ -93,10 +148,22 @@ export class CommandPrimitive<
93148
public readonly flags = this.options.flags ?? t.object({});
94149
public readonly aliases = this.options.aliases ?? [];
95150

151+
protected onInit() {
152+
if (this.options.pre || this.options.post) {
153+
this.options.hide ??= true;
154+
}
155+
}
156+
96157
public get name(): string {
97158
if (this.options.root) {
98159
return "";
99160
}
161+
if (this.options.pre) {
162+
return `pre${this.options.pre}`;
163+
}
164+
if (this.options.post) {
165+
return `post${this.options.post}`;
166+
}
100167
return this.options.name ?? `${this.config.propertyKey}`;
101168
}
102169
}

packages/alepha/src/command/providers/CliProvider.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,23 @@ export class CliProvider {
169169
root: process.cwd(),
170170
};
171171

172+
// Execute pre-hooks
173+
const preHooks = this.findPreHooks(command.name);
174+
for (const hook of preHooks) {
175+
this.log.debug(`Executing pre-hook for '${command.name}'...`);
176+
await hook.options.handler(args as CommandHandlerArgs<TObject>);
177+
}
178+
179+
// Execute main command
172180
await command.options.handler(args as CommandHandlerArgs<TObject>);
173181

182+
// Execute post-hooks
183+
const postHooks = this.findPostHooks(command.name);
184+
for (const hook of postHooks) {
185+
this.log.debug(`Executing post-hook for '${command.name}'...`);
186+
await hook.options.handler(args as CommandHandlerArgs<TObject>);
187+
}
188+
174189
if (command.options.summary !== false) {
175190
runner.summary();
176191
}
@@ -190,6 +205,20 @@ export class CliProvider {
190205
);
191206
}
192207

208+
/**
209+
* Find all pre-hooks for a command.
210+
*/
211+
protected findPreHooks(commandName: string): CommandPrimitive<TObject>[] {
212+
return this.commands.filter((cmd) => cmd.name === `pre${commandName}`);
213+
}
214+
215+
/**
216+
* Find all post-hooks for a command.
217+
*/
218+
protected findPostHooks(commandName: string): CommandPrimitive<TObject>[] {
219+
return this.commands.filter((cmd) => cmd.name === `post${commandName}`);
220+
}
221+
193222
/**
194223
* Get all global flags including those from the root command (name === "")
195224
*/
@@ -464,8 +493,8 @@ export class CliProvider {
464493
const maxCmdLength = this.getMaxCmdLength(this.commands);
465494

466495
for (const command of this.commands) {
467-
// skip root command in list
468-
if (command.name === "") {
496+
// skip root command and hooks in list
497+
if (command.name === "" || command.options.hide) {
469498
continue;
470499
}
471500

@@ -495,11 +524,13 @@ export class CliProvider {
495524

496525
private getMaxCmdLength(commands: CommandPrimitive[]): number {
497526
return Math.max(
498-
...commands.map((c) => {
499-
const cmdStr = [c.name, ...c.aliases].join(", ");
500-
const argsUsage = this.generateArgsUsage(c.options.args);
501-
return `${cmdStr}${argsUsage}`.length;
502-
}),
527+
...commands
528+
.filter((c) => !c.options.hide && c.name !== "")
529+
.map((c) => {
530+
const cmdStr = [c.name, ...c.aliases].join(", ");
531+
const argsUsage = this.generateArgsUsage(c.options.args);
532+
return `${cmdStr}${argsUsage}`.length;
533+
}),
503534
);
504535
}
505536

0 commit comments

Comments
 (0)