@silvery/commander 0.1.0 → 0.2.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 +95 -20
- package/package.json +10 -2
- package/src/index.ts +31 -0
- package/src/typed.ts +153 -25
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @silvery/commander
|
|
2
2
|
|
|
3
|
-
Type-safe, colorized [Commander.js](https://github.com/tj/commander.js) wrapper. Infers option types from `.option()` calls using TypeScript 5.4+ const type parameters and template literal types
|
|
3
|
+
Type-safe, colorized [Commander.js](https://github.com/tj/commander.js) wrapper. Infers option types from `.option()` calls using TypeScript 5.4+ const type parameters and template literal types -- no codegen, no separate type package.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -11,13 +11,13 @@ const cli = createCLI("myapp")
|
|
|
11
11
|
.description("My CLI tool")
|
|
12
12
|
.version("1.0.0")
|
|
13
13
|
.option("-v, --verbose", "Verbose output")
|
|
14
|
-
.option("-p, --port <number>", "Port to listen on")
|
|
14
|
+
.option("-p, --port <number>", "Port to listen on", parseInt)
|
|
15
15
|
.option("-o, --output [path]", "Output path")
|
|
16
16
|
.option("--no-color", "Disable color output")
|
|
17
17
|
|
|
18
18
|
cli.parse()
|
|
19
19
|
const { verbose, port, output, color } = cli.opts()
|
|
20
|
-
// ^boolean ^
|
|
20
|
+
// ^boolean ^number ^string|true ^boolean
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Help output is automatically colorized using Commander's built-in `configureHelp()` style hooks (headings bold, flags green, commands cyan, descriptions dim, arguments yellow).
|
|
@@ -32,29 +32,104 @@ const program = new Command("myapp").description("My CLI tool")
|
|
|
32
32
|
colorizeHelp(program) // applies recursively to all subcommands
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## Custom parser type inference
|
|
36
|
+
|
|
37
|
+
When `.option()` is called with a parser function as the third argument, the return type is inferred:
|
|
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:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { z } from "zod"
|
|
58
|
+
|
|
59
|
+
const cli = createCLI("deploy")
|
|
60
|
+
.option("-p, --port <n>", "Port", z.coerce.number().min(1).max(65535))
|
|
61
|
+
// → port: number (validated at parse time)
|
|
62
|
+
|
|
63
|
+
.option("-e, --env <env>", "Environment", z.enum(["dev", "staging", "prod"]))
|
|
64
|
+
// → env: "dev" | "staging" | "prod" (union type)
|
|
65
|
+
|
|
66
|
+
.option(
|
|
67
|
+
"--tags <t>",
|
|
68
|
+
"Tags",
|
|
69
|
+
z.string().transform((v) => v.split(",")),
|
|
70
|
+
)
|
|
71
|
+
// → tags: string[] (transformed)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Zod is an optional peer dependency -- duck-typed at runtime, never imported at the top level. If Zod validation fails, the error is formatted as a Commander-style error message.
|
|
75
|
+
|
|
76
|
+
## Typed action handlers
|
|
36
77
|
|
|
37
|
-
|
|
38
|
-
|---|---|---|
|
|
39
|
-
| Type inference | 1536-line .d.ts with recursive generic accumulation | ~60 lines using TS 5.4+ const type params + template literals |
|
|
40
|
-
| Colorized help | Not included | Built-in via Commander's native style hooks |
|
|
41
|
-
| Package size | Types only (25 lines runtime) | Types + colorizer (~200 lines, zero deps) |
|
|
42
|
-
| Installation | Separate package alongside commander | Single package, re-exports Commander |
|
|
43
|
-
| React dependency | None | None |
|
|
44
|
-
| Negated flags (--no-X) | Partial | Key extraction + boolean type inference |
|
|
45
|
-
| Typed action handlers | Yes (full signature inference) | Not yet (planned) |
|
|
46
|
-
| Custom parser types | Yes (.option with parseFloat -> number) | Not yet (planned) |
|
|
78
|
+
Action callbacks receive typed arguments and options:
|
|
47
79
|
|
|
48
|
-
|
|
80
|
+
```typescript
|
|
81
|
+
const cli = createCLI("deploy")
|
|
82
|
+
.argument("<env>", "Target environment")
|
|
83
|
+
.argument("[tag]", "Optional deploy tag")
|
|
84
|
+
.option("-f, --force", "Force deploy")
|
|
85
|
+
.action((env, tag, opts) => {
|
|
86
|
+
// env: string, tag: string | undefined, opts: { force: boolean | undefined }
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Required arguments (`<name>`) are `string`, optional arguments (`[name]`) are `string | undefined`.
|
|
91
|
+
|
|
92
|
+
## Choices narrowing
|
|
93
|
+
|
|
94
|
+
Use `.optionWithChoices()` to restrict an option to a fixed set of values with union type inference:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const cli = createCLI("deploy").optionWithChoices("-e, --env <env>", "Environment", ["dev", "staging", "prod"] as const)
|
|
98
|
+
// → env: "dev" | "staging" | "prod" | undefined
|
|
99
|
+
|
|
100
|
+
cli.parse()
|
|
101
|
+
const { env } = cli.opts() // env: "dev" | "staging" | "prod" | undefined
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Commander validates the choice at parse time and rejects invalid values.
|
|
105
|
+
|
|
106
|
+
## Environment variable support
|
|
107
|
+
|
|
108
|
+
Chain `.env()` to set an environment variable fallback for the last-added option:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
.option("-p, --port <n>", "Port").env("PORT")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Improvements over @commander-js/extra-typings
|
|
49
115
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
116
|
+
| Feature | extra-typings | @silvery/commander |
|
|
117
|
+
| ---------------------- | --------------------------------------------------- | --------------------------------------------------------------- |
|
|
118
|
+
| Type inference | 1536-line .d.ts with recursive generic accumulation | ~120 lines using TS 5.4+ const type params + template literals |
|
|
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 |
|
|
53
127
|
|
|
54
128
|
## Credits
|
|
55
129
|
|
|
56
|
-
-
|
|
57
|
-
-
|
|
130
|
+
- [Commander.js](https://github.com/tj/commander.js) by TJ Holowaychuk and contributors -- the underlying CLI framework
|
|
131
|
+
- [@commander-js/extra-typings](https://github.com/commander-js/extra-typings) -- inspired the type inference approach; our implementation uses modern TypeScript features (const type parameters, template literal types) to achieve similar results in fewer lines
|
|
132
|
+
- [@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi) -- optional ANSI color detection for respecting NO_COLOR/FORCE_COLOR/terminal capabilities
|
|
58
133
|
- Uses Commander's built-in `configureHelp()` style hooks (added in Commander 12) for colorization
|
|
59
134
|
|
|
60
135
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvery/commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Colorized Commander.js help output using ANSI escape codes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@commander-js/extra-typings": ">=12.0.0",
|
|
31
|
-
"
|
|
31
|
+
"@silvery/ansi": ">=0.1.0",
|
|
32
|
+
"commander": ">=12.0.0",
|
|
33
|
+
"zod": ">=3.0.0"
|
|
32
34
|
},
|
|
33
35
|
"peerDependenciesMeta": {
|
|
34
36
|
"commander": {
|
|
@@ -36,6 +38,12 @@
|
|
|
36
38
|
},
|
|
37
39
|
"@commander-js/extra-typings": {
|
|
38
40
|
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"@silvery/ansi": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"zod": {
|
|
46
|
+
"optional": true
|
|
39
47
|
}
|
|
40
48
|
},
|
|
41
49
|
"engines": {
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,34 @@ const CYAN = "\x1b[36m"
|
|
|
33
33
|
const GREEN = "\x1b[32m"
|
|
34
34
|
const YELLOW = "\x1b[33m"
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Check if color output should be enabled.
|
|
38
|
+
* Uses @silvery/ansi detectColor() if available, falls back to basic
|
|
39
|
+
* NO_COLOR/FORCE_COLOR/isTTY checks.
|
|
40
|
+
*/
|
|
41
|
+
let _shouldColorize: boolean | undefined
|
|
42
|
+
|
|
43
|
+
export function shouldColorize(): boolean {
|
|
44
|
+
if (_shouldColorize !== undefined) return _shouldColorize
|
|
45
|
+
|
|
46
|
+
// Try @silvery/ansi for full detection (respects NO_COLOR, FORCE_COLOR, TERM, etc.)
|
|
47
|
+
try {
|
|
48
|
+
const { detectColor } = require("@silvery/ansi") as { detectColor: (stdout: NodeJS.WriteStream) => string | null }
|
|
49
|
+
_shouldColorize = detectColor(process.stdout) !== null
|
|
50
|
+
} catch {
|
|
51
|
+
// Fallback: basic NO_COLOR / FORCE_COLOR / isTTY checks
|
|
52
|
+
if (process.env.NO_COLOR !== undefined) {
|
|
53
|
+
_shouldColorize = false
|
|
54
|
+
} else if (process.env.FORCE_COLOR !== undefined) {
|
|
55
|
+
_shouldColorize = true
|
|
56
|
+
} else {
|
|
57
|
+
_shouldColorize = process.stdout?.isTTY ?? true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return _shouldColorize
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
/** Wrap a string with ANSI codes, handling nested resets. */
|
|
37
65
|
function ansi(text: string, code: string): string {
|
|
38
66
|
return `${code}${text}${RESET}`
|
|
@@ -128,6 +156,9 @@ export function colorizeHelp(program: CommandLike, options?: ColorizeHelpOptions
|
|
|
128
156
|
// Tell Commander that color output is supported, even when stdout is not
|
|
129
157
|
// a TTY (e.g., piped output, CI, tests). Without this, Commander strips
|
|
130
158
|
// all ANSI codes from helpInformation() output.
|
|
159
|
+
//
|
|
160
|
+
// Callers who want to respect NO_COLOR/FORCE_COLOR should check
|
|
161
|
+
// shouldColorize() before calling colorizeHelp().
|
|
131
162
|
program.configureOutput({
|
|
132
163
|
getOutHasColors: () => true,
|
|
133
164
|
getErrHasColors: () => true,
|
package/src/typed.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* to infer option types from .option() calls. Inspired by
|
|
6
6
|
* @commander-js/extra-typings, which achieves similar results with a
|
|
7
7
|
* 1536-line .d.ts using recursive generic accumulation. This
|
|
8
|
-
* implementation achieves the same inference in ~
|
|
8
|
+
* implementation achieves the same inference in ~100 lines of type-level
|
|
9
9
|
* code by leveraging modern TS features (const type params, template
|
|
10
10
|
* literal types, conditional mapped types).
|
|
11
11
|
*
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* const cli = createCLI("myapp")
|
|
17
17
|
* .description("My app")
|
|
18
18
|
* .option("-v, --verbose", "Increase verbosity")
|
|
19
|
-
* .option("-p, --port <number>", "Port to listen on")
|
|
19
|
+
* .option("-p, --port <number>", "Port to listen on", parseInt)
|
|
20
20
|
* .option("-o, --output [path]", "Output path")
|
|
21
21
|
* .option("--no-color", "Disable color output")
|
|
22
22
|
*
|
|
23
23
|
* cli.parse()
|
|
24
24
|
* const opts = cli.opts()
|
|
25
|
-
* // ^? { verbose: boolean, port:
|
|
25
|
+
* // ^? { verbose: boolean, port: number, output: string | true, color: boolean }
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
|
|
@@ -41,7 +41,7 @@ import { colorizeHelp } from "./index.ts"
|
|
|
41
41
|
// Negated flags (--no-X) are detected and produce a `X: boolean` key.
|
|
42
42
|
|
|
43
43
|
/** Flatten intersection types for clean hover output */
|
|
44
|
-
type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
|
44
|
+
export type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
|
45
45
|
|
|
46
46
|
/** Check if a flags string is a negated flag like "--no-color" */
|
|
47
47
|
type IsNegated<S extends string> = S extends `${string}--no-${string}` ? true : false
|
|
@@ -72,27 +72,85 @@ type CamelCase<S extends string> = S extends `${infer A}-${infer B}${infer Rest}
|
|
|
72
72
|
: S
|
|
73
73
|
|
|
74
74
|
/** Determine the value type from a flags string */
|
|
75
|
-
type FlagValueType<S extends string> =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
type FlagValueType<S extends string> =
|
|
76
|
+
IsNegated<S> extends true
|
|
77
|
+
? boolean // negated flags are always boolean
|
|
78
|
+
: S extends `${string}<${string}>`
|
|
79
|
+
? string // required arg → string
|
|
80
|
+
: S extends `${string}[${string}]`
|
|
81
|
+
? string | true // optional arg → string | true
|
|
82
|
+
: boolean // no arg → boolean
|
|
82
83
|
|
|
83
84
|
/** Add a flag to an options record */
|
|
84
85
|
type AddOption<Opts, Flags extends string, Default = undefined> = Opts & {
|
|
85
86
|
[K in ExtractLongName<Flags>]: Default extends undefined ? FlagValueType<Flags> | undefined : FlagValueType<Flags>
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// --- Type-level argument parsing ---
|
|
90
|
+
|
|
91
|
+
/** Extract whether an argument is required (<name>) or optional ([name]) */
|
|
92
|
+
type ArgType<S extends string> = S extends `<${string}>`
|
|
93
|
+
? string
|
|
94
|
+
: S extends `[${string}]`
|
|
95
|
+
? string | undefined
|
|
96
|
+
: string
|
|
97
|
+
|
|
98
|
+
// --- Typed opts helper (resolves accumulated Opts for action handlers) ---
|
|
99
|
+
/** Resolve accumulated option types for use in action handler signatures */
|
|
100
|
+
export type TypedOpts<Opts> = Prettify<Opts>
|
|
101
|
+
|
|
102
|
+
// --- Zod schema support (duck-typed, no import) ---
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Duck-type interface for Zod-like schemas.
|
|
106
|
+
* Avoids importing zod — it's an optional peer dependency.
|
|
107
|
+
* Any object with `parse(value: string) => T` and `_def` qualifies.
|
|
108
|
+
*/
|
|
109
|
+
interface ZodLike<T = any> {
|
|
110
|
+
parse(value: unknown): T
|
|
111
|
+
_def: unknown
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Type-level extraction: if Z is a Zod schema, infer its output type */
|
|
115
|
+
type InferZodOutput<Z> = Z extends { parse(value: unknown): infer T; _def: unknown } ? T : never
|
|
116
|
+
|
|
117
|
+
/** Runtime check: is this value a Zod-like schema? */
|
|
118
|
+
function isZodSchema(value: unknown): value is ZodLike {
|
|
119
|
+
return (
|
|
120
|
+
typeof value === "object" &&
|
|
121
|
+
value !== null &&
|
|
122
|
+
typeof (value as any).parse === "function" &&
|
|
123
|
+
"_def" in (value as any)
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Wrap a Zod schema as a Commander parser function */
|
|
128
|
+
function zodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
129
|
+
return (value: string) => {
|
|
130
|
+
try {
|
|
131
|
+
return schema.parse(value)
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
// Format Zod errors as Commander-style messages
|
|
134
|
+
if (err?.issues) {
|
|
135
|
+
const messages = err.issues.map((i: any) => i.message).join(", ")
|
|
136
|
+
throw new Error(messages)
|
|
137
|
+
}
|
|
138
|
+
throw err
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
88
143
|
// --- Typed Command ---
|
|
89
144
|
|
|
90
145
|
/**
|
|
91
|
-
* A Commander Command with inferred option types.
|
|
146
|
+
* A Commander Command with inferred option and argument types.
|
|
92
147
|
* Wraps Commander's Command and tracks option types at the type level.
|
|
93
148
|
* Help is automatically colorized.
|
|
149
|
+
*
|
|
150
|
+
* @typeParam Opts - Accumulated option types from .option() calls
|
|
151
|
+
* @typeParam Args - Accumulated argument types from .argument() calls (tuple)
|
|
94
152
|
*/
|
|
95
|
-
export class TypedCommand<Opts = {}> {
|
|
153
|
+
export class TypedCommand<Opts = {}, Args extends any[] = []> {
|
|
96
154
|
readonly _cmd: BaseCommand
|
|
97
155
|
|
|
98
156
|
constructor(name?: string) {
|
|
@@ -112,14 +170,43 @@ export class TypedCommand<Opts = {}> {
|
|
|
112
170
|
return this
|
|
113
171
|
}
|
|
114
172
|
|
|
115
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Add an option with type inference.
|
|
175
|
+
*
|
|
176
|
+
* Supports four overload patterns:
|
|
177
|
+
* 1. `.option(flags, description?)` — type inferred from flags syntax
|
|
178
|
+
* 2. `.option(flags, description, defaultValue)` — removes `undefined` from type
|
|
179
|
+
* 3. `.option(flags, description, parser, defaultValue?)` — type inferred from parser return type
|
|
180
|
+
* 4. `.option(flags, description, zodSchema)` — type inferred from Zod schema output
|
|
181
|
+
*/
|
|
182
|
+
option<const F extends string, Z extends ZodLike>(
|
|
183
|
+
flags: F,
|
|
184
|
+
description: string,
|
|
185
|
+
schema: Z,
|
|
186
|
+
): TypedCommand<Opts & { [K in ExtractLongName<F>]: InferZodOutput<Z> }, Args>
|
|
187
|
+
|
|
188
|
+
option<const F extends string, P extends (value: string, previous: any) => any>(
|
|
189
|
+
flags: F,
|
|
190
|
+
description: string,
|
|
191
|
+
parseArg: P,
|
|
192
|
+
defaultValue?: ReturnType<P>,
|
|
193
|
+
): TypedCommand<Opts & { [K in ExtractLongName<F>]: ReturnType<P> }, Args>
|
|
194
|
+
|
|
116
195
|
option<const F extends string, D = undefined>(
|
|
117
196
|
flags: F,
|
|
118
197
|
description?: string,
|
|
119
198
|
defaultValue?: D,
|
|
120
|
-
): TypedCommand<AddOption<Opts, F, D
|
|
121
|
-
|
|
122
|
-
|
|
199
|
+
): TypedCommand<AddOption<Opts, F, D>, Args>
|
|
200
|
+
|
|
201
|
+
option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): any {
|
|
202
|
+
if (isZodSchema(parseArgOrDefault)) {
|
|
203
|
+
;(this._cmd as any).option(flags, description ?? "", zodParser(parseArgOrDefault))
|
|
204
|
+
} else if (typeof parseArgOrDefault === "function") {
|
|
205
|
+
;(this._cmd as any).option(flags, description ?? "", parseArgOrDefault, defaultValue)
|
|
206
|
+
} else {
|
|
207
|
+
;(this._cmd as any).option(flags, description ?? "", parseArgOrDefault)
|
|
208
|
+
}
|
|
209
|
+
return this
|
|
123
210
|
}
|
|
124
211
|
|
|
125
212
|
/** Add a required option */
|
|
@@ -127,11 +214,31 @@ export class TypedCommand<Opts = {}> {
|
|
|
127
214
|
flags: F,
|
|
128
215
|
description?: string,
|
|
129
216
|
defaultValue?: string,
|
|
130
|
-
): TypedCommand<Opts & { [K in ExtractLongName<F>]: FlagValueType<F> }> {
|
|
217
|
+
): TypedCommand<Opts & { [K in ExtractLongName<F>]: FlagValueType<F> }, Args> {
|
|
131
218
|
;(this._cmd as any).requiredOption(flags, description ?? "", defaultValue)
|
|
132
219
|
return this as any
|
|
133
220
|
}
|
|
134
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Add an option with a fixed set of allowed values (choices).
|
|
224
|
+
* The option type is narrowed to a union of the provided values.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* .optionWithChoices("-e, --env <env>", "Environment", ["dev", "staging", "prod"] as const)
|
|
229
|
+
* // → env: "dev" | "staging" | "prod" | undefined
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
optionWithChoices<const F extends string, const C extends readonly string[]>(
|
|
233
|
+
flags: F,
|
|
234
|
+
description: string,
|
|
235
|
+
choices: C,
|
|
236
|
+
): TypedCommand<Opts & { [K in ExtractLongName<F>]: C[number] | undefined }, Args> {
|
|
237
|
+
const option = new Option(flags, description).choices(choices as unknown as string[])
|
|
238
|
+
;(this._cmd as any).addOption(option)
|
|
239
|
+
return this as any
|
|
240
|
+
}
|
|
241
|
+
|
|
135
242
|
/** Add a subcommand */
|
|
136
243
|
command(nameAndArgs: string, description?: string): TypedCommand<{}> {
|
|
137
244
|
const sub = (this._cmd as any).command(nameAndArgs, description)
|
|
@@ -142,14 +249,24 @@ export class TypedCommand<Opts = {}> {
|
|
|
142
249
|
return typed
|
|
143
250
|
}
|
|
144
251
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Add an argument with type tracking.
|
|
254
|
+
* `<name>` = required (string), `[name]` = optional (string | undefined).
|
|
255
|
+
*/
|
|
256
|
+
argument<const N extends string>(
|
|
257
|
+
name: N,
|
|
258
|
+
description?: string,
|
|
259
|
+
defaultValue?: unknown,
|
|
260
|
+
): TypedCommand<Opts, [...Args, ArgType<N>]> {
|
|
147
261
|
this._cmd.argument(name, description ?? "", defaultValue)
|
|
148
|
-
return this
|
|
262
|
+
return this as any
|
|
149
263
|
}
|
|
150
264
|
|
|
151
|
-
/**
|
|
152
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Set action handler with typed parameters.
|
|
267
|
+
* Callback receives: ...arguments, opts, command.
|
|
268
|
+
*/
|
|
269
|
+
action(fn: (...args: [...Args, Prettify<Opts>, TypedCommand<Opts, Args>]) => void | Promise<void>): this {
|
|
153
270
|
this._cmd.action(fn as any)
|
|
154
271
|
return this
|
|
155
272
|
}
|
|
@@ -263,6 +380,17 @@ export class TypedCommand<Opts = {}> {
|
|
|
263
380
|
this._cmd.showSuggestionAfterError(displaySuggestion)
|
|
264
381
|
return this
|
|
265
382
|
}
|
|
383
|
+
|
|
384
|
+
/** Set environment variable for the last added option (passthrough) */
|
|
385
|
+
env(name: string): this {
|
|
386
|
+
// Commander's .env() is on Option, not Command. We apply it to the last option.
|
|
387
|
+
const opts = (this._cmd as any).options as any[]
|
|
388
|
+
if (opts.length > 0) {
|
|
389
|
+
opts[opts.length - 1].envVar = name
|
|
390
|
+
opts[opts.length - 1].envVarRequired = false
|
|
391
|
+
}
|
|
392
|
+
return this
|
|
393
|
+
}
|
|
266
394
|
}
|
|
267
395
|
|
|
268
396
|
/**
|
|
@@ -276,11 +404,11 @@ export class TypedCommand<Opts = {}> {
|
|
|
276
404
|
* .description("My tool")
|
|
277
405
|
* .version("1.0.0")
|
|
278
406
|
* .option("-v, --verbose", "Verbose output")
|
|
279
|
-
* .option("-p, --port <number>", "Port")
|
|
407
|
+
* .option("-p, --port <number>", "Port", parseInt)
|
|
280
408
|
*
|
|
281
409
|
* program.parse()
|
|
282
410
|
* const { verbose, port } = program.opts()
|
|
283
|
-
* // ^boolean ^
|
|
411
|
+
* // ^boolean ^number | undefined
|
|
284
412
|
* ```
|
|
285
413
|
*/
|
|
286
414
|
export function createCLI(name?: string): TypedCommand<{}> {
|