@silvery/commander 0.3.0 → 0.5.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/README.md +107 -127
- package/package.json +6 -18
- package/src/colorize.ts +164 -0
- package/src/command.ts +121 -0
- package/src/index.ts +11 -173
- package/src/presets.ts +41 -36
- package/src/z.ts +44 -0
- package/src/typed.ts +0 -465
package/src/index.ts
CHANGED
|
@@ -1,176 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
export { Command
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
// Re-export typed CLI
|
|
6
|
-
export { TypedCommand, createCLI } from "./typed.ts"
|
|
7
|
-
export type { StandardSchemaV1 } from "./typed.ts"
|
|
8
|
-
|
|
9
|
-
// Re-export presets
|
|
10
|
-
export { int, uint, float, port, url, path, csv, json, bool, date, email, regex, intRange, oneOf } from "./presets.ts"
|
|
11
|
-
export type { Preset } from "./presets.ts"
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Commander.js help colorization using ANSI escape codes.
|
|
15
|
-
*
|
|
16
|
-
* Uses Commander's built-in style hooks (styleTitle, styleOptionText, etc.)
|
|
17
|
-
* rather than regex post-processing. Works with @silvery/commander
|
|
18
|
-
* or plain commander — accepts a minimal CommandLike interface so Commander
|
|
19
|
-
* is a peer dependency, not a hard one.
|
|
20
|
-
*
|
|
21
|
-
* Zero dependencies — only raw ANSI escape codes.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* import { Command } from "@silvery/commander"
|
|
26
|
-
* import { colorizeHelp } from "@silvery/commander"
|
|
27
|
-
*
|
|
28
|
-
* const program = new Command("myapp").description("My CLI tool")
|
|
29
|
-
* colorizeHelp(program)
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
// Raw ANSI escape codes — no framework dependencies.
|
|
34
|
-
const RESET = "\x1b[0m"
|
|
35
|
-
const BOLD = "\x1b[1m"
|
|
36
|
-
const DIM = "\x1b[2m"
|
|
37
|
-
const CYAN = "\x1b[36m"
|
|
38
|
-
const GREEN = "\x1b[32m"
|
|
39
|
-
const YELLOW = "\x1b[33m"
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Check if color output should be enabled.
|
|
43
|
-
* Uses @silvery/ansi detectColor() if available, falls back to basic
|
|
44
|
-
* NO_COLOR/FORCE_COLOR/isTTY checks.
|
|
45
|
-
*/
|
|
46
|
-
let _shouldColorize: boolean | undefined
|
|
47
|
-
|
|
48
|
-
export function shouldColorize(): boolean {
|
|
49
|
-
if (_shouldColorize !== undefined) return _shouldColorize
|
|
50
|
-
|
|
51
|
-
// Try @silvery/ansi for full detection (respects NO_COLOR, FORCE_COLOR, TERM, etc.)
|
|
52
|
-
try {
|
|
53
|
-
const { detectColor } = require("@silvery/ansi") as { detectColor: (stdout: NodeJS.WriteStream) => string | null }
|
|
54
|
-
_shouldColorize = detectColor(process.stdout) !== null
|
|
55
|
-
} catch {
|
|
56
|
-
// Fallback: basic NO_COLOR / FORCE_COLOR / isTTY checks
|
|
57
|
-
if (process.env.NO_COLOR !== undefined) {
|
|
58
|
-
_shouldColorize = false
|
|
59
|
-
} else if (process.env.FORCE_COLOR !== undefined) {
|
|
60
|
-
_shouldColorize = true
|
|
61
|
-
} else {
|
|
62
|
-
_shouldColorize = process.stdout?.isTTY ?? true
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return _shouldColorize
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Wrap a string with ANSI codes, handling nested resets. */
|
|
70
|
-
function ansi(text: string, code: string): string {
|
|
71
|
-
return `${code}${text}${RESET}`
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Minimal interface for Commander's Command — avoids requiring Commander
|
|
76
|
-
* as a direct dependency. Works with both `commander` and
|
|
77
|
-
* `@silvery/commander`.
|
|
78
|
-
*
|
|
79
|
-
* Uses permissive types to ensure structural compatibility with all
|
|
80
|
-
* Commander versions, overloads, and generic instantiations.
|
|
81
|
-
*/
|
|
82
|
-
export interface CommandLike {
|
|
83
|
-
// biome-ignore lint: permissive to match Commander's overloaded signatures
|
|
84
|
-
configureHelp(...args: any[]): any
|
|
85
|
-
// biome-ignore lint: permissive to match Commander's overloaded signatures
|
|
86
|
-
configureOutput(...args: any[]): any
|
|
87
|
-
// biome-ignore lint: permissive to match Commander's Command[] structurally
|
|
88
|
-
readonly commands: readonly any[]
|
|
89
|
-
}
|
|
1
|
+
// Enhanced Commander
|
|
2
|
+
export { Command } from "./command.ts"
|
|
3
|
+
export { colorizeHelp, shouldColorize, type ColorizeHelpOptions, type CommandLike } from "./colorize.ts"
|
|
90
4
|
|
|
91
|
-
|
|
92
|
-
export
|
|
93
|
-
|
|
94
|
-
commands?: string
|
|
95
|
-
/** ANSI code for --flags and -short options. Default: green */
|
|
96
|
-
flags?: string
|
|
97
|
-
/** ANSI code for description text. Default: dim */
|
|
98
|
-
description?: string
|
|
99
|
-
/** ANSI code for section headings (Usage:, Options:, etc.). Default: bold */
|
|
100
|
-
heading?: string
|
|
101
|
-
/** ANSI code for <required> and [optional] argument brackets. Default: yellow */
|
|
102
|
-
brackets?: string
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Apply colorized help output to a Commander.js program and all its subcommands.
|
|
107
|
-
*
|
|
108
|
-
* Uses Commander's built-in `configureHelp()` style hooks rather than
|
|
109
|
-
* post-processing the formatted string. This approach is robust against
|
|
110
|
-
* formatting changes in Commander and handles wrapping correctly.
|
|
111
|
-
*
|
|
112
|
-
* @param program - A Commander Command instance (or compatible object)
|
|
113
|
-
* @param options - Override default ANSI color codes for each element
|
|
114
|
-
*/
|
|
115
|
-
export function colorizeHelp(program: CommandLike, options?: ColorizeHelpOptions): void {
|
|
116
|
-
const cmds = options?.commands ?? CYAN
|
|
117
|
-
const flags = options?.flags ?? GREEN
|
|
118
|
-
const desc = options?.description ?? DIM
|
|
119
|
-
const heading = options?.heading ?? BOLD
|
|
120
|
-
const brackets = options?.brackets ?? YELLOW
|
|
121
|
-
|
|
122
|
-
const helpConfig: Record<string, unknown> = {
|
|
123
|
-
// Section headings: "Usage:", "Options:", "Commands:", "Arguments:"
|
|
124
|
-
styleTitle(str: string): string {
|
|
125
|
-
return ansi(str, heading)
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
// Command name in usage line and subcommand terms
|
|
129
|
-
styleCommandText(str: string): string {
|
|
130
|
-
return ansi(str, cmds)
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
// Option terms: "-v, --verbose", "--repo <path>", "[options]"
|
|
134
|
-
styleOptionText(str: string): string {
|
|
135
|
-
return ansi(str, flags)
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
// Subcommand names in the commands list
|
|
139
|
-
styleSubcommandText(str: string): string {
|
|
140
|
-
return ansi(str, cmds)
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
// Argument terms: "<file>", "[dir]"
|
|
144
|
-
styleArgumentText(str: string): string {
|
|
145
|
-
return ansi(str, brackets)
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
// Description text for options, subcommands, arguments
|
|
149
|
-
styleDescriptionText(str: string): string {
|
|
150
|
-
return ansi(str, desc)
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
// Command description (the main program description line) — keep normal
|
|
154
|
-
styleCommandDescription(str: string): string {
|
|
155
|
-
return str
|
|
156
|
-
},
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
program.configureHelp(helpConfig)
|
|
5
|
+
// Re-export Commander's other classes
|
|
6
|
+
export { Option, Argument, CommanderError, InvalidArgumentError, Help } from "commander"
|
|
7
|
+
export type { OptionValues } from "commander"
|
|
160
8
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
// Callers who want to respect NO_COLOR/FORCE_COLOR should check
|
|
166
|
-
// shouldColorize() before calling colorizeHelp().
|
|
167
|
-
program.configureOutput({
|
|
168
|
-
getOutHasColors: () => true,
|
|
169
|
-
getErrHasColors: () => true,
|
|
170
|
-
})
|
|
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"
|
|
171
12
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
colorizeHelp(sub, options)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
13
|
+
// Tree-shakeable: only evaluated if user imports z
|
|
14
|
+
export { z } from "./z.ts"
|
package/src/presets.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Built-in CLI types — Standard Schema v1 validators for common CLI argument patterns.
|
|
3
3
|
*
|
|
4
4
|
* Zero dependencies — validation is manual, no Zod/Valibot/ArkType required.
|
|
5
|
-
* Each
|
|
5
|
+
* Each type implements Standard Schema v1 for interop with any schema library,
|
|
6
6
|
* plus standalone `.parse()` and `.safeParse()` convenience methods.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```ts
|
|
10
|
-
* import {
|
|
10
|
+
* import { Command, port, csv } from "@silvery/commander"
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* new Command("deploy")
|
|
13
13
|
* .option("-p, --port <n>", "Port", port) // number (1-65535)
|
|
14
14
|
* .option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
15
15
|
* .option("--tags <t>", "Tags", csv) // string[]
|
|
16
|
-
* .option("--
|
|
17
|
-
* .option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
|
|
16
|
+
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
18
17
|
*
|
|
19
18
|
* // Standalone usage (outside Commander)
|
|
20
19
|
* port.parse("3000") // 3000
|
|
@@ -22,18 +21,34 @@
|
|
|
22
21
|
* ```
|
|
23
22
|
*/
|
|
24
23
|
|
|
25
|
-
|
|
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
|
+
}
|
|
26
41
|
|
|
27
|
-
/** A Standard Schema v1
|
|
28
|
-
export interface
|
|
42
|
+
/** A Standard Schema v1 CLI type with standalone parse/safeParse methods. */
|
|
43
|
+
export interface CLIType<T> extends StandardSchemaV1<T> {
|
|
29
44
|
/** Parse and validate a value, throwing on failure. */
|
|
30
45
|
parse(value: unknown): T
|
|
31
46
|
/** Parse and validate a value, returning a result object. */
|
|
32
47
|
safeParse(value: unknown): { success: true; value: T } | { success: false; issues: Array<{ message: string }> }
|
|
33
48
|
}
|
|
34
49
|
|
|
35
|
-
function
|
|
36
|
-
const schema:
|
|
50
|
+
function createType<T>(vendor: string, validate: (value: unknown) => T): CLIType<T> {
|
|
51
|
+
const schema: CLIType<T> = {
|
|
37
52
|
"~standard": {
|
|
38
53
|
version: 1,
|
|
39
54
|
vendor,
|
|
@@ -62,7 +77,7 @@ function createPreset<T>(vendor: string, validate: (value: unknown) => T): Prese
|
|
|
62
77
|
const VENDOR = "@silvery/commander"
|
|
63
78
|
|
|
64
79
|
/** Integer (coerced from string). */
|
|
65
|
-
export const int =
|
|
80
|
+
export const int = createType<number>(VENDOR, (v) => {
|
|
66
81
|
const s = String(v).trim()
|
|
67
82
|
if (s === "") throw new Error(`Expected integer, got "${v}"`)
|
|
68
83
|
const n = Number(s)
|
|
@@ -71,7 +86,7 @@ export const int = createPreset<number>(VENDOR, (v) => {
|
|
|
71
86
|
})
|
|
72
87
|
|
|
73
88
|
/** Unsigned integer (>= 0, coerced from string). */
|
|
74
|
-
export const uint =
|
|
89
|
+
export const uint = createType<number>(VENDOR, (v) => {
|
|
75
90
|
const s = String(v).trim()
|
|
76
91
|
if (s === "") throw new Error(`Expected unsigned integer (>= 0), got "${v}"`)
|
|
77
92
|
const n = Number(s)
|
|
@@ -80,7 +95,7 @@ export const uint = createPreset<number>(VENDOR, (v) => {
|
|
|
80
95
|
})
|
|
81
96
|
|
|
82
97
|
/** Float (coerced from string). */
|
|
83
|
-
export const float =
|
|
98
|
+
export const float = createType<number>(VENDOR, (v) => {
|
|
84
99
|
const s = String(v).trim()
|
|
85
100
|
if (s === "" || s === "NaN") throw new Error(`Expected number, got "${v}"`)
|
|
86
101
|
const n = Number(s)
|
|
@@ -89,14 +104,14 @@ export const float = createPreset<number>(VENDOR, (v) => {
|
|
|
89
104
|
})
|
|
90
105
|
|
|
91
106
|
/** Port number (1-65535). */
|
|
92
|
-
export const port =
|
|
107
|
+
export const port = createType<number>(VENDOR, (v) => {
|
|
93
108
|
const n = Number(v)
|
|
94
109
|
if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error(`Expected port (1-65535), got "${v}"`)
|
|
95
110
|
return n
|
|
96
111
|
})
|
|
97
112
|
|
|
98
113
|
/** URL (validated via URL constructor). */
|
|
99
|
-
export const url =
|
|
114
|
+
export const url = createType<string>(VENDOR, (v) => {
|
|
100
115
|
const s = String(v)
|
|
101
116
|
try {
|
|
102
117
|
new URL(s)
|
|
@@ -107,14 +122,14 @@ export const url = createPreset<string>(VENDOR, (v) => {
|
|
|
107
122
|
})
|
|
108
123
|
|
|
109
124
|
/** File path (non-empty string). */
|
|
110
|
-
export const path =
|
|
125
|
+
export const path = createType<string>(VENDOR, (v) => {
|
|
111
126
|
const s = String(v)
|
|
112
127
|
if (!s) throw new Error("Expected non-empty path")
|
|
113
128
|
return s
|
|
114
129
|
})
|
|
115
130
|
|
|
116
131
|
/** Comma-separated values to string[]. */
|
|
117
|
-
export const csv =
|
|
132
|
+
export const csv = createType<string[]>(VENDOR, (v) => {
|
|
118
133
|
return String(v)
|
|
119
134
|
.split(",")
|
|
120
135
|
.map((s) => s.trim())
|
|
@@ -122,7 +137,7 @@ export const csv = createPreset<string[]>(VENDOR, (v) => {
|
|
|
122
137
|
})
|
|
123
138
|
|
|
124
139
|
/** JSON string to parsed value. */
|
|
125
|
-
export const json =
|
|
140
|
+
export const json = createType<unknown>(VENDOR, (v) => {
|
|
126
141
|
try {
|
|
127
142
|
return JSON.parse(String(v))
|
|
128
143
|
} catch {
|
|
@@ -131,7 +146,7 @@ export const json = createPreset<unknown>(VENDOR, (v) => {
|
|
|
131
146
|
})
|
|
132
147
|
|
|
133
148
|
/** Boolean string ("true"/"false"/"1"/"0"/"yes"/"no"). */
|
|
134
|
-
export const bool =
|
|
149
|
+
export const bool = createType<boolean>(VENDOR, (v) => {
|
|
135
150
|
const s = String(v).toLowerCase()
|
|
136
151
|
if (["true", "1", "yes", "y"].includes(s)) return true
|
|
137
152
|
if (["false", "0", "no", "n"].includes(s)) return false
|
|
@@ -139,21 +154,21 @@ export const bool = createPreset<boolean>(VENDOR, (v) => {
|
|
|
139
154
|
})
|
|
140
155
|
|
|
141
156
|
/** Date string to Date object. */
|
|
142
|
-
export const date =
|
|
157
|
+
export const date = createType<Date>(VENDOR, (v) => {
|
|
143
158
|
const d = new Date(String(v))
|
|
144
159
|
if (isNaN(d.getTime())) throw new Error(`Expected valid date, got "${v}"`)
|
|
145
160
|
return d
|
|
146
161
|
})
|
|
147
162
|
|
|
148
163
|
/** Email address (basic validation). */
|
|
149
|
-
export const email =
|
|
164
|
+
export const email = createType<string>(VENDOR, (v) => {
|
|
150
165
|
const s = String(v)
|
|
151
166
|
if (!s.includes("@") || !s.includes(".")) throw new Error(`Expected email address, got "${v}"`)
|
|
152
167
|
return s
|
|
153
168
|
})
|
|
154
169
|
|
|
155
170
|
/** Regex pattern string to RegExp. */
|
|
156
|
-
export const regex =
|
|
171
|
+
export const regex = createType<RegExp>(VENDOR, (v) => {
|
|
157
172
|
try {
|
|
158
173
|
return new RegExp(String(v))
|
|
159
174
|
} catch {
|
|
@@ -162,20 +177,10 @@ export const regex = createPreset<RegExp>(VENDOR, (v) => {
|
|
|
162
177
|
})
|
|
163
178
|
|
|
164
179
|
/** Integer with min/max bounds (factory). */
|
|
165
|
-
export function intRange(min: number, max: number):
|
|
166
|
-
return
|
|
180
|
+
export function intRange(min: number, max: number): CLIType<number> {
|
|
181
|
+
return createType<number>(VENDOR, (v) => {
|
|
167
182
|
const n = Number(v)
|
|
168
|
-
if (!Number.isInteger(n) || n < min || n > max)
|
|
169
|
-
throw new Error(`Expected integer ${min}-${max}, got "${v}"`)
|
|
183
|
+
if (!Number.isInteger(n) || n < min || n > max) throw new Error(`Expected integer ${min}-${max}, got "${v}"`)
|
|
170
184
|
return n
|
|
171
185
|
})
|
|
172
186
|
}
|
|
173
|
-
|
|
174
|
-
/** Enum from a fixed set of string values (factory). */
|
|
175
|
-
export function oneOf<const T extends readonly string[]>(values: T): Preset<T[number]> {
|
|
176
|
-
return createPreset<T[number]>(VENDOR, (v) => {
|
|
177
|
-
const s = String(v)
|
|
178
|
-
if (!values.includes(s as any)) throw new Error(`Expected one of [${values.join(", ")}], got "${v}"`)
|
|
179
|
-
return s as T[number]
|
|
180
|
-
})
|
|
181
|
-
}
|
package/src/z.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|