@silvery/commander 0.2.0 → 0.4.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 +105 -72
- package/package.json +7 -10
- package/src/colorize.ts +164 -0
- package/src/command.ts +117 -0
- package/src/index.ts +11 -168
- package/src/presets.ts +196 -0
- package/src/z.ts +45 -0
- package/src/typed.ts +0 -416
package/README.md
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
# @silvery/commander
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Enhanced [Commander.js](https://github.com/tj/commander.js) with auto-colorized help, Standard Schema validation, and CLI presets. Drop-in replacement -- `Command` is a subclass of Commander's `Command`.
|
|
4
|
+
|
|
5
|
+
## Three layers
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Layer 1: Enhanced Commander (auto-colorized help, Standard Schema support)
|
|
9
|
+
import { Command, port, csv } from "@silvery/commander"
|
|
10
|
+
|
|
11
|
+
// Layer 2: Zero-dep presets (Standard Schema, standalone use)
|
|
12
|
+
import { port, csv, int } from "@silvery/commander/parse"
|
|
13
|
+
|
|
14
|
+
// Layer 3: Zod + CLI presets (batteries included)
|
|
15
|
+
import { Command, z } from "@silvery/commander"
|
|
16
|
+
```
|
|
4
17
|
|
|
5
18
|
## Usage
|
|
6
19
|
|
|
7
20
|
```typescript
|
|
8
|
-
import {
|
|
21
|
+
import { Command, port, csv, oneOf } from "@silvery/commander"
|
|
9
22
|
|
|
10
|
-
const
|
|
11
|
-
.description("
|
|
23
|
+
const program = new Command("deploy")
|
|
24
|
+
.description("Deploy the application")
|
|
12
25
|
.version("1.0.0")
|
|
13
|
-
.option("-
|
|
14
|
-
.option("
|
|
15
|
-
.option("-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const { verbose, port, output, color } = cli.opts()
|
|
20
|
-
// ^boolean ^number ^string|true ^boolean
|
|
26
|
+
.option("-p, --port <n>", "Port", port) // number (1-65535)
|
|
27
|
+
.option("--tags <t>", "Tags", csv) // string[]
|
|
28
|
+
.option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
|
|
29
|
+
|
|
30
|
+
program.parse()
|
|
31
|
+
const opts = program.opts()
|
|
21
32
|
```
|
|
22
33
|
|
|
23
34
|
Help output is automatically colorized using Commander's built-in `configureHelp()` style hooks (headings bold, flags green, commands cyan, descriptions dim, arguments yellow).
|
|
@@ -32,103 +43,125 @@ const program = new Command("myapp").description("My CLI tool")
|
|
|
32
43
|
colorizeHelp(program) // applies recursively to all subcommands
|
|
33
44
|
```
|
|
34
45
|
|
|
35
|
-
##
|
|
46
|
+
## Standard Schema validation
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
const cli = createCLI("deploy")
|
|
41
|
-
.option("-p, --port <n>", "Port", parseInt) // → port: number
|
|
42
|
-
.option("-t, --timeout <ms>", "Timeout", Number) // → timeout: number
|
|
43
|
-
.option("--tags <items>", "Tags", (v) => v.split(",")) // → tags: string[]
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Default values can be passed as the fourth argument:
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
.option("-p, --port <n>", "Port", parseInt, 8080) // → port: number (defaults to 8080)
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Zod schema validation
|
|
53
|
-
|
|
54
|
-
Pass a [Zod](https://zod.dev) schema as the third argument for combined parsing, validation, and type inference:
|
|
48
|
+
Pass any [Standard Schema v1](https://github.com/standard-schema/standard-schema) compatible schema as the third argument to `.option()` for combined parsing, validation, and type inference. This works with the built-in presets, Zod (>=3.24), Valibot (>=1.0), ArkType (>=2.0), and any other library implementing the standard:
|
|
55
49
|
|
|
56
50
|
```typescript
|
|
51
|
+
import { Command } from "@silvery/commander"
|
|
57
52
|
import { z } from "zod"
|
|
58
53
|
|
|
59
|
-
const
|
|
54
|
+
const program = new Command("deploy")
|
|
60
55
|
.option("-p, --port <n>", "Port", z.coerce.number().min(1).max(65535))
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.option("-e, --env <env>", "Environment", z.enum(["dev", "staging", "prod"]))
|
|
64
|
-
// → env: "dev" | "staging" | "prod" (union type)
|
|
65
|
-
|
|
56
|
+
.option("-e, --env <env>", "Env", z.enum(["dev", "staging", "prod"]))
|
|
66
57
|
.option(
|
|
67
58
|
"--tags <t>",
|
|
68
59
|
"Tags",
|
|
69
60
|
z.string().transform((v) => v.split(",")),
|
|
70
61
|
)
|
|
71
|
-
// → tags: string[] (transformed)
|
|
72
62
|
```
|
|
73
63
|
|
|
74
|
-
|
|
64
|
+
Schema libraries are optional peer dependencies -- detected at runtime via the Standard Schema `~standard` interface, never imported at the top level. A legacy fallback supports older Zod versions (pre-3.24) that don't implement Standard Schema yet.
|
|
75
65
|
|
|
76
|
-
##
|
|
66
|
+
## Zod CLI presets
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
Import `z` from `@silvery/commander` for an extended Zod object with CLI-specific schemas:
|
|
79
69
|
|
|
80
70
|
```typescript
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.option("-
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
import { Command, z } from "@silvery/commander"
|
|
72
|
+
|
|
73
|
+
const program = new Command("deploy")
|
|
74
|
+
.option("-p, --port <n>", "Port", z.port) // z.coerce.number().int().min(1).max(65535)
|
|
75
|
+
.option("--tags <t>", "Tags", z.csv) // z.string().transform(...)
|
|
76
|
+
.option("-e, --env <e>", "Env", z.oneOf(["dev", "staging", "prod"]))
|
|
77
|
+
.option("-r, --retries <n>", "Retries", z.int) // z.coerce.number().int()
|
|
88
78
|
```
|
|
89
79
|
|
|
90
|
-
|
|
80
|
+
The `z` export is tree-shakeable -- if you don't import it, Zod won't be in your bundle.
|
|
81
|
+
|
|
82
|
+
Available `z` CLI presets: `z.port`, `z.int`, `z.uint`, `z.float`, `z.csv`, `z.url`, `z.path`, `z.email`, `z.date`, `z.json`, `z.bool`, `z.intRange(min, max)`, `z.oneOf(values)`.
|
|
91
83
|
|
|
92
|
-
##
|
|
84
|
+
## Presets
|
|
93
85
|
|
|
94
|
-
|
|
86
|
+
Pre-built validators for common CLI argument patterns. Each preset implements [Standard Schema v1](https://github.com/standard-schema/standard-schema) and works with Commander's `.option()` or standalone.
|
|
95
87
|
|
|
96
88
|
```typescript
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
import { Command, port, csv, int, url, oneOf } from "@silvery/commander"
|
|
90
|
+
|
|
91
|
+
const program = new Command("deploy")
|
|
92
|
+
.option("-p, --port <n>", "Port", port) // number (1-65535, validated)
|
|
93
|
+
.option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
94
|
+
.option("--tags <t>", "Tags", csv) // string[]
|
|
95
|
+
.option("--callback <url>", "Callback", url) // string (validated URL)
|
|
96
|
+
.option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Standalone usage
|
|
100
|
+
|
|
101
|
+
Presets also work outside Commander for validating env vars, config files, etc. Import from the `@silvery/commander/parse` subpath for tree-shaking:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { port, csv, oneOf } from "@silvery/commander/parse"
|
|
105
|
+
|
|
106
|
+
// .parse() — returns value or throws
|
|
107
|
+
const dbPort = port.parse(process.env.DB_PORT ?? "5432") // 3000
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
const
|
|
109
|
+
// .safeParse() — returns result object, never throws
|
|
110
|
+
const result = port.safeParse("abc")
|
|
111
|
+
// { success: false, issues: [{ message: 'Expected port (1-65535), got "abc"' }] }
|
|
112
|
+
|
|
113
|
+
// Standard Schema ~standard.validate() also available
|
|
114
|
+
const validated = port["~standard"].validate("8080")
|
|
115
|
+
// { value: 8080 }
|
|
102
116
|
```
|
|
103
117
|
|
|
104
|
-
|
|
118
|
+
### Available presets
|
|
119
|
+
|
|
120
|
+
| Preset | Type | Validation |
|
|
121
|
+
| ------- | ---------- | ---------------------------------------- |
|
|
122
|
+
| `int` | `number` | Integer (coerced from string) |
|
|
123
|
+
| `uint` | `number` | Unsigned integer (>= 0) |
|
|
124
|
+
| `float` | `number` | Any finite number (rejects NaN) |
|
|
125
|
+
| `port` | `number` | Integer 1-65535 |
|
|
126
|
+
| `url` | `string` | Valid URL (via `URL` constructor) |
|
|
127
|
+
| `path` | `string` | Non-empty string |
|
|
128
|
+
| `csv` | `string[]` | Comma-separated, trimmed, empty filtered |
|
|
129
|
+
| `json` | `unknown` | Parsed JSON |
|
|
130
|
+
| `bool` | `boolean` | true/false/yes/no/1/0 (case-insensitive) |
|
|
131
|
+
| `date` | `Date` | Valid date string |
|
|
132
|
+
| `email` | `string` | Basic email validation (has @ and .) |
|
|
133
|
+
| `regex` | `RegExp` | Valid regex pattern |
|
|
134
|
+
|
|
135
|
+
### Factory presets
|
|
105
136
|
|
|
106
|
-
|
|
137
|
+
```typescript
|
|
138
|
+
import { intRange, oneOf } from "@silvery/commander"
|
|
107
139
|
|
|
108
|
-
|
|
140
|
+
intRange(1, 100) // Preset<number> — integer within bounds
|
|
141
|
+
oneOf(["a", "b", "c"]) // Preset<"a" | "b" | "c"> — enum from values
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Custom parser type inference
|
|
145
|
+
|
|
146
|
+
When `.option()` is called with a parser function as the third argument, Commander infers the return type:
|
|
109
147
|
|
|
110
148
|
```typescript
|
|
111
|
-
|
|
149
|
+
const program = new Command("deploy")
|
|
150
|
+
.option("-p, --port <n>", "Port", parseInt) // port: number
|
|
151
|
+
.option("-t, --timeout <ms>", "Timeout", Number) // timeout: number
|
|
152
|
+
.option("--tags <items>", "Tags", (v) => v.split(",")) // tags: string[]
|
|
112
153
|
```
|
|
113
154
|
|
|
114
|
-
|
|
155
|
+
Default values can be passed as the fourth argument:
|
|
115
156
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
| Custom parser types | Yes (.option with parseFloat -> number) | Yes (parser return type inference) |
|
|
120
|
-
| Zod schema support | No | Yes (parse + validate + infer from Zod schemas) |
|
|
121
|
-
| Typed action handlers | Yes (full signature inference) | Yes (arguments + options) |
|
|
122
|
-
| Choices narrowing | Via .addOption() | Via .optionWithChoices() |
|
|
123
|
-
| Colorized help | Not included | Built-in via Commander's native style hooks |
|
|
124
|
-
| Package size | Types only (25 lines runtime) | Types + colorizer + Zod bridge (~300 lines, zero required deps) |
|
|
125
|
-
| Installation | Separate package alongside commander | Single package, re-exports Commander |
|
|
126
|
-
| Negated flags (--no-X) | Partial | Key extraction + boolean type inference |
|
|
157
|
+
```typescript
|
|
158
|
+
.option("-p, --port <n>", "Port", parseInt, 8080) // port: number (defaults to 8080)
|
|
159
|
+
```
|
|
127
160
|
|
|
128
161
|
## Credits
|
|
129
162
|
|
|
130
163
|
- [Commander.js](https://github.com/tj/commander.js) by TJ Holowaychuk and contributors -- the underlying CLI framework
|
|
131
|
-
- [
|
|
164
|
+
- [Standard Schema](https://github.com/standard-schema/standard-schema) -- universal schema interop protocol for type-safe validation
|
|
132
165
|
- [@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi) -- optional ANSI color detection for respecting NO_COLOR/FORCE_COLOR/terminal capabilities
|
|
133
166
|
- Uses Commander's built-in `configureHelp()` style hooks (added in Commander 12) for colorization
|
|
134
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvery/commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Colorized Commander.js help output using ANSI escape codes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -20,25 +20,22 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
23
24
|
"exports": {
|
|
24
|
-
".": "./src/index.ts"
|
|
25
|
+
".": "./src/index.ts",
|
|
26
|
+
"./parse": "./src/presets.ts"
|
|
25
27
|
},
|
|
26
28
|
"publishConfig": {
|
|
27
29
|
"access": "public"
|
|
28
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": ">=12.0.0"
|
|
33
|
+
},
|
|
29
34
|
"peerDependencies": {
|
|
30
|
-
"@commander-js/extra-typings": ">=12.0.0",
|
|
31
35
|
"@silvery/ansi": ">=0.1.0",
|
|
32
|
-
"commander": ">=12.0.0",
|
|
33
36
|
"zod": ">=3.0.0"
|
|
34
37
|
},
|
|
35
38
|
"peerDependenciesMeta": {
|
|
36
|
-
"commander": {
|
|
37
|
-
"optional": true
|
|
38
|
-
},
|
|
39
|
-
"@commander-js/extra-typings": {
|
|
40
|
-
"optional": true
|
|
41
|
-
},
|
|
42
39
|
"@silvery/ansi": {
|
|
43
40
|
"optional": true
|
|
44
41
|
},
|
package/src/colorize.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander.js help colorization using ANSI escape codes.
|
|
3
|
+
*
|
|
4
|
+
* Uses Commander's built-in style hooks (styleTitle, styleOptionText, etc.)
|
|
5
|
+
* rather than regex post-processing. Works with @silvery/commander
|
|
6
|
+
* or plain commander — accepts a minimal CommandLike interface so Commander
|
|
7
|
+
* is a peer dependency, not a hard one.
|
|
8
|
+
*
|
|
9
|
+
* Zero dependencies — only raw ANSI escape codes.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { Command } from "@silvery/commander"
|
|
14
|
+
* import { colorizeHelp } from "@silvery/commander"
|
|
15
|
+
*
|
|
16
|
+
* const program = new Command("myapp").description("My CLI tool")
|
|
17
|
+
* colorizeHelp(program)
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Raw ANSI escape codes — no framework dependencies.
|
|
22
|
+
const RESET = "\x1b[0m"
|
|
23
|
+
const BOLD = "\x1b[1m"
|
|
24
|
+
const DIM = "\x1b[2m"
|
|
25
|
+
const CYAN = "\x1b[36m"
|
|
26
|
+
const GREEN = "\x1b[32m"
|
|
27
|
+
const YELLOW = "\x1b[33m"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if color output should be enabled.
|
|
31
|
+
* Uses @silvery/ansi detectColor() if available, falls back to basic
|
|
32
|
+
* NO_COLOR/FORCE_COLOR/isTTY checks.
|
|
33
|
+
*/
|
|
34
|
+
let _shouldColorize: boolean | undefined
|
|
35
|
+
|
|
36
|
+
export function shouldColorize(): boolean {
|
|
37
|
+
if (_shouldColorize !== undefined) return _shouldColorize
|
|
38
|
+
|
|
39
|
+
// Try @silvery/ansi for full detection (respects NO_COLOR, FORCE_COLOR, TERM, etc.)
|
|
40
|
+
try {
|
|
41
|
+
const { detectColor } = require("@silvery/ansi") as { detectColor: (stdout: NodeJS.WriteStream) => string | null }
|
|
42
|
+
_shouldColorize = detectColor(process.stdout) !== null
|
|
43
|
+
} catch {
|
|
44
|
+
// Fallback: basic NO_COLOR / FORCE_COLOR / isTTY checks
|
|
45
|
+
if (process.env.NO_COLOR !== undefined) {
|
|
46
|
+
_shouldColorize = false
|
|
47
|
+
} else if (process.env.FORCE_COLOR !== undefined) {
|
|
48
|
+
_shouldColorize = true
|
|
49
|
+
} else {
|
|
50
|
+
_shouldColorize = process.stdout?.isTTY ?? true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return _shouldColorize
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Wrap a string with ANSI codes, handling nested resets. */
|
|
58
|
+
function ansi(text: string, code: string): string {
|
|
59
|
+
return `${code}${text}${RESET}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Minimal interface for Commander's Command — avoids requiring Commander
|
|
64
|
+
* as a direct dependency. Works with both `commander` and
|
|
65
|
+
* `@silvery/commander`.
|
|
66
|
+
*
|
|
67
|
+
* Uses permissive types to ensure structural compatibility with all
|
|
68
|
+
* Commander versions, overloads, and generic instantiations.
|
|
69
|
+
*/
|
|
70
|
+
export interface CommandLike {
|
|
71
|
+
// biome-ignore lint: permissive to match Commander's overloaded signatures
|
|
72
|
+
configureHelp(...args: any[]): any
|
|
73
|
+
// biome-ignore lint: permissive to match Commander's overloaded signatures
|
|
74
|
+
configureOutput(...args: any[]): any
|
|
75
|
+
// biome-ignore lint: permissive to match Commander's Command[] structurally
|
|
76
|
+
readonly commands: readonly any[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Color scheme for help output. Values are raw ANSI escape sequences. */
|
|
80
|
+
export interface ColorizeHelpOptions {
|
|
81
|
+
/** ANSI code for command/subcommand names. Default: cyan */
|
|
82
|
+
commands?: string
|
|
83
|
+
/** ANSI code for --flags and -short options. Default: green */
|
|
84
|
+
flags?: string
|
|
85
|
+
/** ANSI code for description text. Default: dim */
|
|
86
|
+
description?: string
|
|
87
|
+
/** ANSI code for section headings (Usage:, Options:, etc.). Default: bold */
|
|
88
|
+
heading?: string
|
|
89
|
+
/** ANSI code for <required> and [optional] argument brackets. Default: yellow */
|
|
90
|
+
brackets?: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Apply colorized help output to a Commander.js program and all its subcommands.
|
|
95
|
+
*
|
|
96
|
+
* Uses Commander's built-in `configureHelp()` style hooks rather than
|
|
97
|
+
* post-processing the formatted string. This approach is robust against
|
|
98
|
+
* formatting changes in Commander and handles wrapping correctly.
|
|
99
|
+
*
|
|
100
|
+
* @param program - A Commander Command instance (or compatible object)
|
|
101
|
+
* @param options - Override default ANSI color codes for each element
|
|
102
|
+
*/
|
|
103
|
+
export function colorizeHelp(program: CommandLike, options?: ColorizeHelpOptions): void {
|
|
104
|
+
const cmds = options?.commands ?? CYAN
|
|
105
|
+
const flags = options?.flags ?? GREEN
|
|
106
|
+
const desc = options?.description ?? DIM
|
|
107
|
+
const heading = options?.heading ?? BOLD
|
|
108
|
+
const brackets = options?.brackets ?? YELLOW
|
|
109
|
+
|
|
110
|
+
const helpConfig: Record<string, unknown> = {
|
|
111
|
+
// Section headings: "Usage:", "Options:", "Commands:", "Arguments:"
|
|
112
|
+
styleTitle(str: string): string {
|
|
113
|
+
return ansi(str, heading)
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Command name in usage line and subcommand terms
|
|
117
|
+
styleCommandText(str: string): string {
|
|
118
|
+
return ansi(str, cmds)
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Option terms: "-v, --verbose", "--repo <path>", "[options]"
|
|
122
|
+
styleOptionText(str: string): string {
|
|
123
|
+
return ansi(str, flags)
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Subcommand names in the commands list
|
|
127
|
+
styleSubcommandText(str: string): string {
|
|
128
|
+
return ansi(str, cmds)
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// Argument terms: "<file>", "[dir]"
|
|
132
|
+
styleArgumentText(str: string): string {
|
|
133
|
+
return ansi(str, brackets)
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Description text for options, subcommands, arguments
|
|
137
|
+
styleDescriptionText(str: string): string {
|
|
138
|
+
return ansi(str, desc)
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Command description (the main program description line) — keep normal
|
|
142
|
+
styleCommandDescription(str: string): string {
|
|
143
|
+
return str
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
program.configureHelp(helpConfig)
|
|
148
|
+
|
|
149
|
+
// Tell Commander that color output is supported, even when stdout is not
|
|
150
|
+
// a TTY (e.g., piped output, CI, tests). Without this, Commander strips
|
|
151
|
+
// all ANSI codes from helpInformation() output.
|
|
152
|
+
//
|
|
153
|
+
// Callers who want to respect NO_COLOR/FORCE_COLOR should check
|
|
154
|
+
// shouldColorize() before calling colorizeHelp().
|
|
155
|
+
program.configureOutput({
|
|
156
|
+
getOutHasColors: () => true,
|
|
157
|
+
getErrHasColors: () => true,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Apply recursively to all existing subcommands
|
|
161
|
+
for (const sub of program.commands) {
|
|
162
|
+
colorizeHelp(sub, options)
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Commander Command with auto-colorized help and Standard Schema support.
|
|
3
|
+
*
|
|
4
|
+
* Subclasses Commander's Command so `new Command("app")` just works —
|
|
5
|
+
* it's Commander with auto-colorized help and automatic Standard Schema /
|
|
6
|
+
* legacy Zod detection in `.option()`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Command } from "@silvery/commander"
|
|
11
|
+
* import { port, csv } from "@silvery/commander/parse"
|
|
12
|
+
*
|
|
13
|
+
* const program = new Command("myapp")
|
|
14
|
+
* .description("My CLI tool")
|
|
15
|
+
* .version("1.0.0")
|
|
16
|
+
* .option("-p, --port <n>", "Port", port)
|
|
17
|
+
* .option("--tags <t>", "Tags", csv)
|
|
18
|
+
*
|
|
19
|
+
* program.parse()
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Command as BaseCommand } from "commander"
|
|
24
|
+
import { colorizeHelp } from "./colorize.ts"
|
|
25
|
+
import type { StandardSchemaV1 } from "./presets.ts"
|
|
26
|
+
|
|
27
|
+
// --- Standard Schema support ---
|
|
28
|
+
|
|
29
|
+
/** Runtime check: is this value a Standard Schema v1 object? */
|
|
30
|
+
function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
31
|
+
return typeof value === "object" && value !== null && "~standard" in (value as any)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Wrap a Standard Schema as a Commander parser function */
|
|
35
|
+
function standardSchemaParser<T>(schema: StandardSchemaV1<T>): (value: string) => T {
|
|
36
|
+
return (value: string) => {
|
|
37
|
+
const result = schema["~standard"].validate(value)
|
|
38
|
+
if ("issues" in result) {
|
|
39
|
+
const msg = result.issues.map((i) => i.message).join(", ")
|
|
40
|
+
throw new Error(msg)
|
|
41
|
+
}
|
|
42
|
+
return result.value
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Legacy Zod support (pre-3.24, no ~standard) ---
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Duck-type interface for older Zod schemas that don't implement Standard Schema.
|
|
50
|
+
* Any object with `parse(value: string) => T` and `_def` qualifies.
|
|
51
|
+
*/
|
|
52
|
+
interface ZodLike<T = any> {
|
|
53
|
+
parse(value: unknown): T
|
|
54
|
+
_def: unknown
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Runtime check: is this value a legacy Zod-like schema (without Standard Schema)? */
|
|
58
|
+
function isLegacyZodSchema(value: unknown): value is ZodLike {
|
|
59
|
+
return (
|
|
60
|
+
typeof value === "object" &&
|
|
61
|
+
value !== null &&
|
|
62
|
+
typeof (value as any).parse === "function" &&
|
|
63
|
+
"_def" in (value as any) &&
|
|
64
|
+
!("~standard" in (value as any))
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Wrap a legacy Zod schema as a Commander parser function */
|
|
69
|
+
function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
70
|
+
return (value: string) => {
|
|
71
|
+
try {
|
|
72
|
+
return schema.parse(value)
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
// Format Zod errors as Commander-style messages
|
|
75
|
+
if (err?.issues) {
|
|
76
|
+
const messages = err.issues.map((i: any) => i.message).join(", ")
|
|
77
|
+
throw new Error(messages)
|
|
78
|
+
}
|
|
79
|
+
throw err
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class Command extends BaseCommand {
|
|
85
|
+
constructor(name?: string) {
|
|
86
|
+
super(name)
|
|
87
|
+
colorizeHelp(this as any)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add an option with automatic Standard Schema / legacy Zod detection.
|
|
92
|
+
*
|
|
93
|
+
* When the third argument is a Standard Schema v1 object (Zod >=3.24,
|
|
94
|
+
* Valibot >=1.0, ArkType >=2.0, or @silvery/commander presets), it's
|
|
95
|
+
* automatically wrapped as a Commander parser function.
|
|
96
|
+
*
|
|
97
|
+
* When the third argument is a legacy Zod schema (pre-3.24, has `_def`
|
|
98
|
+
* and `parse` but no `~standard`), it's also wrapped automatically.
|
|
99
|
+
*/
|
|
100
|
+
option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
|
|
101
|
+
if (isStandardSchema(parseArgOrDefault)) {
|
|
102
|
+
return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault))
|
|
103
|
+
}
|
|
104
|
+
if (isLegacyZodSchema(parseArgOrDefault)) {
|
|
105
|
+
return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault))
|
|
106
|
+
}
|
|
107
|
+
if (typeof parseArgOrDefault === "function") {
|
|
108
|
+
return super.option(flags, description ?? "", parseArgOrDefault, defaultValue)
|
|
109
|
+
}
|
|
110
|
+
return super.option(flags, description ?? "", parseArgOrDefault)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Subcommands also get colorized help and Standard Schema support
|
|
114
|
+
createCommand(name?: string): Command {
|
|
115
|
+
return new Command(name)
|
|
116
|
+
}
|
|
117
|
+
}
|