@silvery/commander 0.5.0 → 0.6.1

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 CHANGED
@@ -1,178 +1,86 @@
1
1
  # @silvery/commander
2
2
 
3
- Enhanced [Commander.js](https://github.com/tj/commander.js) with type-safe options, auto-colorized help, [Standard Schema](https://github.com/standard-schema/standard-schema) validation, and built-in CLI types.
3
+ Type-safe [Commander.js](https://github.com/tj/commander.js) with validated options, colorized help, and [Standard Schema](https://github.com/standard-schema/standard-schema) support. Drop-in replacement — `Command` extends Commander's `Command`. Install once, Commander is included.
4
4
 
5
- Drop-in replacement -- `Command` is a subclass of Commander's `Command` with full type inference for options, arguments, and parsed values. Install once, Commander is included.
6
-
7
- ## Usage
8
-
9
- ```typescript
10
- import { Command, port, csv } from "@silvery/commander"
11
-
12
- new Command("deploy")
13
- .description("Deploy the application")
14
- .version("1.0.0")
15
- .option("-p, --port <n>", "Port", port)
16
- .option("--tags <t>", "Tags", csv)
17
- .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
18
-
19
- program.parse()
5
+ ```bash
6
+ npm install @silvery/commander
20
7
  ```
21
8
 
22
- Help output is automatically colorized -- bold headings, green flags, cyan commands, dim descriptions, yellow arguments. Uses [Commander's](https://github.com/tj/commander.js) built-in `configureHelp()` style hooks.
23
-
24
- Colorization works out of the box with raw ANSI codes. Install [`@silvery/ansi`](https://github.com/beorn/silvery/tree/main/packages/ansi) for full terminal capability detection (respects `NO_COLOR`, `FORCE_COLOR`, and `isTTY`).
25
-
26
- ## Validated options with built-in types
27
-
28
- Commander's `.option()` accepts a string and gives you a string back. Our built-in types parse and validate in one step:
29
-
30
- ```typescript
31
- import { Command, port, csv, int } from "@silvery/commander"
32
-
33
- new Command("deploy")
34
- .option("-p, --port <n>", "Port", port) // number (1-65535, validated)
35
- .option("--tags <t>", "Tags", csv) // string[]
36
- .option("-r, --retries <n>", "Retries", int) // number (integer)
37
- .option("-e, --env <e>", "Env", ["dev", "staging", "prod"]) // choices
38
- ```
39
-
40
- These types are **not part of Commander** -- they're provided by `@silvery/commander`. Each implements [Standard Schema v1](https://github.com/standard-schema/standard-schema), so they work with any schema-aware tooling. They have zero dependencies.
41
-
42
- ### Available types
43
-
44
- | Type | Output | Validation |
45
- | ------- | ---------- | ---------------------------------------- |
46
- | `int` | `number` | Integer (coerced from string) |
47
- | `uint` | `number` | Unsigned integer (>= 0) |
48
- | `float` | `number` | Any finite number (rejects NaN) |
49
- | `port` | `number` | Integer 1-65535 |
50
- | `url` | `string` | Valid URL (via `URL` constructor) |
51
- | `path` | `string` | Non-empty string |
52
- | `csv` | `string[]` | Comma-separated, trimmed, empty filtered |
53
- | `json` | `unknown` | Parsed JSON |
54
- | `bool` | `boolean` | true/false/yes/no/1/0 (case-insensitive) |
55
- | `date` | `Date` | Valid date string |
56
- | `email` | `string` | Basic email validation |
57
- | `regex` | `RegExp` | Valid regex pattern |
58
-
59
- ### Factory type
60
-
61
- ```typescript
62
- import { intRange } from "@silvery/commander"
63
-
64
- intRange(1, 100) // CLIType<number> -- integer within bounds
65
- ```
66
-
67
- ### Array choices
68
-
69
- Pass an array as the third argument to restrict an option to a fixed set of values:
70
-
71
- ```typescript
72
- .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
73
- ```
74
-
75
- Commander validates the choice at parse time and rejects invalid values.
76
-
77
- ### Standalone usage
78
-
79
- Types work outside Commander too -- for validating env vars, config files, etc.:
80
-
81
- ```typescript
82
- import { port, csv } from "@silvery/commander/parse"
83
-
84
- port.parse("3000") // 3000
85
- port.parse("abc") // throws: 'Expected port (1-65535), got "abc"'
86
- port.safeParse("3000") // { success: true, value: 3000 }
87
- port.safeParse("abc") // { success: false, issues: [{ message: "..." }] }
88
- ```
89
-
90
- The `/parse` subpath has zero dependencies -- no Commander, no [Zod](https://github.com/colinhacks/zod).
91
-
92
- ## Standard Schema validation
93
-
94
- Pass any [Standard Schema v1](https://github.com/standard-schema/standard-schema) schema as the third argument to `.option()`. This works with [Zod](https://github.com/colinhacks/zod) (>=3.24), [Valibot](https://github.com/fabian-hiller/valibot) (>=1.0), [ArkType](https://github.com/arktypeio/arktype) (>=2.0), and any library implementing the protocol:
95
-
96
- ```typescript
97
- import { Command } from "@silvery/commander"
98
- import { z } from "zod"
99
-
100
- new Command("deploy")
101
- .option("-p, --port <n>", "Port", z.coerce.number().min(1).max(65535))
102
- .option("-e, --env <env>", "Env", z.enum(["dev", "staging", "prod"]))
103
- .option(
104
- "--tags <t>",
105
- "Tags",
106
- z.string().transform((v) => v.split(",")),
107
- )
108
- ```
109
-
110
- Schema libraries are optional peer dependencies -- detected at runtime, never imported at the top level.
111
-
112
- ## Zod CLI types
113
-
114
- Import `z` from `@silvery/commander` for [Zod](https://github.com/colinhacks/zod) extended with CLI-specific schemas:
9
+ ## Example
115
10
 
116
11
  ```typescript
117
12
  import { Command, z } from "@silvery/commander"
118
13
 
119
- new Command("deploy")
120
- .option("-p, --port <n>", "Port", z.port)
121
- .option("--tags <t>", "Tags", z.csv)
122
- .option("-r, --retries <n>", "Retries", z.int)
123
- .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
124
- ```
125
-
126
- The `z` export is tree-shakeable -- if you don't import it, [Zod](https://github.com/colinhacks/zod) won't be in your bundle. Requires `zod` as a peer dependency.
127
-
128
- Available: `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)`.
129
-
130
- ## Function parsers
131
-
132
- [Commander's](https://github.com/tj/commander.js) standard parser function pattern also works:
14
+ const program = new Command("deploy")
15
+ .description("Deploy services to an environment")
16
+ .version("1.0.0")
17
+ .option("-e, --env <env>", "Target environment", z.enum(["dev", "staging", "prod"]))
18
+ .option("-f, --force", "Skip confirmation")
19
+ .option("-v, --verbose", "Verbose output")
20
+
21
+ program
22
+ .command("start <service>")
23
+ .description("Start a service")
24
+ .option("-p, --port <n>", "Port number", z.port)
25
+ .option("-r, --retries <n>", "Retry count", z.int)
26
+ .action((service, opts) => {
27
+ /* ... */
28
+ })
29
+
30
+ program
31
+ .command("stop <service>")
32
+ .description("Stop a running service")
33
+ .action((service) => {
34
+ /* ... */
35
+ })
36
+
37
+ program
38
+ .command("status")
39
+ .description("Show service status")
40
+ .option("--json", "Output as JSON")
41
+ .action((opts) => {
42
+ /* ... */
43
+ })
133
44
 
134
- ```typescript
135
- new Command("app")
136
- .option("-p, --port <n>", "Port", parseInt) // number
137
- .option("--tags <items>", "Tags", (v) => v.split(",")) // string[]
138
- .option("-p, --port <n>", "Port", parseInt, 8080) // number (with default)
45
+ program.parse()
46
+ const { env, force, verbose } = program.opts()
47
+ // │ │ └─ boolean | undefined
48
+ // │ └────────── boolean | undefined
49
+ // └──────────────── "dev" | "staging" | "prod"
139
50
  ```
140
51
 
141
- ## colorizeHelp()
52
+ With plain Commander, `opts()` returns `Record<string, any>` — every value is untyped. With `@silvery/commander`, each option's type is inferred from its schema: `z.port` produces `number`, `z.enum(...)` produces a union, `z.csv` produces `string[]`. Invalid values are rejected at parse time with clear error messages — not silently passed through as strings.
142
53
 
143
- Use standalone with a plain [Commander](https://github.com/tj/commander.js) `Command` (without subclassing):
54
+ [Zod](https://github.com/colinhacks/zod) is entirely optional — `z` is tree-shaken from your bundle if you don't import it. Without Zod, use the built-in types (`port`, `int`, `csv`) or plain Commander.
144
55
 
145
- ```typescript
146
- import { Command } from "commander"
147
- import { colorizeHelp } from "@silvery/commander"
56
+ Help is auto-colorized — bold headings, green flags, cyan commands, dim descriptions:
148
57
 
149
- const program = new Command("myapp")
150
- colorizeHelp(program) // applies recursively to all subcommands
151
- ```
58
+ <p align="center">
59
+ <img src="help-output.svg" alt="Colorized help output" width="80%" />
60
+ </p>
152
61
 
153
- ## Import paths
62
+ Options with [Zod](https://github.com/colinhacks/zod) schemas or built-in types are validated at parse time with clear error messages.
154
63
 
155
- | Path | What | Dependencies |
156
- | -------------------------- | ------------------------------- | ----------------------------------------------- |
157
- | `@silvery/commander` | Command, colorizeHelp, types, z | [commander](https://github.com/tj/commander.js) |
158
- | `@silvery/commander/parse` | Types only (.parse/.safeParse) | none |
64
+ ## What's included
159
65
 
160
- ## Beyond extra-typings
66
+ - **Colorized help** — automatic, with color level detection and [`NO_COLOR`](https://no-color.org)/`FORCE_COLOR` support via [`@silvery/ansi`](https://github.com/beorn/silvery/tree/main/packages/ansi) (optional)
67
+ - **Typed `.option()` parsing** — pass a type as the third argument:
68
+ - 14 built-in types — `port`, `int`, `csv`, `url`, `email`, `date`, [more](https://silvery.dev/reference/commander)
69
+ - Array choices — `["dev", "staging", "prod"]`
70
+ - [Zod](https://github.com/colinhacks/zod) schemas — `z.port`, `z.int`, `z.csv`, or any custom `z.string()`, `z.number()`, etc.
71
+ - Any [Standard Schema](https://github.com/standard-schema/standard-schema) library — [Valibot](https://github.com/fabian-hiller/valibot), [ArkType](https://github.com/arktypeio/arktype)
72
+ - All types usable standalone via `.parse()`/`.safeParse()`
161
73
 
162
- Built on the shoulders of [@commander-js/extra-typings](https://github.com/commander-js/extra-typings). We add:
74
+ ## Docs
163
75
 
164
- - **Auto-colorized help** -- bold headings, green flags, cyan commands
165
- - **Built-in validation** via [Standard Schema](https://github.com/standard-schema/standard-schema) -- works with [Zod](https://github.com/colinhacks/zod), [Valibot](https://github.com/fabian-hiller/valibot), [ArkType](https://github.com/arktypeio/arktype)
166
- - **14 CLI types** -- `port`, `csv`, `int`, `url`, `email` and more, usable standalone via `.parse()`/`.safeParse()`
167
- - **NO_COLOR support** via [`@silvery/ansi`](https://github.com/beorn/silvery/tree/main/packages/ansi) (optional)
168
- - **Commander included** -- one install, no peer dep setup
76
+ Full reference, type table, and API details at **[silvery.dev/reference/commander](https://silvery.dev/reference/commander)**.
169
77
 
170
78
  ## Credits
171
79
 
172
- - **[Commander.js](https://github.com/tj/commander.js)** by TJ Holowaychuk and contributors -- the underlying CLI framework
173
- - **[@commander-js/extra-typings](https://github.com/commander-js/extra-typings)** -- inspired the type inference approach
174
- - **[Standard Schema](https://github.com/standard-schema/standard-schema)** -- universal schema interop protocol
175
- - **[@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi)** -- optional terminal capability detection
80
+ - **[Commander.js](https://github.com/tj/commander.js)** by TJ Holowaychuk and contributors
81
+ - **[@commander-js/extra-typings](https://github.com/commander-js/extra-typings)** inspired the type inference approach
82
+ - **[Standard Schema](https://github.com/standard-schema/standard-schema)** universal schema interop protocol
83
+ - **[@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi)** terminal capability detection
176
84
 
177
85
  ## License
178
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silvery/commander",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Colorized Commander.js help output using ANSI escape codes",
5
5
  "keywords": [
6
6
  "ansi",
@@ -29,16 +29,14 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
+ "@silvery/ansi": ">=0.1.0",
33
+ "@silvery/style": ">=0.1.0",
32
34
  "commander": ">=12.0.0"
33
35
  },
34
36
  "peerDependencies": {
35
- "@silvery/ansi": ">=0.1.0",
36
37
  "zod": ">=3.0.0"
37
38
  },
38
39
  "peerDependenciesMeta": {
39
- "@silvery/ansi": {
40
- "optional": true
41
- },
42
40
  "zod": {
43
41
  "optional": true
44
42
  }
package/src/colorize.ts CHANGED
@@ -6,8 +6,6 @@
6
6
  * or plain commander — accepts a minimal CommandLike interface so Commander
7
7
  * is a peer dependency, not a hard one.
8
8
  *
9
- * Zero dependencies — only raw ANSI escape codes.
10
- *
11
9
  * @example
12
10
  * ```ts
13
11
  * import { Command } from "@silvery/commander"
@@ -18,39 +16,27 @@
18
16
  * ```
19
17
  */
20
18
 
21
- // Raw ANSI escape codes no framework dependencies.
19
+ import { MODIFIERS, FG_COLORS } from "@silvery/style"
20
+ import { detectColor } from "@silvery/ansi"
21
+
22
+ // Derive ANSI escape sequences from @silvery/style constants.
22
23
  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"
24
+ const BOLD = `\x1b[${MODIFIERS.bold![0]}m`
25
+ const DIM = `\x1b[${MODIFIERS.dim![0]}m`
26
+ const CYAN = `\x1b[${FG_COLORS.cyan}m`
27
+ const GREEN = `\x1b[${FG_COLORS.green}m`
28
+ const YELLOW = `\x1b[${FG_COLORS.yellow}m`
28
29
 
29
30
  /**
30
31
  * 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.
32
+ * Uses @silvery/ansi detectColor() for full detection (respects NO_COLOR,
33
+ * FORCE_COLOR, TERM, etc.).
33
34
  */
34
35
  let _shouldColorize: boolean | undefined
35
36
 
36
37
  export function shouldColorize(): boolean {
37
38
  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
-
39
+ _shouldColorize = detectColor(process.stdout) !== null
54
40
  return _shouldColorize
55
41
  }
56
42
 
package/src/command.ts CHANGED
@@ -96,17 +96,17 @@ export class Command extends BaseCommand {
96
96
  * 4. **Function** -- passed through as Commander's parser function
97
97
  * 5. **Anything else** -- passed through as a default value
98
98
  */
99
- option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
99
+ override option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
100
100
  if (Array.isArray(parseArgOrDefault)) {
101
- const opt = new Option(flags, description ?? "").choices(parseArgOrDefault)
101
+ const opt = new Option(flags, description ?? "").choices(parseArgOrDefault as string[])
102
102
  this.addOption(opt)
103
103
  return this
104
104
  }
105
105
  if (isStandardSchema(parseArgOrDefault)) {
106
- return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault))
106
+ return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault), defaultValue)
107
107
  }
108
108
  if (isLegacyZodSchema(parseArgOrDefault)) {
109
- return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault))
109
+ return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault), defaultValue)
110
110
  }
111
111
  if (typeof parseArgOrDefault === "function") {
112
112
  return super.option(flags, description ?? "", parseArgOrDefault, defaultValue)
@@ -115,7 +115,7 @@ export class Command extends BaseCommand {
115
115
  }
116
116
 
117
117
  // Subcommands also get colorized help, Standard Schema, and array choices
118
- createCommand(name?: string): Command {
118
+ override createCommand(name?: string): Command {
119
119
  return new Command(name)
120
120
  }
121
121
  }