Skip to content

Commit 6c84890

Browse files
authored
feat: global flags (#28)
* refactor * tsdoc * make description required. * refactor * fix test * remov no description * move to utils * global flags * refactor * use global flag
1 parent b03b87e commit 6c84890

File tree

9 files changed

+199
-120
lines changed

9 files changed

+199
-120
lines changed

packages/core/src/cli.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import type {
2222
CommandType,
2323
CommandWithHandler,
2424
Commands,
25+
FlagWithoutDescription,
2526
Flags,
27+
FlagsWithoutDescription,
2628
Handler,
2729
HandlerContext,
2830
I18N,
@@ -48,13 +50,14 @@ import { locales } from "./locales";
4850
export const Root = Symbol.for("Clerc.Root");
4951
export type RootType = typeof Root;
5052

51-
export class Clerc<C extends Commands = {}> {
53+
export class Clerc<C extends Commands = {}, GF extends FlagsWithoutDescription = {}> {
5254
#name = "";
5355
#description = "";
5456
#version = "";
5557
#inspectors: Inspector[] = [];
5658
#commands = {} as C;
5759
#commandEmitter = new LiteEmit<MakeEventMap<C>>();
60+
#flags = {} as GF;
5861
#usedNames = new Set<string | RootType>();
5962
#argv: string[] | undefined;
6063
#errorHandlers = [] as ((err: any) => void)[];
@@ -97,6 +100,7 @@ export class Clerc<C extends Commands = {}> {
97100
get _version () { return this.#version; }
98101
get _inspectors () { return this.#inspectors; }
99102
get _commands () { return this.#commands; }
103+
get _flags () { return this.#flags; }
100104

101105
#addCoreLocales () { this.i18n.add(locales); }
102106
#otherMethodCaled () { this.#isOtherMethodCalled = true; }
@@ -244,8 +248,8 @@ export class Clerc<C extends Commands = {}> {
244248
* })
245249
* ```
246250
*/
247-
command<N extends string | RootType, O extends CommandOptions<[...P], A, F>, P extends string[] = string[], A extends MaybeArray<string | RootType> = MaybeArray<string | RootType>, F extends Flags = Flags>(c: CommandWithHandler<N, O & CommandOptions<[...P], A, F>>): this & Clerc<C & Record<N, Command<N, O>>>;
248-
command<N extends string | RootType, O extends CommandOptions<[...P], A, F>, P extends string[] = string[], A extends MaybeArray<string | RootType> = MaybeArray<string | RootType>, F extends Flags = Flags>(name: N, description: string, options?: O & CommandOptions<[...P], A, F>): this & Clerc<C & Record<N, Command<N, O>>>;
251+
command<N extends string | RootType, O extends CommandOptions<[...P], A, F>, P extends string[] = string[], A extends MaybeArray<string | RootType> = MaybeArray<string | RootType>, F extends Flags = Flags>(c: CommandWithHandler<N, O & CommandOptions<[...P], A, F>>): this & Clerc<C & Record<N, Command<N, O>>, GF>;
252+
command<N extends string | RootType, O extends CommandOptions<[...P], A, F>, P extends string[] = string[], A extends MaybeArray<string | RootType> = MaybeArray<string | RootType>, F extends Flags = Flags>(name: N, description: string, options?: O & CommandOptions<[...P], A, F>): this & Clerc<C & Record<N, Command<N, O>>, GF>;
249253
command (nameOrCommand: any, description?: any, options: any = {}) {
250254
this.#callWithErrorHandling(() => this.#command(nameOrCommand, description, options));
251255
return this;
@@ -282,6 +286,28 @@ export class Clerc<C extends Commands = {}> {
282286
return this as any;
283287
}
284288

289+
/**
290+
* Register a global flag
291+
* @param name
292+
* @param options
293+
* @returns
294+
* @example
295+
* ```ts
296+
* Clerc.create()
297+
* .flag("help", {
298+
* alias: "h",
299+
* description: "help",
300+
* })
301+
* ```
302+
*/
303+
flag<N extends string, O extends FlagWithoutDescription>(name: N, description: string, options: O): this & Clerc<C, GF & Record<N, O>> {
304+
this.#flags[name] = {
305+
description,
306+
...options,
307+
} as any;
308+
return this as any;
309+
}
310+
285311
/**
286312
* Register a handler
287313
* @param name
@@ -296,7 +322,7 @@ export class Clerc<C extends Commands = {}> {
296322
* })
297323
* ```
298324
*/
299-
on<K extends LiteralUnion<keyof CM, string | RootType>, CM extends this["_commands"] = this["_commands"]>(name: K, handler: Handler<CM, K>) {
325+
on<K extends LiteralUnion<keyof CM, string | RootType>, CM extends this["_commands"] = this["_commands"]>(name: K, handler: Handler<CM, K, this["_flags"]>) {
300326
this.#commandEmitter.on(name as any, handler as any);
301327
return this;
302328
}
@@ -433,7 +459,7 @@ export class Clerc<C extends Commands = {}> {
433459
return context;
434460
}
435461

436-
#callWithErrorHandling (fn: () => void) {
462+
#callWithErrorHandling (fn: (...args: any[]) => any) {
437463
try {
438464
fn();
439465
} catch (e) {

packages/core/src/types/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import type { ParseFlag, ParseParameters, ParseRaw } from "./utils";
77
export type CommandType = RootType | string;
88

99
export type FlagOptions = FlagSchema & {
10-
description?: string
10+
description: string
1111
};
1212
export type Flag = FlagOptions & {
1313
name: string
1414
};
15+
export type FlagWithoutDescription = Omit<Flag, "description">;
1516
export type Flags = Dict<FlagOptions>;
17+
export type FlagsWithoutDescription = Dict<FlagWithoutDescription>;
1618

1719
export declare interface CommandCustomProperties {}
1820
export interface CommandOptions<P extends string[] = string[], A extends MaybeArray<string | RootType> = MaybeArray<string | RootType>, F extends Flags = Flags> extends CommandCustomProperties {
@@ -37,19 +39,19 @@ export interface ParseOptions {
3739
run?: boolean
3840
}
3941

40-
export interface HandlerContext<C extends Commands = Commands, N extends keyof C = keyof C> {
42+
export interface HandlerContext<C extends Commands = Commands, N extends keyof C = keyof C, GF extends FlagsWithoutDescription = {}> {
4143
name?: LiteralUnion<N, string>
4244
called?: string | RootType
4345
resolved: boolean
4446
hasRootOrAlias: boolean
4547
hasRoot: boolean
46-
raw: Simplify<ParseRaw<C[N]>>
48+
raw: Simplify<ParseRaw<C[N], GF>>
4749
parameters: Simplify<ParseParameters<C, N>>
4850
unknownFlags: ParsedFlags["unknownFlags"]
49-
flags: Simplify<ParseFlag<C, N> & Record<string, any>>
50-
cli: Clerc<C>
51+
flags: Simplify<ParseFlag<C, N, GF> & Record<string, any>>
52+
cli: Clerc<C, GF>
5153
}
52-
export type Handler<C extends Commands = Commands, K extends keyof C = keyof C> = (ctx: HandlerContext<C, K>) => void;
54+
export type Handler<C extends Commands = Commands, K extends keyof C = keyof C, GF extends FlagsWithoutDescription = {}> = (ctx: HandlerContext<C, K, GF>) => void;
5355
export type HandlerInCommand<C extends HandlerContext> = (ctx: { [K in keyof C]: C[K] }) => void;
5456
export type FallbackType<T, U> = {} extends T ? U : T;
5557
export type InspectorContext<C extends Commands = Commands> = HandlerContext<C> & {

packages/core/src/types/utils.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CamelCase, Dict, Equals } from "@clerc/utils";
22
import type { OmitIndexSignature } from "type-fest";
33
import type { TypeFlag } from "./type-flag";
4-
import type { Command, Commands, InspectorContext } from ".";
4+
import type { Command, Commands, Flags, FlagsWithoutDescription, InspectorContext } from ".";
55

66
type StripBrackets<Parameter extends string> = (
77
Parameter extends `<${infer ParameterName}>` | `[${infer ParameterName}]`
@@ -30,13 +30,13 @@ export type TransformParameters<C extends Command> = {
3030

3131
export type MakeEventMap<T extends Commands> = { [K in keyof T]: [InspectorContext] };
3232

33-
type FallbackFlags<C extends Command> = Equals<NonNullableFlag<C>["flags"], {}> extends true ? Dict<any> : NonNullableFlag<C>["flags"];
34-
type NonNullableFlag<C extends Command> = TypeFlag<NonNullable<C["flags"]>>;
35-
export type ParseFlag<C extends Commands, N extends keyof C> = N extends keyof C ? OmitIndexSignature<NonNullableFlag<C[N]>["flags"]> : FallbackFlags<C[N]>["flags"];
36-
export type ParseRaw<C extends Command> = NonNullableFlag<C> & {
37-
flags: FallbackFlags<C>
33+
type FallbackFlags<F extends Flags | undefined> = Equals<NonNullableFlag<F>["flags"], {}> extends true ? Dict<any> : NonNullableFlag<F>["flags"];
34+
type NonNullableFlag<F extends Flags | undefined> = TypeFlag<NonNullable<F>>;
35+
export type ParseFlag<C extends Commands, N extends keyof C, GF extends FlagsWithoutDescription = {}> = N extends keyof C ? OmitIndexSignature<NonNullableFlag<C[N]["flags"] & GF>["flags"]> : FallbackFlags<C[N]["flags"] & GF>["flags"];
36+
export type ParseRaw<C extends Command, GF extends FlagsWithoutDescription = {}> = NonNullableFlag<C["flags"] & GF> & {
37+
flags: FallbackFlags<C["flags"] & GF>
3838
parameters: string[]
39-
mergedFlags: FallbackFlags<C> & NonNullableFlag<C>["unknownFlags"]
39+
mergedFlags: FallbackFlags<C["flags"] & GF> & NonNullableFlag<C["flags"] & GF>["unknownFlags"]
4040
};
4141
export type ParseParameters<C extends Commands = Commands, N extends keyof C = keyof C> =
4242
Equals<TransformParameters<C[N]>, {}> extends true

packages/plugin-help/src/index.ts

Lines changed: 49 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import type { Clerc, Command, HandlerContext, RootType, TranslateFn } from "@clerc/core";
1+
import type { HandlerContext, RootType } from "@clerc/core";
22
import { NoSuchCommandError, Root, definePlugin, formatCommandName, resolveCommandStrict, withBrackets } from "@clerc/core";
3-
4-
import { gracefulFlagName, toArray } from "@clerc/utils";
3+
import { toArray } from "@clerc/utils";
54
import pc from "picocolors";
65

76
import type { Render, Renderers, Section } from "./renderer";
87
import { defaultRenderers, render } from "./renderer";
9-
import { splitTable } from "./utils";
8+
import { DELIMITER, formatFlags, generateCliDetail, generateExamples, print, sortName, splitTable } from "./utils";
109
import { locales } from "./locales";
1110

1211
declare module "@clerc/core" {
@@ -15,46 +14,6 @@ declare module "@clerc/core" {
1514
}
1615
}
1716

18-
const DELIMITER = pc.yellow("-");
19-
20-
const print = (s: string) => { process.stdout.write(s); };
21-
22-
const generateCliDetail = (sections: Section[], cli: Clerc, subcommand?: Command<string | RootType>) => {
23-
const { t } = cli.i18n;
24-
const items = [
25-
{
26-
title: t("help.name")!,
27-
body: pc.red(cli._name),
28-
},
29-
{
30-
title: t("help.version")!,
31-
body: pc.yellow(cli._version),
32-
},
33-
];
34-
if (subcommand) {
35-
items.push({
36-
title: t("help.subcommand")!,
37-
body: pc.green(`${cli._name} ${formatCommandName(subcommand.name)}`),
38-
});
39-
}
40-
sections.push({
41-
type: "inline",
42-
items,
43-
});
44-
sections.push({
45-
title: t("help.description")!,
46-
body: [subcommand?.description || cli._description],
47-
});
48-
};
49-
50-
const generateExamples = (sections: Section[], examples: [string, string][], t: TranslateFn) => {
51-
const examplesFormatted = examples.map(([command, description]) => [command, DELIMITER, description]);
52-
sections.push({
53-
title: t("help.examples")!,
54-
body: splitTable(examplesFormatted),
55-
});
56-
};
57-
5817
const generateHelp = (render: Render, ctx: HandlerContext, notes: string[] | undefined, examples: [string, string][] | undefined) => {
5918
const { cli } = ctx;
6019
const { t } = cli.i18n;
@@ -64,23 +23,31 @@ const generateHelp = (render: Render, ctx: HandlerContext, notes: string[] | und
6423
title: t("help.usage")!,
6524
body: [pc.magenta(`$ ${cli._name} ${withBrackets("command", ctx.hasRootOrAlias)} [flags]`)],
6625
});
67-
const commands = [...(ctx.hasRoot ? [cli._commands[Root]!] : []), ...Object.values(cli._commands)].map((command) => {
26+
const commands = [
27+
...(ctx.hasRoot ? [cli._commands[Root]!] : []),
28+
...Object.values(cli._commands),
29+
].map((command) => {
6830
const commandNameWithAlias = [typeof command.name === "symbol" ? "" : command.name, ...toArray(command.alias || [])]
69-
.sort((a, b) => {
70-
if (a === Root) { return -1; }
71-
if (b === Root) { return 1; }
72-
return a.length - b.length;
73-
})
31+
.sort(sortName)
7432
.map((n) => {
7533
return (n === "" || typeof n === "symbol") ? `${cli._name}` : `${cli._name} ${n}`;
7634
})
7735
.join(", ");
7836
return [pc.cyan(commandNameWithAlias), DELIMITER, command.description];
7937
});
80-
sections.push({
81-
title: t("help.commands")!,
82-
body: splitTable(commands),
83-
});
38+
if (commands.length) {
39+
sections.push({
40+
title: t("help.commands")!,
41+
body: splitTable(commands),
42+
});
43+
}
44+
const globalFlags = formatFlags(cli._flags);
45+
if (globalFlags.length) {
46+
sections.push({
47+
title: t("help.globalFlags")!,
48+
body: splitTable(globalFlags),
49+
});
50+
}
8451
if (notes) {
8552
sections.push({
8653
title: t("help.notes")!,
@@ -118,25 +85,17 @@ const generateSubcommandHelp = (render: Render, ctx: HandlerContext, command: st
11885
title: t("help.usage")!,
11986
body: [pc.magenta(`$ ${cli._name}${commandName}${parametersString}${flagsString}`)],
12087
});
88+
const globalFlags = formatFlags(cli._flags);
89+
if (globalFlags.length) {
90+
sections.push({
91+
title: t("help.globalFlags")!,
92+
body: splitTable(globalFlags),
93+
});
94+
}
12195
if (subcommand.flags) {
12296
sections.push({
12397
title: t("help.flags")!,
124-
body: splitTable(
125-
Object.entries(subcommand.flags).map(([name, flag]) => {
126-
const hasDefault = flag.default !== undefined;
127-
let flagNameWithAlias: string[] = [gracefulFlagName(name)];
128-
if (flag.alias) {
129-
flagNameWithAlias.push(gracefulFlagName(flag.alias));
130-
}
131-
flagNameWithAlias = flagNameWithAlias.map(renderers.renderFlagName);
132-
const items = [pc.blue(flagNameWithAlias.join(", ")), renderers.renderType(flag.type, hasDefault)];
133-
items.push(DELIMITER, flag.description || t("help.noDescription")!);
134-
if (hasDefault) {
135-
items.push(`(${t("help.default", renderers.renderDefault(flag.default))})`);
136-
}
137-
return items;
138-
}),
139-
),
98+
body: splitTable(formatFlags(subcommand.flags)),
14099
});
141100
}
142101
if (subcommand.notes) {
@@ -154,10 +113,15 @@ const generateSubcommandHelp = (render: Render, ctx: HandlerContext, command: st
154113

155114
export interface HelpPluginOptions {
156115
/**
157-
* Whether to registr the help command.
116+
* Whether to register the help command.
158117
* @default true
159118
*/
160119
command?: boolean
120+
/**
121+
* Whether to register the global help flag.
122+
* @default true
123+
*/
124+
flag?: boolean
161125
/**
162126
* Whether to show help when no command is specified.
163127
* @default true
@@ -172,12 +136,13 @@ export interface HelpPluginOptions {
172136
*/
173137
examples?: [string, string][]
174138
/**
175-
* Banner
139+
* Banner.
176140
*/
177141
banner?: string
178142
}
179143
export const helpPlugin = ({
180144
command = true,
145+
flag = true,
181146
showHelpWhenNoCommand = true,
182147
notes,
183148
examples,
@@ -192,7 +157,7 @@ export const helpPlugin = ({
192157
};
193158

194159
if (command) {
195-
cli = cli.command("help", t("help.commandDescription")!, {
160+
cli = cli.command("help", t("help.helpDdescription")!, {
196161
parameters: [
197162
"[command...]",
198163
],
@@ -216,15 +181,23 @@ export const helpPlugin = ({
216181
});
217182
}
218183

184+
if (flag) {
185+
cli = cli.flag("help", t("help.helpDdescription")!, {
186+
alias: "h",
187+
type: Boolean,
188+
default: false,
189+
});
190+
}
191+
219192
cli.inspector((ctx, next) => {
220-
const helpFlag = ctx.raw.mergedFlags.h || ctx.raw.mergedFlags.help;
221-
if (!ctx.hasRootOrAlias && !ctx.raw._.length && showHelpWhenNoCommand && !helpFlag) {
193+
const shouldShowHelp = ctx.flags.help;
194+
if (!ctx.hasRootOrAlias && !ctx.raw._.length && showHelpWhenNoCommand && !shouldShowHelp) {
222195
let str = `${t("core.noCommandGiven")}\n\n`;
223196
str += generateHelp(render, ctx, notes, examples);
224197
str += "\n";
225198
printHelp(str);
226199
process.exit(1);
227-
} else if (helpFlag) {
200+
} else if (shouldShowHelp) {
228201
if (ctx.raw._.length) {
229202
if (ctx.called !== Root) {
230203
if (ctx.name === Root) {

packages/plugin-help/src/locales.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ export const locales: Locales = {
66
"help.version": "Version",
77
"help.subcommand": "Subcommand",
88
"help.commands": "Commands",
9+
"help.globalFlags": "Global Flags",
910
"help.flags": "Flags",
1011
"help.description": "Description",
1112
"help.usage": "Usage",
1213
"help.examples": "Examples",
1314
"help.notes": "Notes",
14-
"help.noDescription": "(No description)",
1515
"help.notes.1": "If no command is specified, show help for the CLI.",
1616
"help.notes.2": "If a command is specified, show help for the command.",
1717
"help.notes.3": "-h is an alias for --help.",
@@ -25,12 +25,12 @@ export const locales: Locales = {
2525
"help.version": "版本",
2626
"help.subcommand": "子命令",
2727
"help.commands": "命令",
28+
"help.globalFlags": "全局标志",
2829
"help.flags": "标志",
2930
"help.description": "描述",
3031
"help.usage": "使用",
3132
"help.examples": "示例",
3233
"help.notes": "备注",
33-
"help.noDescription": "(无描述)",
3434
"help.notes.1": "如果没有指定展示哪个命令的帮助信息,默认展示CLI的。",
3535
"help.notes.2": "如果指定了则展示该命令帮助信息。",
3636
"help.notes.3": "-h 是 --help 的一个别名。",

0 commit comments

Comments
 (0)