@silvery/commander 0.8.1 → 0.17.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/src/command.ts DELETED
@@ -1,321 +0,0 @@
1
- /**
2
- * Enhanced Commander Command with auto-colorized help, Standard Schema support,
3
- * and array-as-choices detection.
4
- *
5
- * Subclasses Commander's Command so `new Command("app")` just works --
6
- * it's Commander with auto-colorized help, automatic Standard Schema /
7
- * legacy Zod detection, and array choices in `.option()`.
8
- *
9
- * @example
10
- * ```ts
11
- * import { Command, port, csv } from "@silvery/commander"
12
- *
13
- * new Command("deploy")
14
- * .option("-p, --port <n>", "Port", port)
15
- * .option("--tags <t>", "Tags", csv)
16
- * .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
17
- *
18
- * program.parse()
19
- * ```
20
- */
21
-
22
- import { Command as BaseCommand, Help, Option } from "commander"
23
- import { colorizeHelp } from "./colorize.ts"
24
- import type { StandardSchemaV1 } from "./presets.ts"
25
-
26
- // --- Standard Schema support ---
27
-
28
- /** Runtime check: is this value a Standard Schema v1 object? */
29
- function isStandardSchema(value: unknown): value is StandardSchemaV1 {
30
- return typeof value === "object" && value !== null && "~standard" in (value as any)
31
- }
32
-
33
- /** Wrap a Standard Schema as a Commander parser function */
34
- function standardSchemaParser<T>(schema: StandardSchemaV1<T>): (value: string) => T {
35
- return (value: string) => {
36
- const result = schema["~standard"].validate(value)
37
- if ("issues" in result) {
38
- const msg = result.issues.map((i) => i.message).join(", ")
39
- throw new Error(msg)
40
- }
41
- return result.value
42
- }
43
- }
44
-
45
- // --- Legacy Zod support (pre-3.24, no ~standard) ---
46
-
47
- interface ZodLike<T = any> {
48
- parse(value: unknown): T
49
- _def: unknown
50
- }
51
-
52
- function isLegacyZodSchema(value: unknown): value is ZodLike {
53
- return (
54
- typeof value === "object" &&
55
- value !== null &&
56
- typeof (value as any).parse === "function" &&
57
- "_def" in (value as any) &&
58
- !("~standard" in (value as any))
59
- )
60
- }
61
-
62
- function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
63
- return (value: string) => {
64
- try {
65
- return schema.parse(value)
66
- } catch (err: any) {
67
- if (err?.issues) {
68
- const messages = err.issues.map((i: any) => i.message).join(", ")
69
- throw new Error(messages)
70
- }
71
- throw err
72
- }
73
- }
74
- }
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
-
112
- export class Command extends BaseCommand {
113
- private _helpSectionList: StoredSection[] = []
114
- private _helpSectionsInstalled = false
115
-
116
- constructor(name?: string) {
117
- super(name)
118
- colorizeHelp(this as any)
119
- this._capitalizeBuiltinDescriptions()
120
- }
121
-
122
- /**
123
- * Add an option with smart third-argument detection.
124
- *
125
- * The third argument is detected in order:
126
- * 1. **Array** -- treated as choices (Commander `.choices()`)
127
- * 2. **Standard Schema v1** -- wrapped as a parser function
128
- * 3. **Legacy Zod** (pre-3.24, has `_def` + `parse`) -- wrapped as a parser
129
- * 4. **Function** -- passed through as Commander's parser function
130
- * 5. **Anything else** -- passed through as a default value
131
- */
132
- override option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
133
- if (Array.isArray(parseArgOrDefault)) {
134
- const opt = new Option(flags, description ?? "").choices(parseArgOrDefault as string[])
135
- this.addOption(opt)
136
- return this
137
- }
138
- if (isStandardSchema(parseArgOrDefault)) {
139
- return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault), defaultValue)
140
- }
141
- if (isLegacyZodSchema(parseArgOrDefault)) {
142
- return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault), defaultValue)
143
- }
144
- if (typeof parseArgOrDefault === "function") {
145
- return super.option(flags, description ?? "", parseArgOrDefault, defaultValue)
146
- }
147
- return super.option(flags, description ?? "", parseArgOrDefault)
148
- }
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
-
199
- // Subcommands also get colorized help, Standard Schema, and array choices
200
- override createCommand(name?: string): Command {
201
- return new Command(name)
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
- }
321
- }
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- // Enhanced Commander
2
- export { Command, type HelpSectionPosition, type HelpSectionContent } from "./command.ts"
3
- export { colorizeHelp, shouldColorize, type ColorizeHelpOptions, type CommandLike } from "./colorize.ts"
4
-
5
- // Re-export Commander's other classes
6
- export { Option, Argument, CommanderError, InvalidArgumentError, Help } from "commander"
7
- export type { OptionValues } from "commander"
8
-
9
- // Built-in types and Standard Schema
10
- export { int, uint, float, port, url, path, csv, json, bool, date, email, regex, intRange } from "./presets.ts"
11
- export type { CLIType, StandardSchemaV1 } from "./presets.ts"
12
-
13
- // Tree-shakeable: only evaluated if user imports z
14
- export { z } from "./z.ts"
package/src/plain.ts DELETED
@@ -1,22 +0,0 @@
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"
package/src/presets.ts DELETED
@@ -1,186 +0,0 @@
1
- /**
2
- * Built-in CLI types — Standard Schema v1 validators for common CLI argument patterns.
3
- *
4
- * Zero dependencies — validation is manual, no Zod/Valibot/ArkType required.
5
- * Each type implements Standard Schema v1 for interop with any schema library,
6
- * plus standalone `.parse()` and `.safeParse()` convenience methods.
7
- *
8
- * @example
9
- * ```ts
10
- * import { Command, port, csv } from "@silvery/commander"
11
- *
12
- * new Command("deploy")
13
- * .option("-p, --port <n>", "Port", port) // number (1-65535)
14
- * .option("-r, --retries <n>", "Retries", int) // number (integer)
15
- * .option("--tags <t>", "Tags", csv) // string[]
16
- * .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
17
- *
18
- * // Standalone usage (outside Commander)
19
- * port.parse("3000") // 3000
20
- * port.safeParse("abc") // { success: false, issues: [{ message: "..." }] }
21
- * ```
22
- */
23
-
24
- /**
25
- * Standard Schema v1 interface — the universal schema interop protocol.
26
- * Supports any schema library that implements Standard Schema (Zod >=3.24,
27
- * Valibot >=1.0, ArkType >=2.0, etc.).
28
- *
29
- * Inlined to avoid any dependency on @standard-schema/spec.
30
- * See: https://github.com/standard-schema/standard-schema
31
- */
32
- export interface StandardSchemaV1<T = unknown> {
33
- readonly "~standard": {
34
- readonly version: 1
35
- readonly vendor: string
36
- readonly validate: (
37
- value: unknown,
38
- ) => { value: T } | { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray<unknown> }> }
39
- }
40
- }
41
-
42
- /** A Standard Schema v1 CLI type with standalone parse/safeParse methods. */
43
- export interface CLIType<T> extends StandardSchemaV1<T> {
44
- /** Parse and validate a value, throwing on failure. */
45
- parse(value: unknown): T
46
- /** Parse and validate a value, returning a result object. */
47
- safeParse(value: unknown): { success: true; value: T } | { success: false; issues: Array<{ message: string }> }
48
- }
49
-
50
- function createType<T>(vendor: string, validate: (value: unknown) => T): CLIType<T> {
51
- const schema: CLIType<T> = {
52
- "~standard": {
53
- version: 1,
54
- vendor,
55
- validate: (value) => {
56
- try {
57
- return { value: validate(value) }
58
- } catch (e: any) {
59
- return { issues: [{ message: e.message }] }
60
- }
61
- },
62
- },
63
- parse(value: unknown): T {
64
- const result = schema["~standard"].validate(value)
65
- if ("issues" in result) throw new Error(result.issues[0]?.message ?? "Validation failed")
66
- return result.value
67
- },
68
- safeParse(value: unknown) {
69
- const result = schema["~standard"].validate(value)
70
- if ("issues" in result) return { success: false as const, issues: [...result.issues] }
71
- return { success: true as const, value: result.value }
72
- },
73
- }
74
- return schema
75
- }
76
-
77
- const VENDOR = "@silvery/commander"
78
-
79
- /** Integer (coerced from string). */
80
- export const int = createType<number>(VENDOR, (v) => {
81
- const s = String(v).trim()
82
- if (s === "") throw new Error(`Expected integer, got "${v}"`)
83
- const n = Number(s)
84
- if (!Number.isInteger(n)) throw new Error(`Expected integer, got "${v}"`)
85
- return n
86
- })
87
-
88
- /** Unsigned integer (>= 0, coerced from string). */
89
- export const uint = createType<number>(VENDOR, (v) => {
90
- const s = String(v).trim()
91
- if (s === "") throw new Error(`Expected unsigned integer (>= 0), got "${v}"`)
92
- const n = Number(s)
93
- if (!Number.isInteger(n) || n < 0) throw new Error(`Expected unsigned integer (>= 0), got "${v}"`)
94
- return n
95
- })
96
-
97
- /** Float (coerced from string). */
98
- export const float = createType<number>(VENDOR, (v) => {
99
- const s = String(v).trim()
100
- if (s === "" || s === "NaN") throw new Error(`Expected number, got "${v}"`)
101
- const n = Number(s)
102
- if (Number.isNaN(n)) throw new Error(`Expected number, got "${v}"`)
103
- return n
104
- })
105
-
106
- /** Port number (1-65535). */
107
- export const port = createType<number>(VENDOR, (v) => {
108
- const n = Number(v)
109
- if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error(`Expected port (1-65535), got "${v}"`)
110
- return n
111
- })
112
-
113
- /** URL (validated via URL constructor). */
114
- export const url = createType<string>(VENDOR, (v) => {
115
- const s = String(v)
116
- try {
117
- new URL(s)
118
- return s
119
- } catch {
120
- throw new Error(`Expected valid URL, got "${v}"`)
121
- }
122
- })
123
-
124
- /** File path (non-empty string). */
125
- export const path = createType<string>(VENDOR, (v) => {
126
- const s = String(v)
127
- if (!s) throw new Error("Expected non-empty path")
128
- return s
129
- })
130
-
131
- /** Comma-separated values to string[]. */
132
- export const csv = createType<string[]>(VENDOR, (v) => {
133
- return String(v)
134
- .split(",")
135
- .map((s) => s.trim())
136
- .filter(Boolean)
137
- })
138
-
139
- /** JSON string to parsed value. */
140
- export const json = createType<unknown>(VENDOR, (v) => {
141
- try {
142
- return JSON.parse(String(v))
143
- } catch {
144
- throw new Error(`Expected valid JSON, got "${v}"`)
145
- }
146
- })
147
-
148
- /** Boolean string ("true"/"false"/"1"/"0"/"yes"/"no"). */
149
- export const bool = createType<boolean>(VENDOR, (v) => {
150
- const s = String(v).toLowerCase()
151
- if (["true", "1", "yes", "y"].includes(s)) return true
152
- if (["false", "0", "no", "n"].includes(s)) return false
153
- throw new Error(`Expected boolean (true/false/yes/no/1/0), got "${v}"`)
154
- })
155
-
156
- /** Date string to Date object. */
157
- export const date = createType<Date>(VENDOR, (v) => {
158
- const d = new Date(String(v))
159
- if (isNaN(d.getTime())) throw new Error(`Expected valid date, got "${v}"`)
160
- return d
161
- })
162
-
163
- /** Email address (basic validation). */
164
- export const email = createType<string>(VENDOR, (v) => {
165
- const s = String(v)
166
- if (!s.includes("@") || !s.includes(".")) throw new Error(`Expected email address, got "${v}"`)
167
- return s
168
- })
169
-
170
- /** Regex pattern string to RegExp. */
171
- export const regex = createType<RegExp>(VENDOR, (v) => {
172
- try {
173
- return new RegExp(String(v))
174
- } catch {
175
- throw new Error(`Expected valid regex, got "${v}"`)
176
- }
177
- })
178
-
179
- /** Integer with min/max bounds (factory). */
180
- export function intRange(min: number, max: number): CLIType<number> {
181
- return createType<number>(VENDOR, (v) => {
182
- const n = Number(v)
183
- if (!Number.isInteger(n) || n < min || n > max) throw new Error(`Expected integer ${min}-${max}, got "${v}"`)
184
- return n
185
- })
186
- }
package/src/z.ts DELETED
@@ -1,44 +0,0 @@
1
- /**
2
- * Extended Zod object with CLI types.
3
- *
4
- * Spreads all of Zod's exports and adds CLI-specific schemas.
5
- * Tree-shakeable -- only evaluated if user imports `z`.
6
- *
7
- * @example
8
- * ```ts
9
- * import { z, Command } from "@silvery/commander"
10
- *
11
- * new Command("deploy")
12
- * .option("-p, --port <n>", "Port", z.port)
13
- * .option("--tags <t>", "Tags", z.csv)
14
- * .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
15
- *
16
- * program.parse()
17
- * ```
18
- */
19
-
20
- import * as zod from "zod"
21
-
22
- export const z = {
23
- ...zod,
24
- // CLI types (built on Zod schemas)
25
- port: zod.coerce.number().int().min(1).max(65535),
26
- int: zod.coerce.number().int(),
27
- uint: zod.coerce.number().int().min(0),
28
- float: zod.coerce.number(),
29
- csv: zod.string().transform((v: string) =>
30
- v
31
- .split(",")
32
- .map((s: string) => s.trim())
33
- .filter(Boolean),
34
- ),
35
- url: zod.string().url(),
36
- path: zod.string().min(1),
37
- email: zod.string().email(),
38
- date: zod.coerce.date(),
39
- json: zod.string().transform((v: string) => JSON.parse(v)),
40
- bool: zod
41
- .enum(["true", "false", "1", "0", "yes", "no", "y", "n"] as const)
42
- .transform((v: string) => ["true", "1", "yes", "y"].includes(v)),
43
- intRange: (min: number, max: number) => zod.coerce.number().int().min(min).max(max),
44
- }