@silvery/commander 0.6.0 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +34 -24
  2. package/package.json +2 -5
  3. package/src/colorize.ts +44 -103
package/README.md CHANGED
@@ -12,44 +12,54 @@ npm install @silvery/commander
12
12
  import { Command, z } from "@silvery/commander"
13
13
 
14
14
  const program = new Command("deploy")
15
- .description("Deploy to an environment")
15
+ .description("Deploy services to an environment")
16
16
  .version("1.0.0")
17
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")
18
24
  .option("-p, --port <n>", "Port number", z.port)
19
25
  .option("-r, --retries <n>", "Retry count", z.int)
20
- .option("--tags <t>", "Labels", z.csv)
21
- .option("-f, --force", "Skip confirmation")
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
+ })
22
44
 
23
45
  program.parse()
24
- const { env, port, retries, tags, force } = program.opts()
25
- // │└─ boolean | undefined
26
- // │ │ └──────── string[]
27
- // │ │ └────────────────── number
28
- // │ └──────────────────────── number (1–65535)
29
- // └─────────────────────────────── "dev" | "staging" | "prod"
46
+ const { env, force, verbose } = program.opts()
47
+ // │ │ └─ boolean | undefined
48
+ // │ └────────── boolean | undefined
49
+ // └──────────────── "dev" | "staging" | "prod"
30
50
  ```
31
51
 
32
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.
33
53
 
34
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.
35
55
 
36
- <pre><code>$ deploy --help
37
-
38
- <b>Usage:</b> <span style="color:#56b6c2">deploy</span> <span style="color:#98c379">[options]</span>
39
-
40
- Deploy to an environment
56
+ Help is auto-colorized — bold headings, green flags, cyan commands, dim descriptions:
41
57
 
42
- <b>Options:</b>
43
- <span style="color:#98c379">-V, --version</span> <span style="color:#888">output the version number</span>
44
- <span style="color:#98c379">-e, --env &lt;env&gt;</span> <span style="color:#888">Target environment</span>
45
- <span style="color:#98c379">-p, --port &lt;n&gt;</span> <span style="color:#888">Port number</span>
46
- <span style="color:#98c379">-r, --retries &lt;n&gt;</span> <span style="color:#888">Retry count</span>
47
- <span style="color:#98c379">--tags &lt;t&gt;</span> <span style="color:#888">Labels</span>
48
- <span style="color:#98c379">-f, --force</span> <span style="color:#888">Skip confirmation</span>
49
- <span style="color:#98c379">-h, --help</span> <span style="color:#888">display help for command</span>
50
- </code></pre>
58
+ <p align="center">
59
+ <img src="help-output.svg" alt="Colorized help output" width="80%" />
60
+ </p>
51
61
 
52
- Help is auto-colorized — bold headings, green flags, cyan commands, dim descriptions. Options with [Zod](https://github.com/colinhacks/zod) schemas or built-in types are validated at parse time with clear error messages.
62
+ Options with [Zod](https://github.com/colinhacks/zod) schemas or built-in types are validated at parse time with clear error messages.
53
63
 
54
64
  ## What's included
55
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silvery/commander",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Colorized Commander.js help output using ANSI escape codes",
5
5
  "keywords": [
6
6
  "ansi",
@@ -29,16 +29,13 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
+ "@silvery/ansi": ">=1.0.0",
32
33
  "commander": ">=12.0.0"
33
34
  },
34
35
  "peerDependencies": {
35
- "@silvery/ansi": ">=0.1.0",
36
36
  "zod": ">=3.0.0"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
- "@silvery/ansi": {
40
- "optional": true
41
- },
42
39
  "zod": {
43
40
  "optional": true
44
41
  }
package/src/colorize.ts CHANGED
@@ -1,71 +1,38 @@
1
1
  /**
2
- * Commander.js help colorization using ANSI escape codes.
2
+ * Commander.js help colorization using @silvery/ansi.
3
3
  *
4
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.
5
+ * rather than regex post-processing.
10
6
  *
11
7
  * @example
12
8
  * ```ts
13
9
  * import { Command } from "@silvery/commander"
10
+ * // Command auto-colorizes in its constructor — no manual call needed.
11
+ * // For plain Commander:
14
12
  * import { colorizeHelp } from "@silvery/commander"
15
- *
16
- * const program = new Command("myapp").description("My CLI tool")
17
13
  * colorizeHelp(program)
18
14
  * ```
19
15
  */
20
16
 
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"
17
+ import { createStyle } from "@silvery/ansi"
18
+
19
+ // Auto-detect terminal color level. The Style instance handles the full
20
+ // degradation chain: truecolor → 256 → basic (ANSI 16) → null (no color).
21
+ // When level is null (NO_COLOR), style methods return plain text.
22
+ const s = createStyle()
28
23
 
29
24
  /**
30
25
  * 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.
26
+ * Delegates to @silvery/ansi's auto-detection (NO_COLOR, FORCE_COLOR, TERM).
33
27
  */
34
- let _shouldColorize: boolean | undefined
35
-
36
28
  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}`
29
+ return s.level > 0
60
30
  }
61
31
 
62
32
  /**
63
33
  * Minimal interface for Commander's Command — avoids requiring Commander
64
34
  * as a direct dependency. Works with both `commander` and
65
35
  * `@silvery/commander`.
66
- *
67
- * Uses permissive types to ensure structural compatibility with all
68
- * Commander versions, overloads, and generic instantiations.
69
36
  */
70
37
  export interface CommandLike {
71
38
  // biome-ignore lint: permissive to match Commander's overloaded signatures
@@ -76,82 +43,56 @@ export interface CommandLike {
76
43
  readonly commands: readonly any[]
77
44
  }
78
45
 
79
- /** Color scheme for help output. Values are raw ANSI escape sequences. */
46
+ /** Color scheme for help output. Each value is a styling function (text → styled text). */
80
47
  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
48
+ /** Style for command/subcommand names. Default: cyan */
49
+ commands?: (text: string) => string
50
+ /** Style for --flags and -short options. Default: green */
51
+ flags?: (text: string) => string
52
+ /** Style for description text. Default: dim */
53
+ description?: (text: string) => string
54
+ /** Style for section headings (Usage:, Options:, etc.). Default: bold */
55
+ heading?: (text: string) => string
56
+ /** Style for <required> and [optional] argument brackets. Default: yellow */
57
+ brackets?: (text: string) => string
91
58
  }
92
59
 
93
60
  /**
94
61
  * Apply colorized help output to a Commander.js program and all its subcommands.
95
62
  *
96
63
  * 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.
64
+ * post-processing the formatted string.
99
65
  *
100
66
  * @param program - A Commander Command instance (or compatible object)
101
- * @param options - Override default ANSI color codes for each element
67
+ * @param options - Override default style functions for each element
102
68
  */
103
69
  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
70
+ // Ensure style generates codes — at minimum basic (ANSI 16).
71
+ // Auto-detected level may be higher (256/truecolor) for richer output.
72
+ // May be 0 if NO_COLOR is set; in that case, force basic since Commander
73
+ // handles the final strip via configureOutput.
74
+ if (s.level === 0) s.level = 1
75
+
76
+ const cmds = options?.commands ?? ((t: string) => s.cyan(t))
77
+ const flags = options?.flags ?? ((t: string) => s.green(t))
78
+ const desc = options?.description ?? ((t: string) => s.dim(t))
79
+ const heading = options?.heading ?? ((t: string) => s.bold(t))
80
+ const brackets = options?.brackets ?? ((t: string) => s.yellow(t))
109
81
 
110
82
  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
- },
83
+ styleTitle: (str: string) => heading(str),
84
+ styleCommandText: (str: string) => cmds(str),
85
+ styleOptionText: (str: string) => flags(str),
86
+ styleSubcommandText: (str: string) => cmds(str),
87
+ styleArgumentText: (str: string) => brackets(str),
88
+ styleDescriptionText: (str: string) => desc(str),
89
+ styleCommandDescription: (str: string) => str,
145
90
  }
146
91
 
147
92
  program.configureHelp(helpConfig)
148
93
 
149
94
  // 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().
95
+ // a TTY. Without this, Commander strips ANSI codes from helpInformation().
155
96
  program.configureOutput({
156
97
  getOutHasColors: () => true,
157
98
  getErrHasColors: () => true,