@silvery/commander 0.7.0 → 0.8.0
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/package.json +3 -2
- package/src/colorize.ts +10 -9
- package/src/command.ts +208 -8
- package/src/index.ts +1 -1
- package/src/plain.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvery/commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Colorized Commander.js help output using ANSI escape codes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
"sideEffects": false,
|
|
24
24
|
"exports": {
|
|
25
25
|
".": "./src/index.ts",
|
|
26
|
+
"./plain": "./src/plain.ts",
|
|
26
27
|
"./parse": "./src/presets.ts"
|
|
27
28
|
},
|
|
28
29
|
"publishConfig": {
|
|
29
30
|
"access": "public"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@silvery/ansi": ">=
|
|
33
|
+
"@silvery/ansi": ">=0.3.0",
|
|
33
34
|
"commander": ">=12.0.0"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
package/src/colorize.ts
CHANGED
|
@@ -45,15 +45,15 @@ export interface CommandLike {
|
|
|
45
45
|
|
|
46
46
|
/** Color scheme for help output. Each value is a styling function (text → styled text). */
|
|
47
47
|
export interface ColorizeHelpOptions {
|
|
48
|
-
/** Style for command/subcommand names. Default:
|
|
48
|
+
/** Style for command/subcommand names. Default: primary (yellow without theme) */
|
|
49
49
|
commands?: (text: string) => string
|
|
50
|
-
/** Style for --flags and -short options. Default:
|
|
50
|
+
/** Style for --flags and -short options. Default: secondary (cyan without theme) */
|
|
51
51
|
flags?: (text: string) => string
|
|
52
|
-
/** Style for description text. Default:
|
|
52
|
+
/** Style for description text. Default: unstyled (normal foreground) */
|
|
53
53
|
description?: (text: string) => string
|
|
54
54
|
/** Style for section headings (Usage:, Options:, etc.). Default: bold */
|
|
55
55
|
heading?: (text: string) => string
|
|
56
|
-
/** Style for <required> and [optional] argument brackets. Default:
|
|
56
|
+
/** Style for <required> and [optional] argument brackets. Default: accent (magenta without theme) */
|
|
57
57
|
brackets?: (text: string) => string
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -73,11 +73,12 @@ export function colorizeHelp(program: CommandLike, options?: ColorizeHelpOptions
|
|
|
73
73
|
// handles the final strip via configureOutput.
|
|
74
74
|
if (s.level === 0) s.level = 1
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
76
|
+
// Semantic token fallback: theme token → named color
|
|
77
|
+
const cmds = options?.commands ?? ((t: string) => s.primary(t))
|
|
78
|
+
const flags = options?.flags ?? ((t: string) => s.secondary(t))
|
|
79
|
+
const desc = options?.description ?? ((t: string) => t)
|
|
79
80
|
const heading = options?.heading ?? ((t: string) => s.bold(t))
|
|
80
|
-
const brackets = options?.brackets ?? ((t: string) => s.
|
|
81
|
+
const brackets = options?.brackets ?? ((t: string) => s.accent(t))
|
|
81
82
|
|
|
82
83
|
const helpConfig: Record<string, unknown> = {
|
|
83
84
|
styleTitle: (str: string) => heading(str),
|
|
@@ -86,7 +87,7 @@ export function colorizeHelp(program: CommandLike, options?: ColorizeHelpOptions
|
|
|
86
87
|
styleSubcommandText: (str: string) => cmds(str),
|
|
87
88
|
styleArgumentText: (str: string) => brackets(str),
|
|
88
89
|
styleDescriptionText: (str: string) => desc(str),
|
|
89
|
-
styleCommandDescription: (str: string) => str,
|
|
90
|
+
styleCommandDescription: (str: string) => s.bold.primary(str),
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
program.configureHelp(helpConfig)
|
package/src/command.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { Command as BaseCommand, Option } from "commander"
|
|
22
|
+
import { Command as BaseCommand, Help, Option } from "commander"
|
|
23
23
|
import { colorizeHelp } from "./colorize.ts"
|
|
24
24
|
import type { StandardSchemaV1 } from "./presets.ts"
|
|
25
25
|
|
|
@@ -44,16 +44,11 @@ function standardSchemaParser<T>(schema: StandardSchemaV1<T>): (value: string) =
|
|
|
44
44
|
|
|
45
45
|
// --- Legacy Zod support (pre-3.24, no ~standard) ---
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
* Duck-type interface for older Zod schemas that don't implement Standard Schema.
|
|
49
|
-
* Any object with `parse(value: string) => T` and `_def` qualifies.
|
|
50
|
-
*/
|
|
51
47
|
interface ZodLike<T = any> {
|
|
52
48
|
parse(value: unknown): T
|
|
53
49
|
_def: unknown
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
/** Runtime check: is this value a legacy Zod-like schema (without Standard Schema)? */
|
|
57
52
|
function isLegacyZodSchema(value: unknown): value is ZodLike {
|
|
58
53
|
return (
|
|
59
54
|
typeof value === "object" &&
|
|
@@ -64,13 +59,11 @@ function isLegacyZodSchema(value: unknown): value is ZodLike {
|
|
|
64
59
|
)
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
/** Wrap a legacy Zod schema as a Commander parser function */
|
|
68
62
|
function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
69
63
|
return (value: string) => {
|
|
70
64
|
try {
|
|
71
65
|
return schema.parse(value)
|
|
72
66
|
} catch (err: any) {
|
|
73
|
-
// Format Zod errors as Commander-style messages
|
|
74
67
|
if (err?.issues) {
|
|
75
68
|
const messages = err.issues.map((i: any) => i.message).join(", ")
|
|
76
69
|
throw new Error(messages)
|
|
@@ -80,10 +73,50 @@ function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
|
80
73
|
}
|
|
81
74
|
}
|
|
82
75
|
|
|
76
|
+
// --- Help Section Types ---
|
|
77
|
+
|
|
78
|
+
/** Position for help sections — mirrors Commander's addHelpText positions. */
|
|
79
|
+
export type HelpSectionPosition = "beforeAll" | "before" | "after" | "afterAll"
|
|
80
|
+
|
|
81
|
+
/** Content for a help section: rows of [term, description] pairs, or free-form text. */
|
|
82
|
+
export type HelpSectionContent = [string, string][] | string
|
|
83
|
+
|
|
84
|
+
// Internal storage
|
|
85
|
+
interface StoredSection {
|
|
86
|
+
position: HelpSectionPosition
|
|
87
|
+
title: string
|
|
88
|
+
content: HelpSectionContent
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Style a section term using Commander's style hooks.
|
|
93
|
+
* Splits the term into segments: option-like (-f), argument brackets (<arg>, [opt]),
|
|
94
|
+
* and command words — each styled with the appropriate hook.
|
|
95
|
+
*/
|
|
96
|
+
function styleSectionTerm(term: string, helper: any): string {
|
|
97
|
+
// Option-like terms: entire term styled as option
|
|
98
|
+
if (/^\s*-/.test(term)) return helper.styleOptionText(term)
|
|
99
|
+
|
|
100
|
+
// Mixed terms: style <arg>/[opt] as arguments, "quoted" as literal values, rest as commands
|
|
101
|
+
return term.replace(
|
|
102
|
+
/(<[^>]+>|\[[^\]]+\])|("[^"]*")|([^<["[\]]+)/g,
|
|
103
|
+
(_match, bracket: string, quoted: string, text: string) => {
|
|
104
|
+
if (bracket) return helper.styleArgumentText(bracket)
|
|
105
|
+
if (quoted) return quoted // literal values — default foreground (quotes distinguish them)
|
|
106
|
+
if (text) return helper.styleCommandText(text)
|
|
107
|
+
return ""
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
83
112
|
export class Command extends BaseCommand {
|
|
113
|
+
private _helpSectionList: StoredSection[] = []
|
|
114
|
+
private _helpSectionsInstalled = false
|
|
115
|
+
|
|
84
116
|
constructor(name?: string) {
|
|
85
117
|
super(name)
|
|
86
118
|
colorizeHelp(this as any)
|
|
119
|
+
this._capitalizeBuiltinDescriptions()
|
|
87
120
|
}
|
|
88
121
|
|
|
89
122
|
/**
|
|
@@ -114,8 +147,175 @@ export class Command extends BaseCommand {
|
|
|
114
147
|
return super.option(flags, description ?? "", parseArgOrDefault)
|
|
115
148
|
}
|
|
116
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Add a styled help section — like Commander's `addHelpText` but with
|
|
152
|
+
* structured content that participates in global column alignment.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* // Rows with aligned descriptions (default position: "after")
|
|
157
|
+
* program.addHelpSection("Getting Started:", [
|
|
158
|
+
* ["myapp init", "Initialize a new project"],
|
|
159
|
+
* ["myapp serve", "Start the dev server"],
|
|
160
|
+
* ])
|
|
161
|
+
*
|
|
162
|
+
* // Free-form text
|
|
163
|
+
* program.addHelpSection("Note:", "Requires Node.js 23+")
|
|
164
|
+
*
|
|
165
|
+
* // Explicit position
|
|
166
|
+
* program.addHelpSection("before", "Prerequisites:", [
|
|
167
|
+
* ["node >= 23", "Required runtime"],
|
|
168
|
+
* ])
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
addHelpSection(title: string, content: HelpSectionContent): this
|
|
172
|
+
addHelpSection(position: HelpSectionPosition, title: string, content: HelpSectionContent): this
|
|
173
|
+
addHelpSection(
|
|
174
|
+
positionOrTitle: HelpSectionPosition | string,
|
|
175
|
+
titleOrContent: string | HelpSectionContent,
|
|
176
|
+
content?: HelpSectionContent,
|
|
177
|
+
): this {
|
|
178
|
+
let position: HelpSectionPosition
|
|
179
|
+
let title: string
|
|
180
|
+
let body: HelpSectionContent
|
|
181
|
+
|
|
182
|
+
if (content !== undefined) {
|
|
183
|
+
// 3-arg: addHelpSection(position, title, content)
|
|
184
|
+
position = positionOrTitle as HelpSectionPosition
|
|
185
|
+
title = titleOrContent as string
|
|
186
|
+
body = content
|
|
187
|
+
} else {
|
|
188
|
+
// 2-arg: addHelpSection(title, content) — defaults to "after"
|
|
189
|
+
position = "after"
|
|
190
|
+
title = positionOrTitle
|
|
191
|
+
body = titleOrContent as HelpSectionContent
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this._helpSectionList.push({ position, title, content: body })
|
|
195
|
+
this._installHelpSectionHooks()
|
|
196
|
+
return this
|
|
197
|
+
}
|
|
198
|
+
|
|
117
199
|
// Subcommands also get colorized help, Standard Schema, and array choices
|
|
118
200
|
override createCommand(name?: string): Command {
|
|
119
201
|
return new Command(name)
|
|
120
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Auto-detect capitalization from user-provided descriptions and match it
|
|
206
|
+
* for built-in options (-V/--version, -h/--help).
|
|
207
|
+
*/
|
|
208
|
+
private _capitalizeBuiltinDescriptions(): void {
|
|
209
|
+
const origHelp = this.helpInformation.bind(this)
|
|
210
|
+
this.helpInformation = () => {
|
|
211
|
+
const builtinFlags = new Set(["-V, --version", "-h, --help"])
|
|
212
|
+
const userDescs = this.options
|
|
213
|
+
.filter((opt) => !builtinFlags.has(opt.flags) && opt.description && /^[a-zA-Z]/.test(opt.description))
|
|
214
|
+
.map((opt) => opt.description!)
|
|
215
|
+
for (const cmd of this.commands) {
|
|
216
|
+
if (cmd.description() && /^[a-zA-Z]/.test(cmd.description())) {
|
|
217
|
+
userDescs.push(cmd.description())
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (userDescs.length > 0) {
|
|
221
|
+
const capitalCount = userDescs.filter((d) => /^[A-Z]/.test(d)).length
|
|
222
|
+
if (capitalCount > userDescs.length / 2) {
|
|
223
|
+
for (const opt of this.options) {
|
|
224
|
+
if (builtinFlags.has(opt.flags) && opt.description && /^[a-z]/.test(opt.description)) {
|
|
225
|
+
opt.description = opt.description[0]!.toUpperCase() + opt.description.slice(1)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const helpOpt = (this as any)._helpOption
|
|
229
|
+
if (helpOpt?.description && /^[a-z]/.test(helpOpt.description)) {
|
|
230
|
+
helpOpt.description = helpOpt.description[0]!.toUpperCase() + helpOpt.description.slice(1)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return origHelp()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Render sections for a given position using the help formatter. */
|
|
239
|
+
private _renderSections(position: HelpSectionPosition, helper: any, termWidth: number): string {
|
|
240
|
+
const sections = this._helpSectionList.filter((s) => s.position === position)
|
|
241
|
+
if (sections.length === 0) return ""
|
|
242
|
+
|
|
243
|
+
const lines: string[] = []
|
|
244
|
+
for (const section of sections) {
|
|
245
|
+
lines.push("")
|
|
246
|
+
lines.push(helper.styleTitle(section.title))
|
|
247
|
+
if (typeof section.content === "string") {
|
|
248
|
+
for (const line of section.content.split("\n")) {
|
|
249
|
+
lines.push(` ${line}`)
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
for (const [term, desc] of section.content) {
|
|
253
|
+
const styleTerm = styleSectionTerm(term, helper)
|
|
254
|
+
lines.push(helper.formatItem(styleTerm, termWidth, helper.styleDescriptionText(desc), helper))
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lines.push("")
|
|
259
|
+
return lines.join("\n")
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Install hooks once — merges with existing configureHelp, adds addHelpText for before/afterAll. */
|
|
263
|
+
private _installHelpSectionHooks(): void {
|
|
264
|
+
if (this._helpSectionsInstalled) return
|
|
265
|
+
this._helpSectionsInstalled = true
|
|
266
|
+
|
|
267
|
+
const self = this
|
|
268
|
+
const existing = (this as any)._helpConfiguration ?? {}
|
|
269
|
+
const origPadWidth = existing.padWidth
|
|
270
|
+
const origFormatHelp = existing.formatHelp
|
|
271
|
+
const protoFormatHelp = Help.prototype.formatHelp
|
|
272
|
+
|
|
273
|
+
this.configureHelp({
|
|
274
|
+
...existing,
|
|
275
|
+
// Include section term widths in global column alignment
|
|
276
|
+
padWidth(cmd: any, helper: any) {
|
|
277
|
+
const base = origPadWidth
|
|
278
|
+
? origPadWidth(cmd, helper)
|
|
279
|
+
: Math.max(
|
|
280
|
+
helper.longestOptionTermLength(cmd, helper),
|
|
281
|
+
helper.longestGlobalOptionTermLength?.(cmd, helper) ?? 0,
|
|
282
|
+
helper.longestSubcommandTermLength(cmd, helper),
|
|
283
|
+
helper.longestArgumentTermLength(cmd, helper),
|
|
284
|
+
)
|
|
285
|
+
let sectionMax = 0
|
|
286
|
+
for (const section of self._helpSectionList) {
|
|
287
|
+
if (typeof section.content !== "string") {
|
|
288
|
+
for (const [term] of section.content) {
|
|
289
|
+
if (term.length > sectionMax) sectionMax = term.length
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return Math.max(base, sectionMax)
|
|
294
|
+
},
|
|
295
|
+
// Render "before" and "after" sections inside formatHelp
|
|
296
|
+
formatHelp(cmd: any, helper: any) {
|
|
297
|
+
const baseHelp = origFormatHelp ? origFormatHelp(cmd, helper) : protoFormatHelp.call(helper, cmd, helper)
|
|
298
|
+
const termWidth = helper.padWidth(cmd, helper)
|
|
299
|
+
const before = self._renderSections("before", helper, termWidth)
|
|
300
|
+
const after = self._renderSections("after", helper, termWidth)
|
|
301
|
+
|
|
302
|
+
// "before" goes after the Usage+Description but before Options.
|
|
303
|
+
// Since we can't inject mid-formatHelp easily, prepend before baseHelp.
|
|
304
|
+
// "after" appends at the end.
|
|
305
|
+
return (before ? before + "\n" : "") + baseHelp + after
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// "beforeAll" and "afterAll" use Commander's addHelpText (propagates to subcommands)
|
|
310
|
+
this.addHelpText("beforeAll", () => {
|
|
311
|
+
const helper = this.createHelp()
|
|
312
|
+
const termWidth = helper.padWidth(this, helper)
|
|
313
|
+
return this._renderSections("beforeAll", helper, termWidth)
|
|
314
|
+
})
|
|
315
|
+
this.addHelpText("afterAll", () => {
|
|
316
|
+
const helper = this.createHelp()
|
|
317
|
+
const termWidth = helper.padWidth(this, helper)
|
|
318
|
+
return this._renderSections("afterAll", helper, termWidth)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
121
321
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Enhanced Commander
|
|
2
|
-
export { Command } from "./command.ts"
|
|
2
|
+
export { Command, type HelpSectionPosition, type HelpSectionContent } from "./command.ts"
|
|
3
3
|
export { colorizeHelp, shouldColorize, type ColorizeHelpOptions, type CommandLike } from "./colorize.ts"
|
|
4
4
|
|
|
5
5
|
// Re-export Commander's other classes
|
package/src/plain.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain Commander — Standard Schema presets without auto-colorization.
|
|
3
|
+
*
|
|
4
|
+
* Exports the base Commander Command (no auto-colorized help) plus the
|
|
5
|
+
* same typed option presets (int, uint, port, csv, intRange). Does NOT
|
|
6
|
+
* import @silvery/ansi — zero styling overhead.
|
|
7
|
+
*
|
|
8
|
+
* For colorization, import `colorizeHelp` from `@silvery/commander` (main entry).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { Command, port, csv } from "@silvery/commander/plain"
|
|
13
|
+
*
|
|
14
|
+
* const program = new Command("myapp")
|
|
15
|
+
* .option("-p, --port <n>", "Port", port)
|
|
16
|
+
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { Command } from "commander"
|
|
21
|
+
export { int, uint, port, csv, intRange } from "./presets.ts"
|
|
22
|
+
export type { StandardSchemaV1 } from "./presets.ts"
|