@clerc/plugin-help 1.0.0-beta.1 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,22 +1,137 @@
1
1
  import { Plugin } from "@clerc/core";
2
2
 
3
+ //#region ../parser/src/types.d.ts
4
+
5
+ /**
6
+ * Defines how a string input is converted to the target type T.
7
+ *
8
+ * @template T The target type.
9
+ */
10
+ type FlagTypeFunction<T = unknown> = ((value: string) => T) & {
11
+ /**
12
+ * Optional display name for the type, useful in help output.
13
+ * If provided, this will be shown instead of the function name.
14
+ */
15
+ displayName?: string;
16
+ };
17
+ type FlagType<T = unknown> = FlagTypeFunction<T> | readonly [FlagTypeFunction<T>];
18
+ //#endregion
19
+ //#region src/types.d.ts
20
+ interface Formatters {
21
+ formatFlagType: (type: FlagType) => string;
22
+ }
23
+ /**
24
+ * A group definition as a tuple of [key, displayName].
25
+ * The key is used in help options to assign items to groups.
26
+ * The displayName is shown in the help output.
27
+ */
28
+ type GroupDefinition = [key: string, name: string];
29
+ /**
30
+ * Options for defining groups in help output.
31
+ */
32
+ interface GroupsOptions {
33
+ /**
34
+ * Groups for commands.
35
+ * Each group is defined as `[key, name]`.
36
+ */
37
+ commands?: GroupDefinition[];
38
+ /**
39
+ * Groups for command-specific flags.
40
+ * Each group is defined as `[key, name]`.
41
+ */
42
+ flags?: GroupDefinition[];
43
+ /**
44
+ * Groups for global flags.
45
+ * Each group is defined as `[key, name]`.
46
+ */
47
+ globalFlags?: GroupDefinition[];
48
+ }
49
+ //#endregion
50
+ //#region src/formatters.d.ts
51
+ declare const defaultFormatters: Formatters;
52
+ //#endregion
3
53
  //#region src/index.d.ts
54
+ interface HelpOptions {
55
+ /**
56
+ * The group this item belongs to.
57
+ * The group must be defined in the `groups` option of `helpPlugin()`.
58
+ */
59
+ group?: string;
60
+ }
61
+ interface CommandHelpOptions extends HelpOptions {
62
+ /**
63
+ * Whether to show the command in help output.
64
+ *
65
+ * @default true
66
+ */
67
+ show?: boolean;
68
+ /**
69
+ * Notes to show in the help output.
70
+ */
71
+ notes?: string[];
72
+ /**
73
+ * Examples to show in the help output.
74
+ * Each example is a tuple of `[command, description]`.
75
+ */
76
+ examples?: [string, string][];
77
+ }
4
78
  declare module "@clerc/core" {
5
79
  interface CommandCustomOptions {
6
- help?: {
7
- showInHelp?: boolean;
8
- notes?: string[];
9
- examples?: [string, string][];
10
- };
80
+ /**
81
+ * Help options for the command.
82
+ */
83
+ help?: CommandHelpOptions;
84
+ }
85
+ interface FlagCustomOptions {
86
+ /**
87
+ * Help options for the flag.
88
+ */
89
+ help?: HelpOptions;
11
90
  }
12
91
  }
13
92
  interface HelpPluginOptions {
93
+ /**
94
+ * Whether to register the `help` command.
95
+ *
96
+ * @default true
97
+ */
14
98
  command?: boolean;
99
+ /**
100
+ * Whether to register the `--help` global flag.
101
+ *
102
+ * @default true
103
+ */
15
104
  flag?: boolean;
105
+ /**
106
+ * Whether to show help when no command is specified.
107
+ *
108
+ * @default true
109
+ */
16
110
  showHelpWhenNoCommandSpecified?: boolean;
111
+ /**
112
+ * Notes to show in the help output.
113
+ */
17
114
  notes?: string[];
115
+ /**
116
+ * Examples to show in the help output.
117
+ * Each example is a tuple of `[command, description]`.
118
+ */
18
119
  examples?: [string, string][];
120
+ /**
121
+ * A banner to show before the help output.
122
+ */
19
123
  banner?: string;
124
+ /**
125
+ * Custom formatters for rendering help.
126
+ */
127
+ formatters?: Partial<Formatters>;
128
+ /**
129
+ * Group definitions for commands and flags.
130
+ * Groups allow organizing commands and flags into logical sections in help output.
131
+ * Each group is defined as `[key, name]` where `key` is the identifier used in help options
132
+ * and `name` is the display name shown in help output.
133
+ */
134
+ groups?: GroupsOptions;
20
135
  }
21
136
  declare const helpPlugin: ({
22
137
  command,
@@ -24,7 +139,9 @@ declare const helpPlugin: ({
24
139
  showHelpWhenNoCommandSpecified,
25
140
  notes,
26
141
  examples,
27
- banner
142
+ banner,
143
+ formatters,
144
+ groups
28
145
  }?: HelpPluginOptions) => Plugin;
29
146
  //#endregion
30
- export { HelpPluginOptions, helpPlugin };
147
+ export { CommandHelpOptions, type GroupDefinition, type GroupsOptions, HelpOptions, HelpPluginOptions, defaultFormatters, helpPlugin };
package/dist/index.js CHANGED
@@ -6,22 +6,40 @@ import * as yc from "yoctocolors";
6
6
 
7
7
  //#region src/utils.ts
8
8
  function formatFlagType(type) {
9
- if (typeof type === "function") return type.name;
10
- return `Array<${type[0].name}>`;
9
+ if (typeof type === "function") return type.displayName ?? type.name;
10
+ const innerType = type[0];
11
+ return `Array<${innerType.displayName ?? innerType.name}>`;
11
12
  }
12
13
 
14
+ //#endregion
15
+ //#region src/formatters.ts
16
+ const defaultFormatters = { formatFlagType };
17
+
13
18
  //#endregion
14
19
  //#region src/renderer.ts
20
+ const DEFAULT_GROUP_KEY = "default";
15
21
  const table = (items) => textTable(items, { stringLength: stringWidth });
16
22
  const splitTable = (items) => table(items).split("\n");
17
23
  const DELIMITER = yc.yellow("-");
24
+ function groupDefinitionsToMap(definitions) {
25
+ const map = /* @__PURE__ */ new Map();
26
+ if (definitions) for (const [key, name] of definitions) map.set(key, name);
27
+ return map;
28
+ }
29
+ function validateGroup(group, groupMap, itemType, itemName) {
30
+ if (group && group !== DEFAULT_GROUP_KEY && !groupMap.has(group)) throw new Error(`Unknown ${itemType} group "${group}" for "${itemName}". Available groups: ${[...groupMap.keys()].join(", ") || "(none)"}`);
31
+ }
18
32
  var HelpRenderer = class {
19
- constructor(_cli, _globalFlags, _command, _notes, _examples) {
33
+ constructor(_formatters, _cli, _globalFlags, _command, _notes, _examples, groups) {
34
+ this._formatters = _formatters;
20
35
  this._cli = _cli;
21
36
  this._globalFlags = _globalFlags;
22
37
  this._command = _command;
23
38
  this._notes = _notes;
24
39
  this._examples = _examples;
40
+ this._commandGroups = groupDefinitionsToMap(groups?.commands);
41
+ this._flagGroups = groupDefinitionsToMap(groups?.flags);
42
+ this._globalFlagGroups = groupDefinitionsToMap(groups?.globalFlags);
25
43
  }
26
44
  render() {
27
45
  return [
@@ -33,7 +51,7 @@ var HelpRenderer = class {
33
51
  this.renderNotes(),
34
52
  this.renderExamples()
35
53
  ].filter(isTruthy).filter((section) => section.body.length > 0).map((section) => {
36
- const body = Array.isArray(section.body) ? section.body.filter(Boolean).join("\n") : section.body;
54
+ const body = Array.isArray(section.body) ? section.body.filter((s) => s !== void 0).join("\n") : section.body;
37
55
  if (!section.title) return body;
38
56
  return `${yc.bold(section.title)}\n${body.split("\n").map((line) => ` ${line}`).join("\n")}`;
39
57
  }).join("\n\n");
@@ -44,7 +62,7 @@ var HelpRenderer = class {
44
62
  const description = command?.description ?? _description;
45
63
  const formattedCommandName = command?.name ? ` ${yc.cyan(command.name)}` : "";
46
64
  const headerLine = command ? `${yc.green(_name)}${formattedCommandName}` : `${yc.green(_name)} ${yc.yellow(formatVersion(_version))}`;
47
- const alias = command?.alias ? `Alias${toArray(command.alias).length > 1 ? "es" : ""}: ${toArray(command.alias).map((a) => yc.cyan(a)).join(", ")}` : "";
65
+ const alias = command?.alias ? `Alias${toArray(command.alias).length > 1 ? "es" : ""}: ${toArray(command.alias).map((a) => yc.cyan(a)).join(", ")}` : void 0;
48
66
  return { body: [`${headerLine}${description ? ` ${DELIMITER} ${description}` : ""}`, alias] };
49
67
  }
50
68
  renderUsage() {
@@ -52,7 +70,7 @@ var HelpRenderer = class {
52
70
  const command = this._command;
53
71
  let usage = `$ ${_scriptName}`;
54
72
  if (command) {
55
- usage += command.name ? ` ${command.name}` : "";
73
+ if (command.name) usage += ` ${command.name}`;
56
74
  if (command.parameters) usage += ` ${command.parameters.join(" ")}`;
57
75
  }
58
76
  if (command?.flags && !objectIsEmpty(command.flags) || !objectIsEmpty(this._globalFlags)) usage += " [FLAGS]";
@@ -64,42 +82,91 @@ var HelpRenderer = class {
64
82
  renderCommands() {
65
83
  const commands = this._cli._commands;
66
84
  if (this._command || commands.size === 0) return;
85
+ const groupedCommands = /* @__PURE__ */ new Map();
86
+ const defaultCommands = [];
87
+ for (const command of commands.values()) {
88
+ if (command.__isAlias || command.help?.show === false) continue;
89
+ const group = command.help?.group;
90
+ validateGroup(group, this._commandGroups, "command", command.name);
91
+ const item = [`${yc.cyan(command.name)}${command.alias ? ` (${toArray(command.alias).join(", ")})` : ""}`, command.description];
92
+ if (group && group !== DEFAULT_GROUP_KEY) {
93
+ const groupItems = groupedCommands.get(group) ?? [];
94
+ groupItems.push(item);
95
+ groupedCommands.set(group, groupItems);
96
+ } else defaultCommands.push(item);
97
+ }
98
+ const body = [];
99
+ for (const [key, name] of this._commandGroups) {
100
+ const items = groupedCommands.get(key);
101
+ if (items && items.length > 0) {
102
+ if (body.length > 0) body.push("");
103
+ body.push(`${yc.dim(name)}`);
104
+ for (const line of splitTable(items)) body.push(` ${line}`);
105
+ }
106
+ }
107
+ if (defaultCommands.length > 0) if (body.length > 0) {
108
+ body.push("");
109
+ body.push(`${yc.dim("Other")}`);
110
+ for (const line of splitTable(defaultCommands)) body.push(` ${line}`);
111
+ } else body.push(...splitTable(defaultCommands));
67
112
  return {
68
113
  title: "Commands",
69
- body: splitTable([...commands.values()].map((command) => {
70
- if (command.__isAlias || command.help?.showInHelp === false) return null;
71
- return [`${yc.cyan(command.name)}${command.alias ? ` (${toArray(command.alias).join(", ")})` : ""}`, command.description];
72
- }).filter(isTruthy))
114
+ body
73
115
  };
74
116
  }
75
- renderFlags(flags) {
76
- return Object.entries(flags).map(([name, flag]) => {
77
- const flagName = formatFlagName(name);
78
- const aliases = (Array.isArray(flag.alias) ? flag.alias : flag.alias ? [flag.alias] : []).map(formatFlagName).join(", ");
79
- const description = flag.description ?? "";
80
- const type = formatFlagType(flag.type);
81
- const defaultValue = flag.default === void 0 ? "" : `[default: ${String(flag.default)}]`;
82
- return [
83
- yc.blue([flagName, aliases].filter(Boolean).join(", ")),
84
- yc.gray(type),
85
- description,
86
- yc.gray(defaultValue)
87
- ];
88
- });
117
+ renderFlagItem(name, flag) {
118
+ const flagName = formatFlagName(name);
119
+ const aliases = (Array.isArray(flag.alias) ? flag.alias : flag.alias ? [flag.alias] : []).map(formatFlagName).join(", ");
120
+ const type = this._formatters.formatFlagType(flag.type);
121
+ return [
122
+ yc.blue([flagName, aliases].filter(Boolean).join(", ")),
123
+ yc.gray(type),
124
+ flag.description,
125
+ flag.default !== void 0 && yc.gray(`[default: ${String(flag.default)}]`)
126
+ ].filter(isTruthy);
127
+ }
128
+ renderGroupedFlags(flags, groupMap, itemType) {
129
+ const groupedFlags = /* @__PURE__ */ new Map();
130
+ const defaultFlags = [];
131
+ for (const [name, flag] of Object.entries(flags)) {
132
+ const group = flag.help?.group;
133
+ validateGroup(group, groupMap, itemType, name);
134
+ const item = this.renderFlagItem(name, flag);
135
+ if (group && group !== DEFAULT_GROUP_KEY) {
136
+ const groupItems = groupedFlags.get(group) ?? [];
137
+ groupItems.push(item);
138
+ groupedFlags.set(group, groupItems);
139
+ } else defaultFlags.push(item);
140
+ }
141
+ const body = [];
142
+ for (const [key, name] of groupMap) {
143
+ const items = groupedFlags.get(key);
144
+ if (items && items.length > 0) {
145
+ if (body.length > 0) body.push("");
146
+ body.push(`${yc.dim(name)}`);
147
+ for (const line of splitTable(items)) body.push(` ${line}`);
148
+ }
149
+ }
150
+ if (defaultFlags.length > 0) if (body.length > 0) {
151
+ body.push("");
152
+ body.push(`${yc.dim("Other")}`);
153
+ for (const line of splitTable(defaultFlags)) body.push(` ${line}`);
154
+ } else body.push(...splitTable(defaultFlags));
155
+ return body;
89
156
  }
90
157
  renderCommandFlags() {
91
158
  const command = this._command;
92
159
  if (!command?.flags || objectIsEmpty(command.flags)) return;
93
160
  return {
94
161
  title: "Flags",
95
- body: splitTable(this.renderFlags(command.flags))
162
+ body: this.renderGroupedFlags(command.flags, this._flagGroups, "flag")
96
163
  };
97
164
  }
98
165
  renderGlobalFlags() {
99
166
  if (!this._globalFlags || objectIsEmpty(this._globalFlags)) return;
100
167
  return {
101
168
  title: "Global Flags",
102
- body: splitTable(this.renderFlags(this._globalFlags))
169
+ body: this.renderGroupedFlags(this._globalFlags, this._globalFlagGroups, "global flag")
103
170
  };
104
171
  }
105
172
  renderNotes() {
@@ -126,7 +193,11 @@ var HelpRenderer = class {
126
193
 
127
194
  //#endregion
128
195
  //#region src/index.ts
129
- const helpPlugin = ({ command = true, flag = true, showHelpWhenNoCommandSpecified = true, notes, examples, banner } = {}) => definePlugin({ setup: (cli) => {
196
+ const helpPlugin = ({ command = true, flag = true, showHelpWhenNoCommandSpecified = true, notes, examples, banner, formatters, groups } = {}) => definePlugin({ setup: (cli) => {
197
+ const mergedFormatters = {
198
+ ...defaultFormatters,
199
+ ...formatters
200
+ };
130
201
  const generalHelpNotes = [
131
202
  "If no command is specified, show help for the CLI.",
132
203
  "If a command is specified, show help for the command.",
@@ -140,7 +211,7 @@ const helpPlugin = ({ command = true, flag = true, showHelpWhenNoCommandSpecifie
140
211
  const effectiveNotes = notes ?? generalHelpNotes;
141
212
  const effectiveExamples = examples ?? generalHelpExamples;
142
213
  function printHelp(s) {
143
- if (banner) console.log(`${banner}`);
214
+ if (banner) console.log(banner);
144
215
  console.log(s);
145
216
  }
146
217
  if (command) cli.command("help", "Show help", {
@@ -159,7 +230,7 @@ const helpPlugin = ({ command = true, flag = true, showHelpWhenNoCommandSpecifie
159
230
  return;
160
231
  }
161
232
  }
162
- printHelp(new HelpRenderer(cli, cli._globalFlags, command$1, command$1 ? command$1.help?.notes : effectiveNotes, command$1 ? command$1.help?.examples : effectiveExamples).render());
233
+ printHelp(new HelpRenderer(mergedFormatters, cli, cli._globalFlags, command$1, command$1 ? command$1.help?.notes : effectiveNotes, command$1 ? command$1.help?.examples : effectiveExamples, groups).render());
163
234
  });
164
235
  if (flag) cli.globalFlag("help", "Show help", {
165
236
  alias: "h",
@@ -169,12 +240,12 @@ const helpPlugin = ({ command = true, flag = true, showHelpWhenNoCommandSpecifie
169
240
  cli.interceptor({
170
241
  enforce: "pre",
171
242
  handler: async (ctx, next) => {
172
- if (ctx.flags.help) printHelp(new HelpRenderer(cli, cli._globalFlags, ctx.command, ctx.command ? ctx.command.help?.notes : effectiveNotes, ctx.command ? ctx.command.help?.examples : effectiveExamples).render());
173
- else if (showHelpWhenNoCommandSpecified && !ctx.command && ctx.rawParsed.parameters.length === 0) printHelp(new HelpRenderer(cli, cli._globalFlags, void 0, effectiveNotes, effectiveExamples).render());
243
+ if (ctx.flags.help) printHelp(new HelpRenderer(mergedFormatters, cli, cli._globalFlags, ctx.command, ctx.command ? ctx.command.help?.notes : effectiveNotes, ctx.command ? ctx.command.help?.examples : effectiveExamples, groups).render());
244
+ else if (showHelpWhenNoCommandSpecified && !ctx.command && ctx.rawParsed.parameters.length === 0) printHelp(new HelpRenderer(mergedFormatters, cli, cli._globalFlags, void 0, effectiveNotes, effectiveExamples, groups).render());
174
245
  else await next();
175
246
  }
176
247
  });
177
248
  } });
178
249
 
179
250
  //#endregion
180
- export { helpPlugin };
251
+ export { defaultFormatters, helpPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clerc/plugin-help",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "author": "Ray <i@mk1.io> (https://github.com/so1ve)",
5
5
  "type": "module",
6
6
  "description": "Clerc plugin help",
@@ -49,11 +49,11 @@
49
49
  "string-width": "^8.1.0",
50
50
  "text-table": "^0.2.0",
51
51
  "yoctocolors": "^2.1.2",
52
- "@clerc/utils": "1.0.0-beta.1"
52
+ "@clerc/utils": "1.0.0-beta.3"
53
53
  },
54
54
  "devDependencies": {
55
- "@clerc/parser": "1.0.0-beta.1",
56
- "@clerc/core": "1.0.0-beta.1"
55
+ "@clerc/core": "1.0.0-beta.3",
56
+ "@clerc/parser": "1.0.0-beta.3"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@clerc/core": "*"