@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/src/index.ts CHANGED
@@ -1,176 +1,14 @@
1
- // Re-export Commander classes (drop-in replacement for @silvery/commander)
2
- export { Command, Option, Argument, CommanderError, InvalidArgumentError, Help } from "commander"
3
- export type { OptionValues } from "commander"
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
- /** Color scheme for help output. Values are raw ANSI escape sequences. */
92
- export interface ColorizeHelpOptions {
93
- /** ANSI code for command/subcommand names. Default: cyan */
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
- // Tell Commander that color output is supported, even when stdout is not
162
- // a TTY (e.g., piped output, CI, tests). Without this, Commander strips
163
- // all ANSI codes from helpInformation() output.
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
- // Apply recursively to all existing subcommands
173
- for (const sub of program.commands) {
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
- * Pre-built Standard Schema v1 presets for common CLI argument patterns.
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 preset implements Standard Schema v1 for interop with any schema library,
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 { createCLI, port, csv, int, url, oneOf } from "@silvery/commander"
10
+ * import { Command, port, csv } from "@silvery/commander"
11
11
  *
12
- * const cli = createCLI("deploy")
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("--callback <url>", "Callback", url) // string (validated URL)
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
- import type { StandardSchemaV1 } from "./typed.ts"
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 preset with standalone parse/safeParse methods. */
28
- export interface Preset<T> extends StandardSchemaV1<T> {
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 createPreset<T>(vendor: string, validate: (value: unknown) => T): Preset<T> {
36
- const schema: Preset<T> = {
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 = createPreset<number>(VENDOR, (v) => {
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 = createPreset<number>(VENDOR, (v) => {
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 = createPreset<number>(VENDOR, (v) => {
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 = createPreset<number>(VENDOR, (v) => {
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 = createPreset<string>(VENDOR, (v) => {
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 = createPreset<string>(VENDOR, (v) => {
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 = createPreset<string[]>(VENDOR, (v) => {
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 = createPreset<unknown>(VENDOR, (v) => {
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 = createPreset<boolean>(VENDOR, (v) => {
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 = createPreset<Date>(VENDOR, (v) => {
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 = createPreset<string>(VENDOR, (v) => {
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 = createPreset<RegExp>(VENDOR, (v) => {
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): Preset<number> {
166
- return createPreset<number>(VENDOR, (v) => {
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
+ }