UNPKG

zlye

Version:

A type-safe CLI parser with a Zod-like schema validation.

382 lines (292 loc) 9.37 kB
# Zlye ✨ *A delightfully simple, type-safe CLI parser for Node.js* Building command-line tools should be enjoyable. Zlye brings the elegance of Zod's schema validation to CLI parsing, making your applications both powerful and maintainable. ## Why Developers Love Zlye 🎨 **Beautiful Help Messages** - Automatically generated, colorized, and intuitive 💫 **Zero Learning Curve** - If you know Zod, you already know Zlye 🔧 **Full TypeScript Support** - Complete type safety from input to output ✨ **Effortless Validation** - Rich error messages that guide your users ## Installation ```bash npm install zlye ``` ## Quick Start ```typescript import { cli, z } from 'zlye' const program = cli() .name('my-app') .version('1.0.0') .description('A simple CLI application') .option('verbose', z.boolean().describe('Enable verbose output')) .option('output', z.string().describe('Output file path')) const result = program.parse() if (result) { console.log('Options:', result.options) // Fully typed! console.log('Arguments:', result.positionals) // Fully typed! } ``` ## Beautiful Help System Zlye automatically generates beautiful help messages based on your schema definitions: ```bash $ myapp --help My awesome CLI tool (v2.1.0) Usage: myapp [command] [...flags] [...args] Commands: build docker build . Build a Docker image run docker run ubuntu:latest Run a container <command> --help Print help text for command. Flags: -c, --config <val> Path to configuration file (default: "./config.json") -p, --port <n> Server port (min: 1024, max: 65535, default: 3000) --features <val,...> List of features to enable -h, --help Display this menu and exit Examples: myapp --verbose myapp build --output dist/ ``` ### Command-specific Help ```bash $ myapp build --help Usage: myapp build [...flags] <context> Build a Docker image Arguments: <context> Build context directory Flags: -f, --file <val> Dockerfile path (default: "./Dockerfile") -t, --tag <val> Image tag --no-cache Do not use cache (default: false) -h, --help Display this menu and exit Examples: myapp build . myapp build --tag myapp:latest . myapp build --file ./custom.Dockerfile --no-cache . ``` ## Complete Type Safety Zlye is built for full type safety: ```typescript const program = cli() .option('count', z.number().min(1)) .option('name', z.string().optional()) .positional('source', z.string().describe('Source file path')) .positional('copies', z.number().int().positive().describe('Number of copies to create')) .positional('tags', z.array(z.string()).describe('List of tags to apply')) const result = program.parse() if (result) { // TypeScript knows these types! result.options.count // number result.options.name // string | undefined result.positionals // readonly [string, number, string[]] } ``` ## Intelligent Error Messages Zlye provides detailed error messages for validation failures: ```bash $ myapp --port 99999 Error: --port must be at most 65535 $ myapp build Error: Argument "context" is required $ myapp --invalid-flag Error: Unknown option: --invalid-flag $ myapp --numbers 2,-10 Error: Argument "numbers": first value must be positive $ my-cli --env invalid Error: --env must be one of dev, staging, or prod $ my-cli --envs dev,invalid,prod Error: --envs: second value must be one of dev, staging, or prod ``` ## Core Concepts ### Basic CLI Setup ```typescript import { cli, z } from 'zlye' const program = cli() .name('myapp') // Program name .version('2.1.0') // Version string .description('My awesome CLI tool') // Description .example([ // Usage examples 'myapp --verbose', 'myapp build --output dist/' ]) ``` ### Adding Options ```typescript const program = cli() .option('config', z.string() .describe('Path to configuration file') .alias('c') .example('./config.json') ) .option('port', z.number() .min(1024) .max(65535) .describe('Server port') .default(3000) ) .option('features', z.array(z.string()) .describe('List of features to enable') ) ``` ### Positional Arguments ```typescript // Simple positional arguments const program = cli() .positional('input', z.string().describe('Input file')) .positional('output', z.string().describe('Output file').optional()) // Advanced positional with validation const program = cli() .positional('command', z.string() .choices(['start', 'stop', 'restart']) .describe('Action to perform') ) ``` ## Schema Types ### String Schema ```typescript // Basic string z.string() // String with constraints z.string() .min(3) // Minimum length .max(50) // Maximum length .regex(/^[a-z]+$/, 'Must be lowercase') // Pattern matching .choices(['red', 'green', 'blue']) // Predefined choices .describe('Color selection') // Description for help .alias('c') // Short flag alias .example('red') // Example value .default('blue') // Default value .optional() // Make optional ``` ### Number Schema ```typescript // Basic number z.number() // Number with constraints z.number() .min(0) // Minimum value .max(100) // Maximum value .int() // Must be integer .positive() // Must be positive .negative() // Must be negative .describe('Port number') .default(3000) ``` ### Boolean Schema ```typescript // Boolean flags z.boolean() .describe('Enable debug mode') .alias('d') .default(false) ``` ### Array Schema ```typescript // Array of strings z.array(z.string()) .min(1) // Minimum items .max(5) // Maximum items .describe('List of files') // Array of numbers z.array(z.number().positive()) .describe('List of port numbers') // Arrays can be provided as comma-separated values: // --files file1.txt,file2.txt,file3.txt // Or as multiple flags: // --files file1.txt --files file2.txt ``` ### Object Schema ```typescript // Nested object configuration z.object({ host: z.string().default('localhost'), port: z.number().min(1).max(65535).default(3000), ssl: z.boolean().default(false) }) .describe('Server configuration') // Usage: --server.host=example.com --server.port=8080 --server.ssl ``` ### Schema Transformations ```typescript // Transform string to uppercase z.string().transform(s => s.toUpperCase()) // Parse JSON string to object z.string().transform(s => JSON.parse(s)) // Convert string to number z.string().regex(/^\d+$/).transform(s => parseInt(s)) ``` ## Commands ```typescript import { cli, z } from 'zlye' const program = cli() .name('my-app') .description('A simple CLI application') program .command('greet', { name: z.string() .describe('Name to greet') .default('world'), uppercase: z.boolean() .describe('Convert greeting to uppercase') }) .description('Greet someone') .example([ 'my-app greet', 'my-app greet --name Alice', 'my-app greet --name Bob --uppercase' ]) .action(({ options }) => { let greeting = `Hello, ${options.name}!` if (options.uppercase) { greeting = greeting.toUpperCase() } console.log(greeting) }) program.parse() ``` ### Important Notes - You can have multiple commands in your CLI application - You can have both global options and commands in the same program - When a command executes and you have both options and a command defined, the result of `program.parse()` will be `undefined` - the command's options are available through the action callback parameters instead ## Advanced Features ### Custom Validation ```typescript // Email validation z.string() .regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Must be a valid email') // URL validation z.string() .regex(/^https?:\/\/.+/, 'Must be a valid URL') // File path validation z.string() .transform(path => { if (!fs.existsSync(path)) { throw new Error(`File not found: ${path}`) } return path }) ``` ### Transforming Option Values You can transform option values into different types using the `transform` method: ```typescript import fs from 'fs' const program = cli() .option('config', z.string() .describe('Configuration file path') .default('./config.json') .transform(configPath => { if (!fs.existsSync(configPath)) { throw new Error(`Config file not found: ${configPath}`) } return JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, any> }) ) ``` When you access the value, it will be the parsed object with the correct type: ```typescript const result = program.parse() if (result) { console.log(result.options.config) // Record<string, any> } ``` ## Contributing We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. ## License MIT License - see [LICENSE](LICENSE) file for details.