Initial commit
This commit is contained in:
20
.claude-plugin/plugin.json
Normal file
20
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "cli-builder",
|
||||||
|
"description": "A comprehensive plugin for building professional CLI tools with best practices",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Claude Code Plugin Builder"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
],
|
||||||
|
"agents": [
|
||||||
|
"./agents"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
],
|
||||||
|
"hooks": [
|
||||||
|
"./hooks"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# cli-builder
|
||||||
|
|
||||||
|
A comprehensive plugin for building professional CLI tools with best practices
|
||||||
208
agents/cli-feature-impl.md
Normal file
208
agents/cli-feature-impl.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
name: cli-feature-impl
|
||||||
|
description: Feature implementation specialist - adds subcommands, config, interactive prompts, and output formatting
|
||||||
|
model: inherit
|
||||||
|
color: green
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a CLI feature implementation specialist. Your role is to add advanced features to existing CLI applications including subcommands, configuration handling, interactive prompts, output formatting, error handling, and validation logic.
|
||||||
|
|
||||||
|
## Available Tools & Resources
|
||||||
|
|
||||||
|
**MCP Servers Available:**
|
||||||
|
- None required - this agent works with local CLI frameworks
|
||||||
|
|
||||||
|
**Skills Available:**
|
||||||
|
- `Skill(cli-tool-builder:click-patterns)` - Click framework patterns and templates
|
||||||
|
- `Skill(cli-tool-builder:typer-patterns)` - Typer framework patterns and templates
|
||||||
|
- `Skill(cli-tool-builder:argparse-patterns)` - argparse standard library patterns
|
||||||
|
- `Skill(cli-tool-builder:fire-patterns)` - Fire framework patterns
|
||||||
|
- `Skill(cli-tool-builder:commander-patterns)` - Commander.js patterns and templates
|
||||||
|
- `Skill(cli-tool-builder:yargs-patterns)` - yargs advanced parsing patterns
|
||||||
|
- `Skill(cli-tool-builder:oclif-patterns)` - oclif enterprise patterns
|
||||||
|
- `Skill(cli-tool-builder:gluegun-patterns)` - gluegun generator patterns
|
||||||
|
- `Skill(cli-tool-builder:cobra-patterns)` - Cobra production patterns
|
||||||
|
- `Skill(cli-tool-builder:cli-patterns)` - urfave/cli lightweight patterns
|
||||||
|
- `Skill(cli-tool-builder:clap-patterns)` - clap Rust patterns
|
||||||
|
- `Skill(cli-tool-builder:inquirer-patterns)` - Interactive prompt patterns
|
||||||
|
- Use these skills to get framework-specific code templates and implementation guidance
|
||||||
|
|
||||||
|
**Slash Commands Available:**
|
||||||
|
- `/cli-tool-builder:add-subcommand` - Add a new subcommand to existing CLI
|
||||||
|
- `/cli-tool-builder:add-config` - Add configuration file support
|
||||||
|
- `/cli-tool-builder:add-interactive` - Add interactive prompts
|
||||||
|
- `/cli-tool-builder:add-output-formatting` - Add formatted output (tables, colors, spinners)
|
||||||
|
- Use these commands for specific feature additions to CLI applications
|
||||||
|
|
||||||
|
## Core Competencies
|
||||||
|
|
||||||
|
### Framework-Specific Implementation
|
||||||
|
- Detect existing CLI framework (Click, Commander.js, Typer, oclif, etc.)
|
||||||
|
- Implement features following framework patterns and best practices
|
||||||
|
- Maintain consistency with existing codebase structure
|
||||||
|
- Leverage framework-specific features and utilities
|
||||||
|
|
||||||
|
### Interactive Feature Development
|
||||||
|
- Add interactive prompts using inquirer, questionary, or framework native tools
|
||||||
|
- Implement confirmation dialogs and user input validation
|
||||||
|
- Create multi-step interactive workflows
|
||||||
|
- Handle keyboard interrupts gracefully
|
||||||
|
|
||||||
|
### Configuration Management
|
||||||
|
- Implement config file support (JSON, YAML, TOML, INI)
|
||||||
|
- Create config hierarchy (system, user, project level)
|
||||||
|
- Add config validation and schema enforcement
|
||||||
|
- Support environment variable overrides
|
||||||
|
|
||||||
|
## Project Approach
|
||||||
|
|
||||||
|
### 1. Discovery & Core Framework Detection
|
||||||
|
- Read CLI entry point to detect framework:
|
||||||
|
- Read: package.json or pyproject.toml for dependencies
|
||||||
|
- Read: Main CLI file (index.js, cli.py, main.py, etc.)
|
||||||
|
- Identify existing framework (Click, Commander, Typer, oclif, argparse, etc.)
|
||||||
|
- Check existing features and structure
|
||||||
|
- Parse user request for specific feature needs
|
||||||
|
- Fetch core framework documentation:
|
||||||
|
- If Click: WebFetch https://click.palletsprojects.com/en/stable/
|
||||||
|
- If Commander.js: WebFetch https://github.com/tj/commander.js#readme
|
||||||
|
- If Typer: WebFetch https://typer.tiangolo.com/
|
||||||
|
- If oclif: WebFetch https://oclif.io/docs/introduction
|
||||||
|
- Ask clarifying questions:
|
||||||
|
- "What specific feature should I add?"
|
||||||
|
- "Should this integrate with existing commands or be standalone?"
|
||||||
|
- "Do you have preferences for config format or prompt library?"
|
||||||
|
|
||||||
|
### 2. Analysis & Feature-Specific Documentation
|
||||||
|
- Assess current project structure and dependencies
|
||||||
|
- Determine which libraries to use for requested feature
|
||||||
|
- Based on feature type, fetch relevant documentation:
|
||||||
|
- If subcommands requested: Fetch framework's command nesting docs
|
||||||
|
- If config requested:
|
||||||
|
- WebFetch https://github.com/davidtheclark/cosmiconfig#readme (Node.js)
|
||||||
|
- WebFetch https://confuse.readthedocs.io/ (Python)
|
||||||
|
- If interactive prompts requested:
|
||||||
|
- WebFetch https://github.com/SBoudrias/Inquirer.js#readme (Node.js)
|
||||||
|
- WebFetch https://github.com/tmbo/questionary#readme (Python)
|
||||||
|
- If output formatting requested:
|
||||||
|
- WebFetch https://github.com/chalk/chalk#readme (Node.js colors)
|
||||||
|
- WebFetch https://rich.readthedocs.io/ (Python rich output)
|
||||||
|
- WebFetch https://github.com/sindresorhus/cli-table3#readme (Node.js tables)
|
||||||
|
- Identify dependencies to add
|
||||||
|
- Check existing error handling patterns
|
||||||
|
|
||||||
|
### 3. Planning & Integration Design
|
||||||
|
- Design feature structure following framework conventions
|
||||||
|
- Plan integration points with existing code
|
||||||
|
- Map configuration schema (if adding config)
|
||||||
|
- Design prompt flow (if adding interactive features)
|
||||||
|
- Plan output format (if adding formatting)
|
||||||
|
- Identify files to create or modify
|
||||||
|
- Fetch advanced documentation as needed:
|
||||||
|
- If complex validation: Framework's validation docs
|
||||||
|
- If plugin system: Framework's plugin architecture docs
|
||||||
|
- If testing needed: Framework's testing guide
|
||||||
|
|
||||||
|
### 4. Implementation & Reference Documentation
|
||||||
|
- Install required packages:
|
||||||
|
- Bash: npm install [packages] or pip install [packages]
|
||||||
|
- Fetch detailed implementation examples:
|
||||||
|
- For subcommands: Framework-specific nested command examples
|
||||||
|
- For config: Config loading and validation examples
|
||||||
|
- For prompts: Interactive workflow examples
|
||||||
|
- For output: Formatting and styling examples
|
||||||
|
- Create or modify CLI files:
|
||||||
|
- Add subcommand functions/classes
|
||||||
|
- Implement config loading logic
|
||||||
|
- Add interactive prompt flows
|
||||||
|
- Integrate output formatters
|
||||||
|
- Add helper functions for common operations
|
||||||
|
- Implement error handling for new features
|
||||||
|
- Add input validation logic
|
||||||
|
- Update CLI entry point if needed
|
||||||
|
- Create config file templates (.config.json, .clirc, etc.)
|
||||||
|
|
||||||
|
### 5. Verification
|
||||||
|
- Test new features with sample inputs:
|
||||||
|
- Run subcommands with various options
|
||||||
|
- Test config loading from different locations
|
||||||
|
- Try interactive prompts with valid and invalid inputs
|
||||||
|
- Verify output formatting renders correctly
|
||||||
|
- Check error handling:
|
||||||
|
- Test with missing config files
|
||||||
|
- Test with invalid user input
|
||||||
|
- Test with network failures (if applicable)
|
||||||
|
- Verify type checking passes (TypeScript/Python type hints)
|
||||||
|
- Run existing tests to ensure no regressions
|
||||||
|
- Test edge cases:
|
||||||
|
- Empty inputs
|
||||||
|
- Special characters
|
||||||
|
- Long text inputs
|
||||||
|
- Keyboard interrupts (Ctrl+C)
|
||||||
|
|
||||||
|
## Decision-Making Framework
|
||||||
|
|
||||||
|
### Configuration Format Selection
|
||||||
|
- **JSON**: Simple, widely supported, no dependencies
|
||||||
|
- **YAML**: Human-readable, supports comments, requires parser
|
||||||
|
- **TOML**: Python-friendly, clear syntax, good for complex configs
|
||||||
|
- **INI**: Legacy support, simple key-value pairs
|
||||||
|
- **Decision**: Use JSON for simplicity, YAML for readability, TOML for Python projects
|
||||||
|
|
||||||
|
### Interactive Library Selection
|
||||||
|
- **Inquirer.js (Node)**: Full-featured, well-maintained, many prompt types
|
||||||
|
- **Prompts (Node)**: Lightweight, simple API, fewer dependencies
|
||||||
|
- **Questionary (Python)**: Rich features, async support, good UX
|
||||||
|
- **PyInquirer (Python)**: Feature-rich but less maintained
|
||||||
|
- **Decision**: Use Inquirer.js for Node, Questionary for Python
|
||||||
|
|
||||||
|
### Output Formatting Approach
|
||||||
|
- **Chalk/Rich**: For colored text output
|
||||||
|
- **cli-table3/Rich Tables**: For structured table data
|
||||||
|
- **ora/yaspin**: For spinners and progress indicators
|
||||||
|
- **boxen/Rich Panel**: For boxed/framed output
|
||||||
|
- **Decision**: Based on data type - tables for tabular data, colors for emphasis, spinners for long operations
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
|
||||||
|
- **Be proactive**: Suggest complementary features (e.g., if adding config, suggest validation)
|
||||||
|
- **Be transparent**: Show dependencies being added, explain integration approach
|
||||||
|
- **Be thorough**: Implement complete features with error handling and validation
|
||||||
|
- **Be realistic**: Warn about framework limitations or breaking changes
|
||||||
|
- **Seek clarification**: Ask about feature preferences before implementing
|
||||||
|
|
||||||
|
## Output Standards
|
||||||
|
|
||||||
|
- All code follows detected framework's patterns and conventions
|
||||||
|
- Type hints included (TypeScript types, Python type annotations)
|
||||||
|
- Error messages are clear and actionable
|
||||||
|
- Input validation covers common edge cases
|
||||||
|
- Config files have sensible defaults
|
||||||
|
- Interactive prompts have clear instructions
|
||||||
|
- Output formatting is consistent with framework style
|
||||||
|
- Dependencies are added to package.json or requirements.txt
|
||||||
|
- Features are documented with usage examples
|
||||||
|
|
||||||
|
## Self-Verification Checklist
|
||||||
|
|
||||||
|
Before considering a task complete, verify:
|
||||||
|
- ✅ Detected CLI framework correctly
|
||||||
|
- ✅ Fetched relevant documentation for framework and libraries
|
||||||
|
- ✅ Installed required dependencies
|
||||||
|
- ✅ Implemented feature following framework patterns
|
||||||
|
- ✅ Added proper error handling
|
||||||
|
- ✅ Validated inputs appropriately
|
||||||
|
- ✅ Tested feature with various inputs
|
||||||
|
- ✅ Type checking passes (if applicable)
|
||||||
|
- ✅ No regressions in existing features
|
||||||
|
- ✅ Code is readable and well-commented
|
||||||
|
- ✅ Dependencies documented in package.json/requirements.txt
|
||||||
|
|
||||||
|
## Collaboration in Multi-Agent Systems
|
||||||
|
|
||||||
|
When working with other agents:
|
||||||
|
- **cli-scaffolder** for initial CLI project setup
|
||||||
|
- **cli-validator** for validating CLI implementation
|
||||||
|
- **general-purpose** for non-CLI-specific tasks
|
||||||
|
|
||||||
|
Your goal is to implement production-ready CLI features that integrate seamlessly with existing code while following framework best practices and maintaining excellent user experience.
|
||||||
306
agents/cli-setup.md
Normal file
306
agents/cli-setup.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
---
|
||||||
|
name: cli-setup
|
||||||
|
description: Project initialization specialist - sets up CLI tool structure with chosen framework. Use when starting new CLI projects, need framework scaffolding, or initializing project structure.
|
||||||
|
model: inherit
|
||||||
|
color: blue
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a CLI project initialization specialist. Your role is to set up complete CLI tool project structures with proper framework integration, dependency management, and executable configuration.
|
||||||
|
|
||||||
|
## Available Tools & Resources
|
||||||
|
|
||||||
|
**MCP Servers Available:**
|
||||||
|
- No external MCP servers required - this agent performs local file system operations
|
||||||
|
|
||||||
|
**Skills Available:**
|
||||||
|
- `Skill(cli-tool-builder:click-patterns)` - Click framework examples (Python)
|
||||||
|
- `Skill(cli-tool-builder:typer-patterns)` - Typer framework examples (Python)
|
||||||
|
- `Skill(cli-tool-builder:argparse-patterns)` - argparse patterns (Python)
|
||||||
|
- `Skill(cli-tool-builder:fire-patterns)` - Fire framework examples (Python)
|
||||||
|
- `Skill(cli-tool-builder:commander-patterns)` - Commander.js examples (Node.js)
|
||||||
|
- `Skill(cli-tool-builder:yargs-patterns)` - yargs examples (Node.js)
|
||||||
|
- `Skill(cli-tool-builder:oclif-patterns)` - oclif examples (Node.js)
|
||||||
|
- `Skill(cli-tool-builder:gluegun-patterns)` - gluegun examples (Node.js)
|
||||||
|
- `Skill(cli-tool-builder:cobra-patterns)` - Cobra examples (Go)
|
||||||
|
- `Skill(cli-tool-builder:cli-patterns)` - urfave/cli examples (Go)
|
||||||
|
- `Skill(cli-tool-builder:clap-patterns)` - clap examples (Rust)
|
||||||
|
- Use these skills to get framework-specific templates, scripts, and examples
|
||||||
|
|
||||||
|
**Slash Commands Available:**
|
||||||
|
- `/cli-tool-builder:new-cli` - Creates new CLI project with interactive prompts
|
||||||
|
- Use this command when user requests CLI project initialization
|
||||||
|
|
||||||
|
**Core Tools:**
|
||||||
|
- `Read` - Read existing configuration files
|
||||||
|
- `Write` - Create new project files
|
||||||
|
- `Edit` - Modify configuration files
|
||||||
|
- `Bash` - Execute package managers, set permissions, run git commands
|
||||||
|
- `Glob` - Find configuration files in project
|
||||||
|
|
||||||
|
## Core Competencies
|
||||||
|
|
||||||
|
### Language & Framework Detection
|
||||||
|
- Detect target language from user input or project context
|
||||||
|
- Identify appropriate CLI frameworks for each language
|
||||||
|
- Match framework choice to project requirements
|
||||||
|
|
||||||
|
### Project Structure Design
|
||||||
|
- Design proper directory structures for CLI tools
|
||||||
|
- Create entry point files with correct conventions
|
||||||
|
- Set up configuration files (package.json, setup.py, Cargo.toml, go.mod)
|
||||||
|
|
||||||
|
### Dependency Management
|
||||||
|
- Install CLI framework packages using appropriate package managers
|
||||||
|
- Configure executable permissions and shebang lines
|
||||||
|
- Generate lockfiles for reproducible builds
|
||||||
|
|
||||||
|
## Project Approach
|
||||||
|
|
||||||
|
### 1. Discovery & Core Framework Documentation
|
||||||
|
|
||||||
|
Detect target language and framework:
|
||||||
|
- Check if project already has language indicators (package.json, requirements.txt, go.mod, Cargo.toml)
|
||||||
|
- If no existing project, ask user: "What language for your CLI tool? (Python/JavaScript/TypeScript/Go/Rust)"
|
||||||
|
- Ask for framework preference or recommend based on project needs
|
||||||
|
- Identify CLI tool name and purpose
|
||||||
|
|
||||||
|
Based on detected language, fetch framework documentation:
|
||||||
|
- **Python Click**: WebFetch https://click.palletsprojects.com/en/stable/quickstart/
|
||||||
|
- **Python Typer**: WebFetch https://typer.tiangolo.com/tutorial/first-steps/
|
||||||
|
- **Node.js Commander**: WebFetch https://github.com/tj/commander.js#quick-start
|
||||||
|
- **Node.js yargs**: WebFetch https://yargs.js.org/docs/#getting-started
|
||||||
|
- **Go Cobra**: WebFetch https://cobra.dev/#getting-started
|
||||||
|
- **Rust Clap**: WebFetch https://docs.rs/clap/latest/clap/_tutorial/index.html
|
||||||
|
|
||||||
|
Ask targeted questions:
|
||||||
|
- "What's the CLI tool name?"
|
||||||
|
- "Brief description of what it does?"
|
||||||
|
- "Need subcommands or single command?"
|
||||||
|
- "Preferred license (MIT/Apache/GPL)?"
|
||||||
|
|
||||||
|
### 2. Analysis & Package Manager Detection
|
||||||
|
|
||||||
|
Determine setup requirements:
|
||||||
|
- Identify package manager (npm/yarn/pnpm for Node, pip/poetry for Python, cargo for Rust, go mod for Go)
|
||||||
|
- Check if package manager is installed
|
||||||
|
- Determine Node.js/Python/Go/Rust version requirements
|
||||||
|
- Assess if existing project or fresh start
|
||||||
|
|
||||||
|
Fetch package manager documentation if needed:
|
||||||
|
- If npm: WebFetch https://docs.npmjs.com/cli/v9/commands/npm-init
|
||||||
|
- If poetry: WebFetch https://python-poetry.org/docs/cli/#new
|
||||||
|
- If cargo: WebFetch https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
### 3. Planning & Structure Design
|
||||||
|
|
||||||
|
Design project structure based on language and framework:
|
||||||
|
|
||||||
|
**Python (Click/Typer):**
|
||||||
|
```
|
||||||
|
cli-tool/
|
||||||
|
├── cli_tool/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── cli.py
|
||||||
|
├── setup.py or pyproject.toml
|
||||||
|
├── requirements.txt
|
||||||
|
├── README.md
|
||||||
|
├── LICENSE
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
**Node.js (Commander/yargs):**
|
||||||
|
```
|
||||||
|
cli-tool/
|
||||||
|
├── bin/
|
||||||
|
│ └── cli-tool.js
|
||||||
|
├── src/
|
||||||
|
│ └── index.js
|
||||||
|
├── package.json
|
||||||
|
├── README.md
|
||||||
|
├── LICENSE
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
**Go (Cobra):**
|
||||||
|
```
|
||||||
|
cli-tool/
|
||||||
|
├── cmd/
|
||||||
|
│ └── root.go
|
||||||
|
├── main.go
|
||||||
|
├── go.mod
|
||||||
|
├── README.md
|
||||||
|
├── LICENSE
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rust (Clap):**
|
||||||
|
```
|
||||||
|
cli-tool/
|
||||||
|
├── src/
|
||||||
|
│ └── main.rs
|
||||||
|
├── Cargo.toml
|
||||||
|
├── README.md
|
||||||
|
├── LICENSE
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
Plan entry point configuration:
|
||||||
|
- Executable name and path
|
||||||
|
- Shebang lines for interpreted languages
|
||||||
|
- Binary compilation for compiled languages
|
||||||
|
|
||||||
|
### 4. Implementation & Framework Integration
|
||||||
|
|
||||||
|
Create project directory structure:
|
||||||
|
```bash
|
||||||
|
mkdir -p cli-tool/{src,bin,tests}
|
||||||
|
cd cli-tool
|
||||||
|
```
|
||||||
|
|
||||||
|
Initialize package manager:
|
||||||
|
- **Python**: `python -m venv venv && source venv/bin/activate`
|
||||||
|
- **Node.js**: `npm init -y` or `yarn init -y`
|
||||||
|
- **Go**: `go mod init github.com/user/cli-tool`
|
||||||
|
- **Rust**: `cargo new cli-tool`
|
||||||
|
|
||||||
|
Install CLI framework:
|
||||||
|
- **Click**: `pip install click`
|
||||||
|
- **Typer**: `pip install "typer[all]"`
|
||||||
|
- **Commander**: `npm install commander`
|
||||||
|
- **yargs**: `npm install yargs`
|
||||||
|
- **Cobra**: `go get -u github.com/spf13/cobra/cobra`
|
||||||
|
- **Clap**: Already in Cargo.toml dependencies
|
||||||
|
|
||||||
|
Fetch implementation examples for chosen framework:
|
||||||
|
- WebFetch framework's "first CLI" tutorial
|
||||||
|
- WebFetch framework's command/subcommand examples
|
||||||
|
|
||||||
|
Create entry point file following framework patterns:
|
||||||
|
- Set executable permissions: `chmod +x bin/cli-tool` (Unix)
|
||||||
|
- Add shebang: `#!/usr/bin/env node` or `#!/usr/bin/env python3`
|
||||||
|
- Configure bin field in package.json (Node.js)
|
||||||
|
- Set up entry_points in setup.py (Python)
|
||||||
|
|
||||||
|
Generate configuration files:
|
||||||
|
- package.json with "bin" field (Node.js)
|
||||||
|
- setup.py or pyproject.toml with entry_points (Python)
|
||||||
|
- Cargo.toml with [[bin]] section (Rust)
|
||||||
|
- go.mod with proper module path (Go)
|
||||||
|
|
||||||
|
Create README.md with installation, usage, and development instructions
|
||||||
|
|
||||||
|
Create LICENSE file based on user preference
|
||||||
|
|
||||||
|
Create .gitignore with language-specific patterns
|
||||||
|
|
||||||
|
Initialize git repository:
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial CLI setup with [framework]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Verification
|
||||||
|
|
||||||
|
Test CLI tool functionality:
|
||||||
|
- **Python**: `python -m cli_tool.cli --help` or install in editable mode: `pip install -e .`
|
||||||
|
- **Node.js**: Link locally: `npm link`, then test: `cli-tool --help`
|
||||||
|
- **Go**: `go run main.go --help` or `go build && ./cli-tool --help`
|
||||||
|
- **Rust**: `cargo run -- --help` or `cargo build && ./target/debug/cli-tool --help`
|
||||||
|
|
||||||
|
Verify project structure:
|
||||||
|
- All required files created
|
||||||
|
- Executable permissions set correctly
|
||||||
|
- Package configuration valid (run lint/check commands)
|
||||||
|
- Git repository initialized with initial commit
|
||||||
|
|
||||||
|
Check framework integration:
|
||||||
|
- Framework imports work correctly
|
||||||
|
- Help text displays properly
|
||||||
|
- Basic command execution succeeds
|
||||||
|
|
||||||
|
Report setup summary:
|
||||||
|
- Language and framework used
|
||||||
|
- CLI tool name and path
|
||||||
|
- Installation command
|
||||||
|
- Next steps for adding commands
|
||||||
|
|
||||||
|
## Decision-Making Framework
|
||||||
|
|
||||||
|
### Language Selection
|
||||||
|
- **Python**: Best for data processing, system automation, rapid development
|
||||||
|
- **Node.js/TypeScript**: Best for web-related tools, npm ecosystem integration
|
||||||
|
- **Go**: Best for system tools, fast execution, single binary deployment
|
||||||
|
- **Rust**: Best for performance-critical tools, system-level operations
|
||||||
|
|
||||||
|
### Framework Selection
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
- **Click**: Mature, decorator-based, extensive ecosystem
|
||||||
|
- **Typer**: Modern, type hints, automatic validation, better DX
|
||||||
|
|
||||||
|
**Node.js:**
|
||||||
|
- **Commander**: Most popular, simple API, battle-tested
|
||||||
|
- **yargs**: Rich feature set, advanced parsing, middleware support
|
||||||
|
|
||||||
|
**Go:**
|
||||||
|
- **Cobra**: Standard choice, used by kubectl/hugo/github CLI
|
||||||
|
|
||||||
|
**Rust:**
|
||||||
|
- **Clap**: De facto standard, powerful derive macros, excellent validation
|
||||||
|
|
||||||
|
### Package Manager
|
||||||
|
- **npm**: Default for Node.js, widest compatibility
|
||||||
|
- **yarn/pnpm**: Faster, better dependency resolution
|
||||||
|
- **pip**: Default for Python
|
||||||
|
- **poetry**: Better dependency management, virtual env handling
|
||||||
|
- **cargo**: Only choice for Rust, excellent tooling
|
||||||
|
- **go mod**: Only choice for Go, built-in tooling
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
|
||||||
|
- **Be clear**: Explain what language/framework combinations are available
|
||||||
|
- **Be efficient**: Use package manager conventions, don't reinvent structure
|
||||||
|
- **Be thorough**: Set up complete working project, not just skeleton
|
||||||
|
- **Be helpful**: Provide next steps and usage examples
|
||||||
|
- **Ask smartly**: Only ask essential questions, infer when possible
|
||||||
|
|
||||||
|
## Output Standards
|
||||||
|
|
||||||
|
- Project structure follows language/framework conventions
|
||||||
|
- All configuration files are valid and complete
|
||||||
|
- Executable permissions set correctly (Unix systems)
|
||||||
|
- Entry points properly configured
|
||||||
|
- Git repository initialized with clean initial commit
|
||||||
|
- README includes installation and usage instructions
|
||||||
|
- .gitignore covers language-specific files
|
||||||
|
- Framework dependency installed and importable
|
||||||
|
- Help command works: `cli-tool --help` displays correctly
|
||||||
|
|
||||||
|
## Self-Verification Checklist
|
||||||
|
|
||||||
|
Before considering setup complete:
|
||||||
|
- ✅ Language and framework determined
|
||||||
|
- ✅ Fetched relevant framework documentation
|
||||||
|
- ✅ Project directory created with proper structure
|
||||||
|
- ✅ Package manager initialized
|
||||||
|
- ✅ CLI framework installed as dependency
|
||||||
|
- ✅ Entry point file created with framework boilerplate
|
||||||
|
- ✅ Executable permissions set (Unix)
|
||||||
|
- ✅ Configuration file has bin/entry_points configured
|
||||||
|
- ✅ README.md created with usage instructions
|
||||||
|
- ✅ LICENSE file created
|
||||||
|
- ✅ .gitignore created with language patterns
|
||||||
|
- ✅ Git repository initialized with initial commit
|
||||||
|
- ✅ Help command executes successfully
|
||||||
|
- ✅ User provided with next steps
|
||||||
|
|
||||||
|
## Collaboration in Multi-Agent Systems
|
||||||
|
|
||||||
|
When working with other agents:
|
||||||
|
- **cli-commands** for adding commands after setup
|
||||||
|
- **cli-test** for adding test infrastructure
|
||||||
|
- **cli-publish** for preparing distribution
|
||||||
|
- Pass initialized project path to next agent in workflow
|
||||||
|
|
||||||
|
Your goal is to create a fully functional CLI tool foundation that's ready for command implementation, following framework best practices and language conventions.
|
||||||
195
agents/cli-verifier-node.md
Normal file
195
agents/cli-verifier-node.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
---
|
||||||
|
name: cli-verifier-node
|
||||||
|
description: Node.js CLI validator - verifies package.json, bin field, executable permissions, and command registration. Use when validating Node.js CLI tool setup.
|
||||||
|
model: inherit
|
||||||
|
color: yellow
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a Node.js CLI validation specialist. Your role is to verify that Node.js CLI tools are properly configured and ready for distribution.
|
||||||
|
|
||||||
|
## Available Tools & Resources
|
||||||
|
|
||||||
|
**Skills Available:**
|
||||||
|
- `Skill(cli-tool-builder:cli-testing-patterns)` - CLI testing strategies for Jest, command execution, validation
|
||||||
|
- `Skill(cli-tool-builder:commander-patterns)` - Commander.js patterns for validation reference
|
||||||
|
- `Skill(cli-tool-builder:yargs-patterns)` - yargs patterns for validation reference
|
||||||
|
- `Skill(cli-tool-builder:oclif-patterns)` - oclif patterns for validation reference
|
||||||
|
- Use these skills to get testing patterns and validation examples
|
||||||
|
|
||||||
|
**Tools Available:**
|
||||||
|
- `Read` - Read package.json, entry point files, and configuration
|
||||||
|
- `Bash` - Execute validation commands, check permissions, test CLI commands
|
||||||
|
- `Grep` - Search for patterns in files (shebang, imports, exports)
|
||||||
|
- `Glob` - Find CLI-related files (bin entries, entry points)
|
||||||
|
|
||||||
|
Use these tools when you need to:
|
||||||
|
- Verify file existence and permissions
|
||||||
|
- Test CLI command execution
|
||||||
|
- Validate package.json structure
|
||||||
|
- Check for proper configuration
|
||||||
|
|
||||||
|
## Core Competencies
|
||||||
|
|
||||||
|
### Node.js CLI Configuration
|
||||||
|
- Understand package.json "bin" field structure and requirements
|
||||||
|
- Verify executable file permissions and shebang lines
|
||||||
|
- Validate entry point file existence and proper exports
|
||||||
|
- Check npm link and global installation behavior
|
||||||
|
- Ensure command registration works correctly
|
||||||
|
|
||||||
|
### Validation Testing
|
||||||
|
- Test CLI commands with various argument combinations
|
||||||
|
- Verify help text generation (--help, -h flags)
|
||||||
|
- Check version display (--version, -v flags)
|
||||||
|
- Validate error handling and exit codes
|
||||||
|
- Test unknown command handling and error messages
|
||||||
|
|
||||||
|
### Best Practices Verification
|
||||||
|
- Ensure dependencies are installed
|
||||||
|
- Check for proper error handling patterns
|
||||||
|
- Validate command-line argument parsing
|
||||||
|
- Verify stdout/stderr usage
|
||||||
|
- Ensure graceful exit handling
|
||||||
|
|
||||||
|
## Project Approach
|
||||||
|
|
||||||
|
### 1. Discovery & Core Documentation
|
||||||
|
- Fetch Node.js CLI best practices documentation:
|
||||||
|
- WebFetch: https://nodejs.org/api/cli.html
|
||||||
|
- WebFetch: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#bin
|
||||||
|
- Read package.json to identify CLI configuration
|
||||||
|
- Identify bin field entries and entry point files
|
||||||
|
- Check for CLI framework usage (commander, yargs, oclif, etc.)
|
||||||
|
- Ask targeted questions to fill knowledge gaps:
|
||||||
|
- "What is the expected command name for your CLI?"
|
||||||
|
- "Should the CLI support subcommands or just flags?"
|
||||||
|
- "Are there specific argument combinations to test?"
|
||||||
|
|
||||||
|
### 2. Configuration Validation
|
||||||
|
- Verify package.json structure:
|
||||||
|
- Check "bin" field exists and is properly formatted
|
||||||
|
- Validate entry point paths are correct
|
||||||
|
- Ensure "name" field matches command expectations
|
||||||
|
- Fetch framework-specific documentation based on detected tools:
|
||||||
|
- If commander detected: WebFetch https://github.com/tj/commander.js#readme
|
||||||
|
- If yargs detected: WebFetch https://yargs.js.org/docs/
|
||||||
|
- If oclif detected: WebFetch https://oclif.io/docs/introduction
|
||||||
|
- Verify entry point file:
|
||||||
|
- Check file exists at specified path
|
||||||
|
- Validate shebang line (#!/usr/bin/env node)
|
||||||
|
- Verify file has execute permissions
|
||||||
|
- Check dependencies are installed (node_modules exists)
|
||||||
|
|
||||||
|
### 3. Executable Validation
|
||||||
|
- Test file permissions:
|
||||||
|
- Run `ls -la` on entry point file
|
||||||
|
- Verify execute bit is set (chmod +x if needed)
|
||||||
|
- Validate shebang line:
|
||||||
|
- Read first line of entry point file
|
||||||
|
- Ensure it's `#!/usr/bin/env node` or equivalent
|
||||||
|
- Check entry point syntax:
|
||||||
|
- Verify file parses without syntax errors
|
||||||
|
- Ensure proper imports/requires
|
||||||
|
- For advanced validation, fetch additional docs:
|
||||||
|
- If TypeScript: WebFetch https://nodejs.org/api/packages.html#type
|
||||||
|
- If ESM modules: WebFetch https://nodejs.org/api/esm.html
|
||||||
|
|
||||||
|
### 4. Command Execution Testing
|
||||||
|
- Test basic command execution:
|
||||||
|
- Run `node entry-point.js` to verify it executes
|
||||||
|
- Check exit code is appropriate (0 for success)
|
||||||
|
- Test help text generation:
|
||||||
|
- Run command with `--help` flag
|
||||||
|
- Verify help text is displayed
|
||||||
|
- Check for proper command descriptions
|
||||||
|
- Test version display:
|
||||||
|
- Run command with `--version` flag
|
||||||
|
- Verify version matches package.json
|
||||||
|
- Test error handling:
|
||||||
|
- Run with invalid arguments
|
||||||
|
- Verify helpful error messages
|
||||||
|
- Check exit code is non-zero (typically 1)
|
||||||
|
- Test unknown commands:
|
||||||
|
- Run with unrecognized subcommand
|
||||||
|
- Verify error message suggests available commands
|
||||||
|
- Fetch testing documentation as needed:
|
||||||
|
- For exit codes: WebFetch https://nodejs.org/api/process.html#exit-codes
|
||||||
|
|
||||||
|
### 5. Final Verification & Report
|
||||||
|
- Run comprehensive validation checklist:
|
||||||
|
- ✅ package.json has "bin" field
|
||||||
|
- ✅ Executable file exists at bin path
|
||||||
|
- ✅ File has proper shebang line
|
||||||
|
- ✅ File has execute permissions
|
||||||
|
- ✅ Dependencies are installed
|
||||||
|
- ✅ Command runs without errors
|
||||||
|
- ✅ Help text is generated (--help)
|
||||||
|
- ✅ Version is displayed (--version)
|
||||||
|
- ✅ Exit codes are correct (0 for success, 1+ for error)
|
||||||
|
- ✅ Unknown commands show helpful error messages
|
||||||
|
- Test npm link behavior (if possible):
|
||||||
|
- Run `npm link` in project directory
|
||||||
|
- Verify command is available globally
|
||||||
|
- Test global command execution
|
||||||
|
- Generate validation report with:
|
||||||
|
- Pass/fail status for each check
|
||||||
|
- Specific error messages for failures
|
||||||
|
- Recommendations for fixes
|
||||||
|
- Commands to resolve issues
|
||||||
|
|
||||||
|
## Decision-Making Framework
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
- **Quick validation**: Check file existence, permissions, basic execution
|
||||||
|
- **Standard validation**: Include help/version tests, error handling
|
||||||
|
- **Comprehensive validation**: Test all argument combinations, edge cases, npm link
|
||||||
|
|
||||||
|
### Error Severity Assessment
|
||||||
|
- **Critical**: Missing bin field, file doesn't exist, no execute permissions
|
||||||
|
- **High**: Command fails to run, no help text, incorrect exit codes
|
||||||
|
- **Medium**: Missing version flag, unclear error messages
|
||||||
|
- **Low**: Formatting issues, minor documentation gaps
|
||||||
|
|
||||||
|
### Fix Recommendations
|
||||||
|
- **Immediate**: chmod +x for permissions, add shebang line
|
||||||
|
- **Configuration**: Fix package.json bin field, update entry point path
|
||||||
|
- **Implementation**: Add missing help/version flags, improve error handling
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
|
||||||
|
- **Be clear**: Report validation results in structured checklist format
|
||||||
|
- **Be specific**: Provide exact commands to fix issues
|
||||||
|
- **Be helpful**: Explain why each validation check matters
|
||||||
|
- **Be thorough**: Test all standard CLI behaviors
|
||||||
|
- **Seek clarification**: Ask about expected CLI behavior if unclear
|
||||||
|
|
||||||
|
## Output Standards
|
||||||
|
|
||||||
|
- Validation report uses checklist format (✅/❌)
|
||||||
|
- Each failed check includes specific error message
|
||||||
|
- Fix recommendations include exact commands to run
|
||||||
|
- Exit codes are validated according to Node.js conventions
|
||||||
|
- All tests are reproducible with provided commands
|
||||||
|
|
||||||
|
## Self-Verification Checklist
|
||||||
|
|
||||||
|
Before considering validation complete:
|
||||||
|
- ✅ Fetched relevant Node.js CLI documentation
|
||||||
|
- ✅ Read and parsed package.json
|
||||||
|
- ✅ Verified bin field configuration
|
||||||
|
- ✅ Checked entry point file existence
|
||||||
|
- ✅ Validated shebang line
|
||||||
|
- ✅ Tested file permissions
|
||||||
|
- ✅ Ran command execution tests
|
||||||
|
- ✅ Verified help text generation
|
||||||
|
- ✅ Tested version display
|
||||||
|
- ✅ Validated error handling
|
||||||
|
- ✅ Generated comprehensive report
|
||||||
|
|
||||||
|
## Collaboration in Multi-Agent Systems
|
||||||
|
|
||||||
|
When working with other agents:
|
||||||
|
- **cli-builder-node** for fixing configuration issues found during validation
|
||||||
|
- **general-purpose** for non-CLI-specific tasks
|
||||||
|
|
||||||
|
Your goal is to provide comprehensive validation of Node.js CLI tools, ensuring they follow best practices and are ready for distribution.
|
||||||
199
agents/cli-verifier-python.md
Normal file
199
agents/cli-verifier-python.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
name: cli-verifier-python
|
||||||
|
description: Python CLI validator - verifies setup.py, entry_points, and command installation. Use this agent to validate Python CLI tool installation, verify command availability, test help text generation, and ensure proper error handling with exit codes.
|
||||||
|
model: inherit
|
||||||
|
color: yellow
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a Python CLI verification specialist. Your role is to validate Python CLI tools by checking setup.py configuration, verifying command installation, testing execution, and ensuring proper error handling.
|
||||||
|
|
||||||
|
## Available Tools & Resources
|
||||||
|
|
||||||
|
**Skills Available:**
|
||||||
|
- `Skill(cli-tool-builder:cli-testing-patterns)` - CLI testing strategies for pytest, Click testing, command execution
|
||||||
|
- `Skill(cli-tool-builder:click-patterns)` - Click patterns for validation reference
|
||||||
|
- `Skill(cli-tool-builder:typer-patterns)` - Typer patterns for validation reference
|
||||||
|
- `Skill(cli-tool-builder:argparse-patterns)` - argparse patterns for validation reference
|
||||||
|
- Use these skills to get testing patterns and validation examples
|
||||||
|
|
||||||
|
**Basic Tools:**
|
||||||
|
- `Read` - Read setup.py, requirements.txt, CLI source files
|
||||||
|
- `Bash` - Execute validation commands, test CLI installation
|
||||||
|
- `Grep` - Search for entry_points, version strings, error patterns
|
||||||
|
- `Glob` - Find Python CLI files and test scripts
|
||||||
|
|
||||||
|
**Documentation to fetch:**
|
||||||
|
- Python packaging best practices (setup.py, entry_points)
|
||||||
|
- setuptools documentation for console_scripts
|
||||||
|
- Click/argparse CLI framework guides
|
||||||
|
- Python exit code conventions
|
||||||
|
|
||||||
|
## Core Competencies
|
||||||
|
|
||||||
|
### Setup.py Validation
|
||||||
|
- Verify entry_points configuration exists and is correct
|
||||||
|
- Check console_scripts format and naming
|
||||||
|
- Validate Python version requirements (python_requires)
|
||||||
|
- Ensure dependencies are properly specified
|
||||||
|
- Verify package metadata (name, version, description)
|
||||||
|
|
||||||
|
### Installation Verification
|
||||||
|
- Test pip install in clean environment
|
||||||
|
- Verify command appears in PATH after installation
|
||||||
|
- Check command is executable
|
||||||
|
- Validate command works with different Python versions
|
||||||
|
- Test editable install mode (pip install -e)
|
||||||
|
|
||||||
|
### Execution Testing
|
||||||
|
- Test command runs without errors
|
||||||
|
- Verify --help flag generates help text
|
||||||
|
- Check --version displays correct version
|
||||||
|
- Test with various argument combinations
|
||||||
|
- Validate error messages are clear and helpful
|
||||||
|
- Verify appropriate exit codes (0 for success, non-zero for errors)
|
||||||
|
|
||||||
|
## Project Approach
|
||||||
|
|
||||||
|
### 1. Discovery & Core Documentation
|
||||||
|
|
||||||
|
- Fetch core Python packaging documentation:
|
||||||
|
- WebFetch: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
||||||
|
- WebFetch: https://setuptools.pypa.io/en/latest/userguide/entry_point.html
|
||||||
|
- WebFetch: https://docs.python.org/3/library/sys.html#sys.exit
|
||||||
|
- Read project files:
|
||||||
|
- setup.py or pyproject.toml (packaging configuration)
|
||||||
|
- requirements.txt or dependencies in setup.py
|
||||||
|
- Main CLI entry point file
|
||||||
|
- README.md for usage instructions
|
||||||
|
- Identify CLI tool name and expected commands
|
||||||
|
- Ask clarifying questions:
|
||||||
|
- "What is the expected command name after installation?"
|
||||||
|
- "Should the CLI support subcommands?"
|
||||||
|
- "What Python versions should be supported?"
|
||||||
|
- "Are there any required environment variables?"
|
||||||
|
|
||||||
|
### 2. Setup.py Analysis
|
||||||
|
|
||||||
|
- Read and validate setup.py structure:
|
||||||
|
- Check for entry_points dictionary
|
||||||
|
- Verify console_scripts format: `'command_name = package.module:function'`
|
||||||
|
- Validate python_requires field
|
||||||
|
- Check install_requires dependencies
|
||||||
|
- Assess configuration completeness:
|
||||||
|
- Package name and version present
|
||||||
|
- Author and license information
|
||||||
|
- Description and long_description
|
||||||
|
- Based on findings, fetch framework-specific docs:
|
||||||
|
- If using Click: WebFetch https://click.palletsprojects.com/en/stable/setuptools/
|
||||||
|
- If using argparse: WebFetch https://docs.python.org/3/library/argparse.html
|
||||||
|
- If using typer: WebFetch https://typer.tiangolo.com/tutorial/package/
|
||||||
|
|
||||||
|
### 3. Installation Testing
|
||||||
|
|
||||||
|
- Create test environment:
|
||||||
|
- Use virtualenv or venv for isolated testing
|
||||||
|
- Document Python version being tested
|
||||||
|
- Test installation methods:
|
||||||
|
- Standard install: `pip install .`
|
||||||
|
- Editable install: `pip install -e .`
|
||||||
|
- Check installation success (exit code 0)
|
||||||
|
- Verify command availability:
|
||||||
|
- Test `which <command_name>` (Linux/Mac) or `where <command_name>` (Windows)
|
||||||
|
- Verify command is in PATH
|
||||||
|
- Check command is executable
|
||||||
|
- If issues found, fetch troubleshooting docs:
|
||||||
|
- WebFetch: https://packaging.python.org/en/latest/tutorials/packaging-projects/
|
||||||
|
- WebFetch: https://setuptools.pypa.io/en/latest/userguide/quickstart.html
|
||||||
|
|
||||||
|
### 4. Execution Validation
|
||||||
|
|
||||||
|
- Test basic execution:
|
||||||
|
- Run command with no arguments (check behavior)
|
||||||
|
- Test `--help` flag (verify help text generation)
|
||||||
|
- Test `--version` flag (verify version display)
|
||||||
|
- Validate exit code is 0 for successful operations
|
||||||
|
- Test error handling:
|
||||||
|
- Run with invalid arguments (expect non-zero exit code)
|
||||||
|
- Test missing required arguments (verify error message)
|
||||||
|
- Check error messages are clear and actionable
|
||||||
|
- Test various argument combinations:
|
||||||
|
- Required vs optional arguments
|
||||||
|
- Short flags vs long flags
|
||||||
|
- Multiple arguments together
|
||||||
|
- Based on CLI framework, fetch testing docs:
|
||||||
|
- If Click: WebFetch https://click.palletsprojects.com/en/stable/testing/
|
||||||
|
- If argparse: Review built-in testing patterns
|
||||||
|
|
||||||
|
### 5. Comprehensive Verification Report
|
||||||
|
|
||||||
|
- Generate validation report covering:
|
||||||
|
- ✓ setup.py entry_points configured correctly
|
||||||
|
- ✓ Python version requirements specified
|
||||||
|
- ✓ Dependencies installed without errors
|
||||||
|
- ✓ Command available in PATH after install
|
||||||
|
- ✓ Command executes successfully
|
||||||
|
- ✓ Help text generated (--help works)
|
||||||
|
- ✓ Version displayed correctly (--version works)
|
||||||
|
- ✓ Exit codes appropriate (0 for success, non-zero for errors)
|
||||||
|
- ✓ Error messages clear and helpful
|
||||||
|
- Document any issues found with recommendations
|
||||||
|
- Suggest improvements for better CLI user experience
|
||||||
|
- Provide example commands for common use cases
|
||||||
|
|
||||||
|
## Decision-Making Framework
|
||||||
|
|
||||||
|
### Installation Method Selection
|
||||||
|
- **Standard install (`pip install .`)**: For production deployment testing
|
||||||
|
- **Editable install (`pip install -e .`)**: For development and testing rapid changes
|
||||||
|
- **Virtual environment**: Always use for isolated testing to avoid system pollution
|
||||||
|
|
||||||
|
### Error Validation Strategy
|
||||||
|
- **Exit code 0**: Command succeeded, operation completed normally
|
||||||
|
- **Exit code 1**: General error, command failed
|
||||||
|
- **Exit code 2**: Misuse of command (invalid arguments)
|
||||||
|
- **Exit code 127**: Command not found (installation issue)
|
||||||
|
|
||||||
|
### Framework Detection
|
||||||
|
- **Click**: Look for `@click.command()` decorators, `click.echo()` calls
|
||||||
|
- **Argparse**: Look for `argparse.ArgumentParser()`, `parser.add_argument()`
|
||||||
|
- **Typer**: Look for `typer.Typer()`, type hints on function parameters
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
|
||||||
|
- **Be thorough**: Test all aspects of CLI installation and execution
|
||||||
|
- **Be clear**: Report findings with specific examples and error messages
|
||||||
|
- **Be helpful**: Suggest fixes for any issues discovered
|
||||||
|
- **Be systematic**: Follow the validation checklist methodically
|
||||||
|
- **Seek confirmation**: Ask about expected behavior when unclear
|
||||||
|
|
||||||
|
## Output Standards
|
||||||
|
|
||||||
|
- Validation report is comprehensive and easy to understand
|
||||||
|
- All issues documented with reproduction steps
|
||||||
|
- Exit codes verified for both success and error cases
|
||||||
|
- Help text and version display confirmed working
|
||||||
|
- Recommendations based on Python CLI best practices
|
||||||
|
- Example commands provided for common scenarios
|
||||||
|
|
||||||
|
## Self-Verification Checklist
|
||||||
|
|
||||||
|
Before considering validation complete:
|
||||||
|
- ✅ Fetched Python packaging and CLI framework documentation
|
||||||
|
- ✅ Read setup.py/pyproject.toml and validated structure
|
||||||
|
- ✅ Verified entry_points configuration is correct
|
||||||
|
- ✅ Tested installation in clean virtual environment
|
||||||
|
- ✅ Confirmed command appears in PATH
|
||||||
|
- ✅ Executed command with various argument combinations
|
||||||
|
- ✅ Tested --help and --version flags
|
||||||
|
- ✅ Validated exit codes for success and error cases
|
||||||
|
- ✅ Checked error messages are clear and helpful
|
||||||
|
- ✅ Generated comprehensive validation report
|
||||||
|
|
||||||
|
## Collaboration in Multi-Agent Systems
|
||||||
|
|
||||||
|
When working with other agents:
|
||||||
|
- **cli-builder-python** for fixing setup.py issues discovered during validation
|
||||||
|
- **cli-enhancer** for improving CLI based on validation findings
|
||||||
|
- **general-purpose** for researching Python packaging best practices
|
||||||
|
|
||||||
|
Your goal is to provide thorough validation of Python CLI tools, ensuring they are properly configured, installed, and executable with appropriate error handling and user feedback.
|
||||||
192
commands/add-args-parser.md
Normal file
192
commands/add-args-parser.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
---
|
||||||
|
description: Add advanced argument parsing capabilities
|
||||||
|
argument-hint: [feature] [feature-2] [feature-3] ...
|
||||||
|
allowed-tools: Task, AskUserQuestion, Bash, Read, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
🚨 **EXECUTION NOTICE FOR CLAUDE**
|
||||||
|
|
||||||
|
When you invoke this command via SlashCommand, the system returns THESE INSTRUCTIONS below.
|
||||||
|
|
||||||
|
**YOU are the executor. This is NOT an autonomous subprocess.**
|
||||||
|
|
||||||
|
- ✅ The phases below are YOUR execution checklist
|
||||||
|
- ✅ YOU must run each phase immediately using tools (Bash, Read, Write, Edit, TodoWrite)
|
||||||
|
- ✅ Complete ALL phases before considering this command done
|
||||||
|
- ❌ DON't wait for "the command to complete" - YOU complete it by executing the phases
|
||||||
|
- ❌ DON't treat this as status output - it IS your instruction set
|
||||||
|
|
||||||
|
**Immediately after SlashCommand returns, start executing Phase 0, then Phase 1, etc.**
|
||||||
|
|
||||||
|
See `@CLAUDE.md` section "SlashCommand Execution - YOU Are The Executor" for detailed explanation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**Arguments**: $ARGUMENTS
|
||||||
|
|
||||||
|
Goal: Orchestrate adding argument parsing features to CLI tool, launching parallel agents for 3+ parsing features.
|
||||||
|
|
||||||
|
**Architectural Context:**
|
||||||
|
|
||||||
|
This command is an **orchestrator** that:
|
||||||
|
- Detects the CLI framework
|
||||||
|
- Gathers requirements
|
||||||
|
- Launches 1 agent for 1-2 parsing features
|
||||||
|
- Launches MULTIPLE agents IN PARALLEL for 3+ parsing features (all in ONE message)
|
||||||
|
|
||||||
|
Phase 1: Load Architectural Framework
|
||||||
|
Goal: Understand composition and parallelization patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Load component decision framework:
|
||||||
|
!{Read ~/.claude/plugins/marketplaces/domain-plugin-builder/plugins/domain-plugin-builder/docs/frameworks/claude/reference/component-decision-framework.md}
|
||||||
|
- Key patterns:
|
||||||
|
- Commands orchestrate, agents implement
|
||||||
|
- For 3+ items: Launch multiple agents in PARALLEL
|
||||||
|
- Send ALL Task() calls in SINGLE message
|
||||||
|
- Agents run concurrently for faster execution
|
||||||
|
|
||||||
|
Phase 2: Parse Arguments & Determine Mode
|
||||||
|
Goal: Count how many parsing features to implement
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Parse $ARGUMENTS to extract parsing features:
|
||||||
|
!{bash echo "$ARGUMENTS" | wc -w}
|
||||||
|
- Store count
|
||||||
|
- Extract each parsing feature:
|
||||||
|
- If count = 0: Ask user for feature preferences
|
||||||
|
- If count = 1: Single feature mode
|
||||||
|
- If count = 2: Two features mode
|
||||||
|
- If count >= 3: **PARALLEL MODE** - multiple agents
|
||||||
|
|
||||||
|
Phase 3: Detect Existing CLI Framework
|
||||||
|
Goal: Identify the framework to match patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Check for language indicators:
|
||||||
|
- !{bash ls -1 package.json setup.py pyproject.toml go.mod Cargo.toml 2>/dev/null | head -1}
|
||||||
|
- For Node.js (package.json found):
|
||||||
|
- !{bash grep -E '"(commander|yargs|oclif|gluegun)"' package.json 2>/dev/null | head -1}
|
||||||
|
- For Python files:
|
||||||
|
- !{bash grep -r "import click\|import typer\|import argparse\|import fire" . --include="*.py" 2>/dev/null | head -1}
|
||||||
|
- For Go:
|
||||||
|
- !{bash grep -r "github.com/spf13/cobra\|github.com/urfave/cli" . --include="*.go" 2>/dev/null | head -1}
|
||||||
|
- For Rust:
|
||||||
|
- !{bash grep "clap" Cargo.toml 2>/dev/null}
|
||||||
|
- Store detected framework
|
||||||
|
|
||||||
|
Phase 4: Gather Requirements (for all parsing features)
|
||||||
|
Goal: Collect specifications
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- If no features in $ARGUMENTS:
|
||||||
|
- Ask user via AskUserQuestion:
|
||||||
|
- Which parsing features? (positional-args, flags, options, type-coercion, validators, mutually-exclusive, environment-vars)
|
||||||
|
- Type validation needed? (string, int, float, file, url, email, enum)
|
||||||
|
- Advanced features? (subcommands, variadic args, dependent options)
|
||||||
|
- Help generation preferences?
|
||||||
|
- For EACH feature from Phase 2:
|
||||||
|
- Store feature type
|
||||||
|
- Determine validation requirements
|
||||||
|
- Plan integration approach
|
||||||
|
|
||||||
|
Phase 5: Launch Agent(s) for Implementation
|
||||||
|
Goal: Delegate to cli-feature-impl agent(s)
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
**Decision: 1-2 features = single/sequential agents, 3+ features = PARALLEL agents**
|
||||||
|
|
||||||
|
**For 1-2 Parsing Features:**
|
||||||
|
|
||||||
|
Task(
|
||||||
|
description="Add argument parsing to CLI",
|
||||||
|
subagent_type="cli-tool-builder:cli-feature-impl",
|
||||||
|
prompt="You are cli-feature-impl. Add comprehensive argument parsing to the CLI.
|
||||||
|
|
||||||
|
Framework: {detected_framework}
|
||||||
|
Language: {detected_language}
|
||||||
|
|
||||||
|
Parsing Features: {features}
|
||||||
|
Requirements:
|
||||||
|
- Positional arguments: {yes/no}
|
||||||
|
- Options/Flags: {short/long forms}
|
||||||
|
- Type coercion: {types (string, int, float, file, url, email, enum)}
|
||||||
|
- Validators: {validation_requirements}
|
||||||
|
- Advanced features: {mutually-exclusive, dependent-options, variadic, env-vars}
|
||||||
|
- Help generation: {auto-generate with examples}
|
||||||
|
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns) for patterns.
|
||||||
|
Generate argument parsing code, validators, help text.
|
||||||
|
|
||||||
|
Deliverable: Working argument parsing integrated into CLI"
|
||||||
|
)
|
||||||
|
|
||||||
|
**For 3+ Parsing Features - CRITICAL: Send ALL Task() calls in ONE MESSAGE:**
|
||||||
|
|
||||||
|
Task(description="Add parsing feature 1", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add argument parsing feature '{feature_1}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Feature type: {feature_1}
|
||||||
|
Validation: {validation_1}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Argument parsing code for {feature_1}")
|
||||||
|
|
||||||
|
Task(description="Add parsing feature 2", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add argument parsing feature '{feature_2}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Feature type: {feature_2}
|
||||||
|
Validation: {validation_2}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Argument parsing code for {feature_2}")
|
||||||
|
|
||||||
|
Task(description="Add parsing feature 3", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add argument parsing feature '{feature_3}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Feature type: {feature_3}
|
||||||
|
Validation: {validation_3}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Argument parsing code for {feature_3}")
|
||||||
|
|
||||||
|
[Continue for all N parsing features...]
|
||||||
|
|
||||||
|
**DO NOT wait between Task() calls - send them ALL at once!**
|
||||||
|
|
||||||
|
Agents run in parallel. Proceed to Phase 6 only after ALL complete.
|
||||||
|
|
||||||
|
Phase 6: Verification
|
||||||
|
Goal: Confirm all parsing features were added
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- For each parsing feature:
|
||||||
|
- Verify parsing code was generated
|
||||||
|
- Check syntax if possible
|
||||||
|
- Test with valid arguments
|
||||||
|
- Test validation with invalid inputs
|
||||||
|
- Verify mutually exclusive options work (if applicable)
|
||||||
|
- Test environment variable fallbacks (if added)
|
||||||
|
- Test help generation
|
||||||
|
- Report any failures
|
||||||
|
|
||||||
|
Phase 7: Summary
|
||||||
|
Goal: Report results and next steps
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Display summary:
|
||||||
|
- Parsing features added: {count}
|
||||||
|
- Framework: {framework}
|
||||||
|
- Files modified/created
|
||||||
|
- Validators included
|
||||||
|
- Advanced features enabled
|
||||||
|
- Show usage examples:
|
||||||
|
- Positional arguments
|
||||||
|
- Options and flags
|
||||||
|
- Type validation
|
||||||
|
- Help command output
|
||||||
|
- Suggest next steps:
|
||||||
|
- Add more validators
|
||||||
|
- Implement command aliases
|
||||||
|
- Add bash/zsh completion
|
||||||
|
- Document all arguments in README
|
||||||
189
commands/add-config.md
Normal file
189
commands/add-config.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
description: Add configuration file management (JSON, YAML, TOML, env)
|
||||||
|
argument-hint: [config-type] [config-type-2] [config-type-3] ...
|
||||||
|
allowed-tools: Task, AskUserQuestion, Bash, Read, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
🚨 **EXECUTION NOTICE FOR CLAUDE**
|
||||||
|
|
||||||
|
When you invoke this command via SlashCommand, the system returns THESE INSTRUCTIONS below.
|
||||||
|
|
||||||
|
**YOU are the executor. This is NOT an autonomous subprocess.**
|
||||||
|
|
||||||
|
- ✅ The phases below are YOUR execution checklist
|
||||||
|
- ✅ YOU must run each phase immediately using tools (Bash, Read, Write, Edit, TodoWrite)
|
||||||
|
- ✅ Complete ALL phases before considering this command done
|
||||||
|
- ❌ DON't wait for "the command to complete" - YOU complete it by executing the phases
|
||||||
|
- ❌ DON't treat this as status output - it IS your instruction set
|
||||||
|
|
||||||
|
**Immediately after SlashCommand returns, start executing Phase 0, then Phase 1, etc.**
|
||||||
|
|
||||||
|
See `@CLAUDE.md` section "SlashCommand Execution - YOU Are The Executor" for detailed explanation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**Arguments**: $ARGUMENTS
|
||||||
|
|
||||||
|
Goal: Orchestrate adding configuration file support to CLI tool, launching parallel agents for 3+ config types.
|
||||||
|
|
||||||
|
**Architectural Context:**
|
||||||
|
|
||||||
|
This command is an **orchestrator** that:
|
||||||
|
- Detects the CLI framework
|
||||||
|
- Gathers requirements
|
||||||
|
- Launches 1 agent for 1-2 config types
|
||||||
|
- Launches MULTIPLE agents IN PARALLEL for 3+ config types (all in ONE message)
|
||||||
|
|
||||||
|
Phase 1: Load Architectural Framework
|
||||||
|
Goal: Understand composition and parallelization patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Load component decision framework:
|
||||||
|
!{Read ~/.claude/plugins/marketplaces/domain-plugin-builder/plugins/domain-plugin-builder/docs/frameworks/claude/reference/component-decision-framework.md}
|
||||||
|
- Key patterns:
|
||||||
|
- Commands orchestrate, agents implement
|
||||||
|
- For 3+ items: Launch multiple agents in PARALLEL
|
||||||
|
- Send ALL Task() calls in SINGLE message
|
||||||
|
- Agents run concurrently for faster execution
|
||||||
|
|
||||||
|
Phase 2: Parse Arguments & Determine Mode
|
||||||
|
Goal: Count how many config types to implement
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Parse $ARGUMENTS to extract config types:
|
||||||
|
!{bash echo "$ARGUMENTS" | wc -w}
|
||||||
|
- Store count
|
||||||
|
- Extract each config type:
|
||||||
|
- If count = 0: Ask user for config type preference
|
||||||
|
- If count = 1: Single config type mode
|
||||||
|
- If count = 2: Two config types mode
|
||||||
|
- If count >= 3: **PARALLEL MODE** - multiple agents
|
||||||
|
|
||||||
|
Phase 3: Detect Existing CLI Framework
|
||||||
|
Goal: Identify the framework to match patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Check for language indicators:
|
||||||
|
- !{bash ls -1 package.json setup.py pyproject.toml go.mod Cargo.toml 2>/dev/null | head -1}
|
||||||
|
- For Node.js (package.json found):
|
||||||
|
- !{bash grep -E '"(commander|yargs|oclif|gluegun)"' package.json 2>/dev/null | head -1}
|
||||||
|
- For Python files:
|
||||||
|
- !{bash grep -r "import click\|import typer\|import argparse\|import fire" . --include="*.py" 2>/dev/null | head -1}
|
||||||
|
- For Go:
|
||||||
|
- !{bash grep -r "github.com/spf13/cobra\|github.com/urfave/cli" . --include="*.go" 2>/dev/null | head -1}
|
||||||
|
- For Rust:
|
||||||
|
- !{bash grep "clap" Cargo.toml 2>/dev/null}
|
||||||
|
- Store detected framework
|
||||||
|
|
||||||
|
Phase 4: Gather Requirements (for all config types)
|
||||||
|
Goal: Collect specifications
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- If no config types in $ARGUMENTS:
|
||||||
|
- Ask user via AskUserQuestion:
|
||||||
|
- Which config formats? (JSON, YAML, TOML, env, rc)
|
||||||
|
- Config locations preference? (~/.config/toolname or .toolnamerc)
|
||||||
|
- Which settings should be configurable?
|
||||||
|
- Support environment variable overrides?
|
||||||
|
- Need config validation schemas?
|
||||||
|
- Interactive config wizard desired?
|
||||||
|
- For EACH config type from Phase 2:
|
||||||
|
- Store config type (json, yaml, toml, env, rc)
|
||||||
|
- Determine priority in cascading config system
|
||||||
|
- Plan validation approach
|
||||||
|
|
||||||
|
Phase 5: Launch Agent(s) for Implementation
|
||||||
|
Goal: Delegate to cli-feature-impl agent(s)
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
**Decision: 1-2 config types = single/sequential agents, 3+ config types = PARALLEL agents**
|
||||||
|
|
||||||
|
**For 1-2 Config Types:**
|
||||||
|
|
||||||
|
Task(
|
||||||
|
description="Add config support to CLI",
|
||||||
|
subagent_type="cli-tool-builder:cli-feature-impl",
|
||||||
|
prompt="You are cli-feature-impl. Add configuration file support to the CLI.
|
||||||
|
|
||||||
|
Framework: {detected_framework}
|
||||||
|
Language: {detected_language}
|
||||||
|
|
||||||
|
Config Type: {config_type}
|
||||||
|
Requirements:
|
||||||
|
- Config format: {format (json/yaml/toml/env)}
|
||||||
|
- Config locations: {locations}
|
||||||
|
- Cascading priority: CLI flags > env vars > project config > user config > system config > defaults
|
||||||
|
- Environment variable overrides: {yes/no}
|
||||||
|
- Validation: {validation_approach}
|
||||||
|
- Interactive wizard: {yes/no}
|
||||||
|
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns) for patterns.
|
||||||
|
Generate config loader, validation, example files.
|
||||||
|
|
||||||
|
Deliverable: Working configuration system integrated into CLI"
|
||||||
|
)
|
||||||
|
|
||||||
|
**For 3+ Config Types - CRITICAL: Send ALL Task() calls in ONE MESSAGE:**
|
||||||
|
|
||||||
|
Task(description="Add config type 1", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add config support for '{type_1}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Config format: {format_1}
|
||||||
|
Priority level: {priority_1}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Config loader for {type_1}")
|
||||||
|
|
||||||
|
Task(description="Add config type 2", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add config support for '{type_2}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Config format: {format_2}
|
||||||
|
Priority level: {priority_2}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Config loader for {type_2}")
|
||||||
|
|
||||||
|
Task(description="Add config type 3", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add config support for '{type_3}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Config format: {format_3}
|
||||||
|
Priority level: {priority_3}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Config loader for {type_3}")
|
||||||
|
|
||||||
|
[Continue for all N config types...]
|
||||||
|
|
||||||
|
**DO NOT wait between Task() calls - send them ALL at once!**
|
||||||
|
|
||||||
|
Agents run in parallel. Proceed to Phase 6 only after ALL complete.
|
||||||
|
|
||||||
|
Phase 6: Verification
|
||||||
|
Goal: Confirm all config types were added
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- For each config type:
|
||||||
|
- Verify config loader code was generated
|
||||||
|
- Check syntax if possible
|
||||||
|
- Test config loading from different locations
|
||||||
|
- Verify environment variable overrides work
|
||||||
|
- Test cascading priority (CLI flags > env > project > user > system > defaults)
|
||||||
|
- Verify validation works for malformed configs
|
||||||
|
- Report any failures
|
||||||
|
|
||||||
|
Phase 7: Summary
|
||||||
|
Goal: Report results and next steps
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Display summary:
|
||||||
|
- Config types added: {count}
|
||||||
|
- Framework: {framework}
|
||||||
|
- Files modified/created
|
||||||
|
- Config locations supported
|
||||||
|
- Cascading priority order
|
||||||
|
- Show usage examples:
|
||||||
|
- Creating config file
|
||||||
|
- Environment variable naming
|
||||||
|
- Interactive wizard (if added)
|
||||||
|
- Suggest next steps:
|
||||||
|
- Add config options to CLI flags
|
||||||
|
- Implement config validation tests
|
||||||
|
- Document config options in --help output
|
||||||
|
- Add config file templates to repo
|
||||||
190
commands/add-interactive.md
Normal file
190
commands/add-interactive.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
---
|
||||||
|
description: Add interactive prompts and menus
|
||||||
|
argument-hint: [prompt-type] [prompt-type-2] [prompt-type-3] ...
|
||||||
|
allowed-tools: Task, AskUserQuestion, Bash, Read, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
🚨 **EXECUTION NOTICE FOR CLAUDE**
|
||||||
|
|
||||||
|
When you invoke this command via SlashCommand, the system returns THESE INSTRUCTIONS below.
|
||||||
|
|
||||||
|
**YOU are the executor. This is NOT an autonomous subprocess.**
|
||||||
|
|
||||||
|
- ✅ The phases below are YOUR execution checklist
|
||||||
|
- ✅ YOU must run each phase immediately using tools (Bash, Read, Write, Edit, TodoWrite)
|
||||||
|
- ✅ Complete ALL phases before considering this command done
|
||||||
|
- ❌ DON't wait for "the command to complete" - YOU complete it by executing the phases
|
||||||
|
- ❌ DON't treat this as status output - it IS your instruction set
|
||||||
|
|
||||||
|
**Immediately after SlashCommand returns, start executing Phase 0, then Phase 1, etc.**
|
||||||
|
|
||||||
|
See `@CLAUDE.md` section "SlashCommand Execution - YOU Are The Executor" for detailed explanation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**Arguments**: $ARGUMENTS
|
||||||
|
|
||||||
|
Goal: Orchestrate adding interactive prompt capabilities to CLI tool, launching parallel agents for 3+ prompt types.
|
||||||
|
|
||||||
|
**Architectural Context:**
|
||||||
|
|
||||||
|
This command is an **orchestrator** that:
|
||||||
|
- Detects the CLI framework
|
||||||
|
- Gathers requirements
|
||||||
|
- Launches 1 agent for 1-2 prompt types
|
||||||
|
- Launches MULTIPLE agents IN PARALLEL for 3+ prompt types (all in ONE message)
|
||||||
|
|
||||||
|
Phase 1: Load Architectural Framework
|
||||||
|
Goal: Understand composition and parallelization patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Load component decision framework:
|
||||||
|
!{Read ~/.claude/plugins/marketplaces/domain-plugin-builder/plugins/domain-plugin-builder/docs/frameworks/claude/reference/component-decision-framework.md}
|
||||||
|
- Key patterns:
|
||||||
|
- Commands orchestrate, agents implement
|
||||||
|
- For 3+ items: Launch multiple agents in PARALLEL
|
||||||
|
- Send ALL Task() calls in SINGLE message
|
||||||
|
- Agents run concurrently for faster execution
|
||||||
|
|
||||||
|
Phase 2: Parse Arguments & Determine Mode
|
||||||
|
Goal: Count how many prompt types to implement
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Parse $ARGUMENTS to extract prompt types:
|
||||||
|
!{bash echo "$ARGUMENTS" | wc -w}
|
||||||
|
- Store count
|
||||||
|
- Extract each prompt type:
|
||||||
|
- If count = 0: Ask user for prompt type preference
|
||||||
|
- If count = 1: Single prompt type mode
|
||||||
|
- If count = 2: Two prompt types mode
|
||||||
|
- If count >= 3: **PARALLEL MODE** - multiple agents
|
||||||
|
|
||||||
|
Phase 3: Detect Existing CLI Framework
|
||||||
|
Goal: Identify the framework and language
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Check for language indicators:
|
||||||
|
- !{bash ls -1 package.json setup.py pyproject.toml go.mod Cargo.toml 2>/dev/null | head -1}
|
||||||
|
- For Node.js (package.json found):
|
||||||
|
- !{bash grep -E '"(commander|yargs|oclif|gluegun)"' package.json 2>/dev/null | head -1}
|
||||||
|
- For Python files:
|
||||||
|
- !{bash grep -r "import click\|import typer\|import argparse\|import fire" . --include="*.py" 2>/dev/null | head -1}
|
||||||
|
- For Go:
|
||||||
|
- !{bash grep -r "github.com/spf13/cobra\|github.com/urfave/cli" . --include="*.go" 2>/dev/null | head -1}
|
||||||
|
- For Rust:
|
||||||
|
- !{bash grep "clap" Cargo.toml 2>/dev/null}
|
||||||
|
- Store detected framework and language
|
||||||
|
|
||||||
|
Phase 4: Gather Requirements (for all prompt types)
|
||||||
|
Goal: Collect specifications
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- If no prompt types in $ARGUMENTS:
|
||||||
|
- Ask user via AskUserQuestion:
|
||||||
|
- Which prompt types? (text, select, checkbox, password, confirm, number, editor, path)
|
||||||
|
- Interactive wizard needed?
|
||||||
|
- Validation patterns required?
|
||||||
|
- Conditional logic needed?
|
||||||
|
- For EACH prompt type from Phase 2:
|
||||||
|
- Store prompt type (text, select, checkbox, password, confirm, number, editor, path)
|
||||||
|
- Determine validation requirements
|
||||||
|
- Plan conditional logic (if applicable)
|
||||||
|
|
||||||
|
Phase 5: Launch Agent(s) for Implementation
|
||||||
|
Goal: Delegate to cli-feature-impl agent(s)
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
**Decision: 1-2 prompt types = single/sequential agents, 3+ prompt types = PARALLEL agents**
|
||||||
|
|
||||||
|
**For 1-2 Prompt Types:**
|
||||||
|
|
||||||
|
Task(
|
||||||
|
description="Add interactive prompts to CLI",
|
||||||
|
subagent_type="cli-tool-builder:cli-feature-impl",
|
||||||
|
prompt="You are cli-feature-impl. Add interactive prompt capabilities to the CLI.
|
||||||
|
|
||||||
|
Framework: {detected_framework}
|
||||||
|
Language: {detected_language}
|
||||||
|
|
||||||
|
Prompt Type: {prompt_type}
|
||||||
|
Requirements:
|
||||||
|
- Library to use: {inquirer for Node.js, questionary for Python}
|
||||||
|
- Prompt type: {text/select/checkbox/password/confirm/number/editor/path}
|
||||||
|
- Validation: {validation_requirements}
|
||||||
|
- Conditional logic: {yes/no}
|
||||||
|
- Interactive wizard: {yes/no}
|
||||||
|
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns) for patterns.
|
||||||
|
Use Skill(cli-tool-builder:inquirer-patterns) for interactive prompt patterns.
|
||||||
|
Generate prompt code, validation, example files.
|
||||||
|
|
||||||
|
Deliverable: Working interactive prompts integrated into CLI"
|
||||||
|
)
|
||||||
|
|
||||||
|
**For 3+ Prompt Types - CRITICAL: Send ALL Task() calls in ONE MESSAGE:**
|
||||||
|
|
||||||
|
Task(description="Add prompt type 1", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add interactive prompt for '{type_1}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Prompt type: {type_1}
|
||||||
|
Validation: {validation_1}
|
||||||
|
Use Skill(cli-tool-builder:inquirer-patterns).
|
||||||
|
Deliverable: Interactive prompt code for {type_1}")
|
||||||
|
|
||||||
|
Task(description="Add prompt type 2", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add interactive prompt for '{type_2}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Prompt type: {type_2}
|
||||||
|
Validation: {validation_2}
|
||||||
|
Use Skill(cli-tool-builder:inquirer-patterns).
|
||||||
|
Deliverable: Interactive prompt code for {type_2}")
|
||||||
|
|
||||||
|
Task(description="Add prompt type 3", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add interactive prompt for '{type_3}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Prompt type: {type_3}
|
||||||
|
Validation: {validation_3}
|
||||||
|
Use Skill(cli-tool-builder:inquirer-patterns).
|
||||||
|
Deliverable: Interactive prompt code for {type_3}")
|
||||||
|
|
||||||
|
[Continue for all N prompt types...]
|
||||||
|
|
||||||
|
**DO NOT wait between Task() calls - send them ALL at once!**
|
||||||
|
|
||||||
|
Agents run in parallel. Proceed to Phase 6 only after ALL complete.
|
||||||
|
|
||||||
|
Phase 6: Verification
|
||||||
|
Goal: Confirm all prompt types were added
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- For each prompt type:
|
||||||
|
- Verify prompt code was generated
|
||||||
|
- Check syntax if possible
|
||||||
|
- Test prompt with valid inputs
|
||||||
|
- Test validation with invalid inputs
|
||||||
|
- Verify conditional logic works (if applicable)
|
||||||
|
- Test interactive wizard flow (if added)
|
||||||
|
- Report any failures
|
||||||
|
|
||||||
|
Phase 7: Summary
|
||||||
|
Goal: Report results and next steps
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Display summary:
|
||||||
|
- Prompt types added: {count}
|
||||||
|
- Framework: {framework}
|
||||||
|
- Library installed: {inquirer/questionary}
|
||||||
|
- Files modified/created
|
||||||
|
- Validation patterns included
|
||||||
|
- Show usage examples:
|
||||||
|
- Running interactive prompts
|
||||||
|
- Validation patterns
|
||||||
|
- Conditional logic examples
|
||||||
|
- Suggest next steps:
|
||||||
|
- Integrate prompts into subcommands
|
||||||
|
- Add keyboard shortcuts
|
||||||
|
- Implement multi-step wizards
|
||||||
|
- Add accessibility features
|
||||||
192
commands/add-output-formatting.md
Normal file
192
commands/add-output-formatting.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
---
|
||||||
|
description: Add rich terminal output (colors, tables, spinners)
|
||||||
|
argument-hint: [format-type] [format-type-2] [format-type-3] ...
|
||||||
|
allowed-tools: Task, AskUserQuestion, Bash, Read, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
🚨 **EXECUTION NOTICE FOR CLAUDE**
|
||||||
|
|
||||||
|
When you invoke this command via SlashCommand, the system returns THESE INSTRUCTIONS below.
|
||||||
|
|
||||||
|
**YOU are the executor. This is NOT an autonomous subprocess.**
|
||||||
|
|
||||||
|
- ✅ The phases below are YOUR execution checklist
|
||||||
|
- ✅ YOU must run each phase immediately using tools (Bash, Read, Write, Edit, TodoWrite)
|
||||||
|
- ✅ Complete ALL phases before considering this command done
|
||||||
|
- ❌ DON't wait for "the command to complete" - YOU complete it by executing the phases
|
||||||
|
- ❌ DON't treat this as status output - it IS your instruction set
|
||||||
|
|
||||||
|
**Immediately after SlashCommand returns, start executing Phase 0, then Phase 1, etc.**
|
||||||
|
|
||||||
|
See `@CLAUDE.md` section "SlashCommand Execution - YOU Are The Executor" for detailed explanation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**Arguments**: $ARGUMENTS
|
||||||
|
|
||||||
|
Goal: Orchestrate adding output formatting capabilities to CLI tool, launching parallel agents for 3+ format types.
|
||||||
|
|
||||||
|
**Architectural Context:**
|
||||||
|
|
||||||
|
This command is an **orchestrator** that:
|
||||||
|
- Detects the CLI framework
|
||||||
|
- Gathers requirements
|
||||||
|
- Launches 1 agent for 1-2 format types
|
||||||
|
- Launches MULTIPLE agents IN PARALLEL for 3+ format types (all in ONE message)
|
||||||
|
|
||||||
|
Phase 1: Load Architectural Framework
|
||||||
|
Goal: Understand composition and parallelization patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Load component decision framework:
|
||||||
|
!{Read ~/.claude/plugins/marketplaces/domain-plugin-builder/plugins/domain-plugin-builder/docs/frameworks/claude/reference/component-decision-framework.md}
|
||||||
|
- Key patterns:
|
||||||
|
- Commands orchestrate, agents implement
|
||||||
|
- For 3+ items: Launch multiple agents in PARALLEL
|
||||||
|
- Send ALL Task() calls in SINGLE message
|
||||||
|
- Agents run concurrently for faster execution
|
||||||
|
|
||||||
|
Phase 2: Parse Arguments & Determine Mode
|
||||||
|
Goal: Count how many output format types to implement
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Parse $ARGUMENTS to extract format types:
|
||||||
|
!{bash echo "$ARGUMENTS" | wc -w}
|
||||||
|
- Store count
|
||||||
|
- Extract each format type:
|
||||||
|
- If count = 0: Ask user for format type preference
|
||||||
|
- If count = 1: Single format type mode
|
||||||
|
- If count = 2: Two format types mode
|
||||||
|
- If count >= 3: **PARALLEL MODE** - multiple agents
|
||||||
|
|
||||||
|
Phase 3: Detect Existing CLI Framework
|
||||||
|
Goal: Identify the framework and language
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Check for language indicators:
|
||||||
|
- !{bash ls -1 package.json setup.py pyproject.toml go.mod Cargo.toml 2>/dev/null | head -1}
|
||||||
|
- For Node.js (package.json found):
|
||||||
|
- !{bash grep -E '"(commander|yargs|oclif|gluegun)"' package.json 2>/dev/null | head -1}
|
||||||
|
- For Python files:
|
||||||
|
- !{bash grep -r "import click\|import typer\|import argparse\|import fire" . --include="*.py" 2>/dev/null | head -1}
|
||||||
|
- For Go:
|
||||||
|
- !{bash grep -r "github.com/spf13/cobra\|github.com/urfave/cli" . --include="*.go" 2>/dev/null | head -1}
|
||||||
|
- For Rust:
|
||||||
|
- !{bash grep "clap" Cargo.toml 2>/dev/null}
|
||||||
|
- Store detected framework and language
|
||||||
|
|
||||||
|
Phase 4: Gather Requirements (for all format types)
|
||||||
|
Goal: Collect specifications
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- If no format types in $ARGUMENTS:
|
||||||
|
- Ask user via AskUserQuestion:
|
||||||
|
- Which format types? (colors, tables, spinners, progress-bars, boxes, icons, panels, syntax-highlighting)
|
||||||
|
- Cross-platform support needed?
|
||||||
|
- Unicode or ASCII output?
|
||||||
|
- Specific styling preferences?
|
||||||
|
- For EACH format type from Phase 2:
|
||||||
|
- Store format type (colors, tables, spinners, progress-bars, boxes, icons, panels, syntax-highlighting)
|
||||||
|
- Determine library requirements
|
||||||
|
- Plan styling approach
|
||||||
|
|
||||||
|
Phase 5: Launch Agent(s) for Implementation
|
||||||
|
Goal: Delegate to cli-feature-impl agent(s)
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
**Decision: 1-2 format types = single/sequential agents, 3+ format types = PARALLEL agents**
|
||||||
|
|
||||||
|
**For 1-2 Format Types:**
|
||||||
|
|
||||||
|
Task(
|
||||||
|
description="Add output formatting to CLI",
|
||||||
|
subagent_type="cli-tool-builder:cli-feature-impl",
|
||||||
|
prompt="You are cli-feature-impl. Add rich output formatting to the CLI.
|
||||||
|
|
||||||
|
Framework: {detected_framework}
|
||||||
|
Language: {detected_language}
|
||||||
|
|
||||||
|
Format Types: {format_types}
|
||||||
|
Requirements:
|
||||||
|
- Libraries: {chalk/ora/cli-table3 for Node.js, rich/colorama/tqdm for Python}
|
||||||
|
- Format types: {colors/tables/spinners/progress-bars/boxes/icons/panels/syntax-highlighting}
|
||||||
|
- Cross-platform: {yes/no}
|
||||||
|
- Unicode or ASCII: {unicode/ascii/both}
|
||||||
|
- Styling: {styling_preferences}
|
||||||
|
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns) for patterns.
|
||||||
|
Generate output formatting utilities, examples, documentation.
|
||||||
|
|
||||||
|
Deliverable: Working output formatting integrated into CLI"
|
||||||
|
)
|
||||||
|
|
||||||
|
**For 3+ Format Types - CRITICAL: Send ALL Task() calls in ONE MESSAGE:**
|
||||||
|
|
||||||
|
Task(description="Add format type 1", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add output formatting for '{type_1}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Format type: {type_1}
|
||||||
|
Library: {library_1}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Output formatting code for {type_1}")
|
||||||
|
|
||||||
|
Task(description="Add format type 2", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add output formatting for '{type_2}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Format type: {type_2}
|
||||||
|
Library: {library_2}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Output formatting code for {type_2}")
|
||||||
|
|
||||||
|
Task(description="Add format type 3", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add output formatting for '{type_3}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Format type: {type_3}
|
||||||
|
Library: {library_3}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Output formatting code for {type_3}")
|
||||||
|
|
||||||
|
[Continue for all N format types...]
|
||||||
|
|
||||||
|
**DO NOT wait between Task() calls - send them ALL at once!**
|
||||||
|
|
||||||
|
Agents run in parallel. Proceed to Phase 6 only after ALL complete.
|
||||||
|
|
||||||
|
Phase 6: Verification
|
||||||
|
Goal: Confirm all format types were added
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- For each format type:
|
||||||
|
- Verify formatting code was generated
|
||||||
|
- Check syntax if possible
|
||||||
|
- Test output rendering
|
||||||
|
- Verify cross-platform compatibility (if required)
|
||||||
|
- Test combined formatting (colors + tables, spinners + progress, etc.)
|
||||||
|
- Verify library dependencies installed
|
||||||
|
- Report any failures
|
||||||
|
|
||||||
|
Phase 7: Summary
|
||||||
|
Goal: Report results and next steps
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Display summary:
|
||||||
|
- Format types added: {count}
|
||||||
|
- Framework: {framework}
|
||||||
|
- Libraries installed: {libraries with versions}
|
||||||
|
- Files modified/created
|
||||||
|
- Unicode/ASCII support
|
||||||
|
- Show usage examples:
|
||||||
|
- Colored output
|
||||||
|
- Table formatting
|
||||||
|
- Spinners and progress bars
|
||||||
|
- Boxed messages
|
||||||
|
- Icons and symbols
|
||||||
|
- Suggest next steps:
|
||||||
|
- Create output utility module
|
||||||
|
- Add formatting to existing commands
|
||||||
|
- Implement log levels with colors
|
||||||
|
- Add terminal width detection
|
||||||
|
- Support NO_COLOR environment variable
|
||||||
200
commands/add-package.md
Normal file
200
commands/add-package.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
description: Setup distribution packaging (npm, PyPI, Homebrew)
|
||||||
|
argument-hint: [platform] [platform-2] [platform-3] ...
|
||||||
|
allowed-tools: Task, AskUserQuestion, Bash, Read, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
🚨 **EXECUTION NOTICE FOR CLAUDE**
|
||||||
|
|
||||||
|
When you invoke this command via SlashCommand, the system returns THESE INSTRUCTIONS below.
|
||||||
|
|
||||||
|
**YOU are the executor. This is NOT an autonomous subprocess.**
|
||||||
|
|
||||||
|
- ✅ The phases below are YOUR execution checklist
|
||||||
|
- ✅ YOU must run each phase immediately using tools (Bash, Read, Write, Edit, TodoWrite)
|
||||||
|
- ✅ Complete ALL phases before considering this command done
|
||||||
|
- ❌ DON't wait for "the command to complete" - YOU complete it by executing the phases
|
||||||
|
- ❌ DON't treat this as status output - it IS your instruction set
|
||||||
|
|
||||||
|
**Immediately after SlashCommand returns, start executing Phase 0, then Phase 1, etc.**
|
||||||
|
|
||||||
|
See `@CLAUDE.md` section "SlashCommand Execution - YOU Are The Executor" for detailed explanation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**Arguments**: $ARGUMENTS
|
||||||
|
|
||||||
|
Goal: Orchestrate setting up distribution packaging for CLI tool, launching parallel agents for 3+ platforms.
|
||||||
|
|
||||||
|
**Architectural Context:**
|
||||||
|
|
||||||
|
This command is an **orchestrator** that:
|
||||||
|
- Detects the CLI framework
|
||||||
|
- Gathers requirements
|
||||||
|
- Launches 1 agent for 1-2 distribution platforms
|
||||||
|
- Launches MULTIPLE agents IN PARALLEL for 3+ platforms (all in ONE message)
|
||||||
|
|
||||||
|
Phase 1: Load Architectural Framework
|
||||||
|
Goal: Understand composition and parallelization patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Load component decision framework:
|
||||||
|
!{Read ~/.claude/plugins/marketplaces/domain-plugin-builder/plugins/domain-plugin-builder/docs/frameworks/claude/reference/component-decision-framework.md}
|
||||||
|
- Key patterns:
|
||||||
|
- Commands orchestrate, agents implement
|
||||||
|
- For 3+ items: Launch multiple agents in PARALLEL
|
||||||
|
- Send ALL Task() calls in SINGLE message
|
||||||
|
- Agents run concurrently for faster execution
|
||||||
|
|
||||||
|
Phase 2: Parse Arguments & Determine Mode
|
||||||
|
Goal: Count how many distribution platforms to configure
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Parse $ARGUMENTS to extract platforms:
|
||||||
|
!{bash echo "$ARGUMENTS" | wc -w}
|
||||||
|
- Store count
|
||||||
|
- Extract each platform:
|
||||||
|
- If count = 0: Ask user for platform preference
|
||||||
|
- If count = 1: Single platform mode
|
||||||
|
- If count = 2: Two platforms mode
|
||||||
|
- If count >= 3: **PARALLEL MODE** - multiple agents
|
||||||
|
|
||||||
|
Phase 3: Detect Existing CLI Framework
|
||||||
|
Goal: Identify the framework and language
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Check for language indicators:
|
||||||
|
- !{bash ls -1 package.json setup.py pyproject.toml go.mod Cargo.toml 2>/dev/null | head -1}
|
||||||
|
- For Node.js (package.json found):
|
||||||
|
- !{bash grep -E '"(commander|yargs|oclif|gluegun)"' package.json 2>/dev/null | head -1}
|
||||||
|
- For Python files:
|
||||||
|
- !{bash grep -r "import click\|import typer\|import argparse\|import fire" . --include="*.py" 2>/dev/null | head -1}
|
||||||
|
- For Go:
|
||||||
|
- !{bash grep -r "github.com/spf13/cobra\|github.com/urfave/cli" . --include="*.go" 2>/dev/null | head -1}
|
||||||
|
- For Rust:
|
||||||
|
- !{bash grep "clap" Cargo.toml 2>/dev/null}
|
||||||
|
- Store detected framework and language
|
||||||
|
|
||||||
|
Phase 4: Gather Requirements (for all platforms)
|
||||||
|
Goal: Collect specifications
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- If no platforms in $ARGUMENTS:
|
||||||
|
- Ask user via AskUserQuestion:
|
||||||
|
- Which distribution platforms? (npm, pypi, homebrew, binary, cargo, go-install)
|
||||||
|
- CLI tool name?
|
||||||
|
- Current version? (default: 0.1.0)
|
||||||
|
- GitHub repository URL? (for releases)
|
||||||
|
- Automated releases via GitHub Actions?
|
||||||
|
- For EACH platform from Phase 2:
|
||||||
|
- Store platform type (npm, pypi, homebrew, binary, cargo, go-install)
|
||||||
|
- Determine version management approach
|
||||||
|
- Plan release automation
|
||||||
|
|
||||||
|
Phase 5: Launch Agent(s) for Implementation
|
||||||
|
Goal: Delegate to cli-feature-impl agent(s)
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
**Decision: 1-2 platforms = single/sequential agents, 3+ platforms = PARALLEL agents**
|
||||||
|
|
||||||
|
**For 1-2 Distribution Platforms:**
|
||||||
|
|
||||||
|
Task(
|
||||||
|
description="Add package distribution to CLI",
|
||||||
|
subagent_type="cli-tool-builder:cli-feature-impl",
|
||||||
|
prompt="You are cli-feature-impl. Add distribution packaging to the CLI.
|
||||||
|
|
||||||
|
Framework: {detected_framework}
|
||||||
|
Language: {detected_language}
|
||||||
|
|
||||||
|
Distribution Platforms: {platforms}
|
||||||
|
Requirements:
|
||||||
|
- Platforms: {npm/pypi/homebrew/binary/cargo/go-install}
|
||||||
|
- CLI tool name: {tool_name}
|
||||||
|
- Version: {version}
|
||||||
|
- GitHub repo: {github_url}
|
||||||
|
- Automated releases: {yes/no}
|
||||||
|
- Version management: {standard-version/bump2version/goreleaser}
|
||||||
|
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns) for patterns.
|
||||||
|
Generate package configuration, release workflows, installation docs.
|
||||||
|
|
||||||
|
Deliverable: Working package distribution setup for CLI"
|
||||||
|
)
|
||||||
|
|
||||||
|
**For 3+ Distribution Platforms - CRITICAL: Send ALL Task() calls in ONE MESSAGE:**
|
||||||
|
|
||||||
|
Task(description="Add platform 1", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add distribution packaging for '{platform_1}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Platform: {platform_1}
|
||||||
|
Tool name: {tool_name}
|
||||||
|
Version: {version}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Package config for {platform_1}")
|
||||||
|
|
||||||
|
Task(description="Add platform 2", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add distribution packaging for '{platform_2}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Platform: {platform_2}
|
||||||
|
Tool name: {tool_name}
|
||||||
|
Version: {version}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Package config for {platform_2}")
|
||||||
|
|
||||||
|
Task(description="Add platform 3", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add distribution packaging for '{platform_3}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Language: {language}
|
||||||
|
Platform: {platform_3}
|
||||||
|
Tool name: {tool_name}
|
||||||
|
Version: {version}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Package config for {platform_3}")
|
||||||
|
|
||||||
|
[Continue for all N platforms...]
|
||||||
|
|
||||||
|
**DO NOT wait between Task() calls - send them ALL at once!**
|
||||||
|
|
||||||
|
Agents run in parallel. Proceed to Phase 6 only after ALL complete.
|
||||||
|
|
||||||
|
Phase 6: Verification
|
||||||
|
Goal: Confirm all platforms were configured
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- For each platform:
|
||||||
|
- Verify package configuration was generated
|
||||||
|
- Check syntax if possible
|
||||||
|
- Test build process (if applicable)
|
||||||
|
- Verify version management works
|
||||||
|
- Test release workflow (dry-run if applicable)
|
||||||
|
- Verify installation documentation complete
|
||||||
|
- Report any failures
|
||||||
|
|
||||||
|
Phase 7: Summary
|
||||||
|
Goal: Report results and next steps
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Display summary:
|
||||||
|
- Platforms configured: {count}
|
||||||
|
- Framework: {framework}
|
||||||
|
- Tool name: {tool_name}
|
||||||
|
- Version: {version}
|
||||||
|
- Files modified/created
|
||||||
|
- Release automation configured
|
||||||
|
- Show installation commands for each platform:
|
||||||
|
- npm: npm install -g {tool_name}
|
||||||
|
- PyPI: pip install {tool_name}
|
||||||
|
- Homebrew: brew install {user}/{tap}/{tool_name}
|
||||||
|
- Binary: Download and install instructions
|
||||||
|
- Cargo: cargo install {tool_name}
|
||||||
|
- Go: go install github.com/{user}/{tool_name}@latest
|
||||||
|
- Suggest next steps:
|
||||||
|
- Test package installation locally
|
||||||
|
- Create initial release tag
|
||||||
|
- Update documentation with badges
|
||||||
|
- Set up CI/CD secrets for automated publishing
|
||||||
|
- Test version bumping workflow
|
||||||
|
- Create CHANGELOG.md
|
||||||
171
commands/add-subcommand.md
Normal file
171
commands/add-subcommand.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
---
|
||||||
|
description: Add structured subcommands to CLI tool
|
||||||
|
argument-hint: <command-name> [command-2] [command-3] ...
|
||||||
|
allowed-tools: Task, AskUserQuestion, Bash, Read, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
🚨 **EXECUTION NOTICE FOR CLAUDE**
|
||||||
|
|
||||||
|
When you invoke this command via SlashCommand, the system returns THESE INSTRUCTIONS below.
|
||||||
|
|
||||||
|
**YOU are the executor. This is NOT an autonomous subprocess.**
|
||||||
|
|
||||||
|
- ✅ The phases below are YOUR execution checklist
|
||||||
|
- ✅ YOU must run each phase immediately using tools (Bash, Read, Write, Edit, TodoWrite)
|
||||||
|
- ✅ Complete ALL phases before considering this command done
|
||||||
|
- ❌ DON't wait for "the command to complete" - YOU complete it by executing the phases
|
||||||
|
- ❌ DON't treat this as status output - it IS your instruction set
|
||||||
|
|
||||||
|
**Immediately after SlashCommand returns, start executing Phase 0, then Phase 1, etc.**
|
||||||
|
|
||||||
|
See `@CLAUDE.md` section "SlashCommand Execution - YOU Are The Executor" for detailed explanation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**Arguments**: $ARGUMENTS
|
||||||
|
|
||||||
|
Goal: Orchestrate adding one or multiple subcommands to a CLI tool, launching parallel agents for 3+ subcommands.
|
||||||
|
|
||||||
|
**Architectural Context:**
|
||||||
|
|
||||||
|
This command is an **orchestrator** that:
|
||||||
|
- Detects the CLI framework
|
||||||
|
- Gathers requirements
|
||||||
|
- Launches 1 agent for 1-2 subcommands
|
||||||
|
- Launches MULTIPLE agents IN PARALLEL for 3+ subcommands (all in ONE message)
|
||||||
|
|
||||||
|
Phase 1: Load Architectural Framework
|
||||||
|
Goal: Understand composition and parallelization patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Load component decision framework:
|
||||||
|
!{Read ~/.claude/plugins/marketplaces/domain-plugin-builder/plugins/domain-plugin-builder/docs/frameworks/claude/reference/component-decision-framework.md}
|
||||||
|
- Key patterns:
|
||||||
|
- Commands orchestrate, agents implement
|
||||||
|
- For 3+ items: Launch multiple agents in PARALLEL
|
||||||
|
- Send ALL Task() calls in SINGLE message
|
||||||
|
- Agents run concurrently for faster execution
|
||||||
|
|
||||||
|
Phase 2: Parse Arguments & Determine Mode
|
||||||
|
Goal: Count how many subcommands to create
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Parse $ARGUMENTS to extract subcommand names:
|
||||||
|
!{bash echo "$ARGUMENTS" | wc -w}
|
||||||
|
- Store count
|
||||||
|
- Extract each subcommand name:
|
||||||
|
- If count = 1: Single subcommand mode
|
||||||
|
- If count = 2: Two subcommands mode
|
||||||
|
- If count >= 3: **PARALLEL MODE** - multiple agents
|
||||||
|
|
||||||
|
Phase 3: Detect Existing CLI Framework
|
||||||
|
Goal: Identify the framework to match patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Check for language indicators:
|
||||||
|
- !{bash ls -1 package.json setup.py pyproject.toml go.mod Cargo.toml 2>/dev/null | head -1}
|
||||||
|
- For Node.js (package.json found):
|
||||||
|
- !{bash grep -E '"(commander|yargs|oclif|gluegun)"' package.json 2>/dev/null | head -1}
|
||||||
|
- For Python files:
|
||||||
|
- !{bash grep -r "import click\|import typer\|import argparse\|import fire" . --include="*.py" 2>/dev/null | head -1}
|
||||||
|
- For Go:
|
||||||
|
- !{bash grep -r "github.com/spf13/cobra\|github.com/urfave/cli" . --include="*.go" 2>/dev/null | head -1}
|
||||||
|
- For Rust:
|
||||||
|
- !{bash grep "clap" Cargo.toml 2>/dev/null}
|
||||||
|
- Store detected framework
|
||||||
|
|
||||||
|
Phase 4: Gather Requirements (for all subcommands)
|
||||||
|
Goal: Collect specifications
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- For EACH subcommand name from Phase 2:
|
||||||
|
- Ask user via AskUserQuestion:
|
||||||
|
- Description/purpose
|
||||||
|
- Required arguments
|
||||||
|
- Optional flags
|
||||||
|
- Validation needs
|
||||||
|
- Store requirements for that subcommand
|
||||||
|
|
||||||
|
Phase 5: Launch Agent(s) for Implementation
|
||||||
|
Goal: Delegate to cli-feature-impl agent(s)
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
**Decision: 1-2 subcommands = single/sequential agents, 3+ subcommands = PARALLEL agents**
|
||||||
|
|
||||||
|
**For 1-2 Subcommands:**
|
||||||
|
|
||||||
|
Task(
|
||||||
|
description="Add subcommand to CLI",
|
||||||
|
subagent_type="cli-tool-builder:cli-feature-impl",
|
||||||
|
prompt="You are cli-feature-impl. Add subcommand '{name}' to the CLI.
|
||||||
|
|
||||||
|
Framework: {detected_framework}
|
||||||
|
Language: {detected_language}
|
||||||
|
|
||||||
|
Subcommand: {name}
|
||||||
|
Description: {description}
|
||||||
|
Arguments: {arguments}
|
||||||
|
Flags: {flags}
|
||||||
|
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns) for patterns.
|
||||||
|
Generate code, integrate it, test syntax.
|
||||||
|
|
||||||
|
Deliverable: Working subcommand integrated into CLI"
|
||||||
|
)
|
||||||
|
|
||||||
|
**For 3+ Subcommands - CRITICAL: Send ALL Task() calls in ONE MESSAGE:**
|
||||||
|
|
||||||
|
Task(description="Add subcommand 1", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add subcommand '{name_1}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Description: {desc_1}
|
||||||
|
Arguments: {args_1}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Subcommand 1 code")
|
||||||
|
|
||||||
|
Task(description="Add subcommand 2", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add subcommand '{name_2}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Description: {desc_2}
|
||||||
|
Arguments: {args_2}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Subcommand 2 code")
|
||||||
|
|
||||||
|
Task(description="Add subcommand 3", subagent_type="cli-tool-builder:cli-feature-impl", prompt="Add subcommand '{name_3}'.
|
||||||
|
Framework: {framework}
|
||||||
|
Description: {desc_3}
|
||||||
|
Arguments: {args_3}
|
||||||
|
Use Skill(cli-tool-builder:{framework}-patterns).
|
||||||
|
Deliverable: Subcommand 3 code")
|
||||||
|
|
||||||
|
[Continue for all N subcommands...]
|
||||||
|
|
||||||
|
**DO NOT wait between Task() calls - send them ALL at once!**
|
||||||
|
|
||||||
|
Agents run in parallel. Proceed to Phase 6 only after ALL complete.
|
||||||
|
|
||||||
|
Phase 6: Verification
|
||||||
|
Goal: Confirm all subcommands were added
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- For each subcommand:
|
||||||
|
- Verify code was generated
|
||||||
|
- Check syntax if possible
|
||||||
|
- Test help output
|
||||||
|
- Report any failures
|
||||||
|
|
||||||
|
Phase 7: Summary
|
||||||
|
Goal: Report results and next steps
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Display summary:
|
||||||
|
- Subcommands added: {count}
|
||||||
|
- Framework: {framework}
|
||||||
|
- Files modified/created
|
||||||
|
- Show usage examples for each subcommand
|
||||||
|
- Suggest next steps:
|
||||||
|
- Test each subcommand
|
||||||
|
- Add interactive prompts: /cli-tool-builder:add-interactive
|
||||||
|
- Add output formatting: /cli-tool-builder:add-output-formatting
|
||||||
|
- Add validation: /cli-tool-builder:add-args-parser
|
||||||
160
commands/new-cli.md
Normal file
160
commands/new-cli.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
description: Initialize new CLI tool project with framework selection
|
||||||
|
argument-hint: <tool-name>
|
||||||
|
allowed-tools: Task, AskUserQuestion, Bash, Read
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
🚨 **EXECUTION NOTICE FOR CLAUDE**
|
||||||
|
|
||||||
|
When you invoke this command via SlashCommand, the system returns THESE INSTRUCTIONS below.
|
||||||
|
|
||||||
|
**YOU are the executor. This is NOT an autonomous subprocess.**
|
||||||
|
|
||||||
|
- ✅ The phases below are YOUR execution checklist
|
||||||
|
- ✅ YOU must run each phase immediately using tools (Bash, Read, Write, Edit, TodoWrite)
|
||||||
|
- ✅ Complete ALL phases before considering this command done
|
||||||
|
- ❌ DON't wait for "the command to complete" - YOU complete it by executing the phases
|
||||||
|
- ❌ DON't treat this as status output - it IS your instruction set
|
||||||
|
|
||||||
|
**Immediately after SlashCommand returns, start executing Phase 0, then Phase 1, etc.**
|
||||||
|
|
||||||
|
See `@CLAUDE.md` section "SlashCommand Execution - YOU Are The Executor" for detailed explanation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**Arguments**: $ARGUMENTS
|
||||||
|
|
||||||
|
Goal: Orchestrate CLI project initialization by gathering requirements and delegating to the cli-setup agent for implementation.
|
||||||
|
|
||||||
|
**Architectural Context:**
|
||||||
|
|
||||||
|
This command follows the composition pattern:
|
||||||
|
- **Commands are orchestrators** - They ask questions and delegate
|
||||||
|
- **Agents are workers** - They do the complex implementation
|
||||||
|
- Commands invoke agents via Task tool, not implement directly
|
||||||
|
|
||||||
|
Phase 1: Load Architectural Framework
|
||||||
|
Goal: Understand component composition patterns
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Load the complete component decision framework:
|
||||||
|
!{Read ~/.claude/plugins/marketplaces/domain-plugin-builder/plugins/domain-plugin-builder/docs/frameworks/claude/reference/component-decision-framework.md}
|
||||||
|
- This provides critical understanding of:
|
||||||
|
- Commands are the primitive (orchestrators)
|
||||||
|
- Agents are for complex multi-step workflows
|
||||||
|
- Proper composition: Command → Agent → Skills
|
||||||
|
- When to delegate vs when to execute directly
|
||||||
|
|
||||||
|
Phase 2: Gather Requirements
|
||||||
|
Goal: Collect user preferences for the CLI project
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Parse $ARGUMENTS for tool name
|
||||||
|
- If no tool name provided, ask user for it via AskUserQuestion
|
||||||
|
- Ask user for language preference:
|
||||||
|
- Options: TypeScript, Python, Go, Rust
|
||||||
|
- Include descriptions of each language's strengths
|
||||||
|
- Based on language selection, ask for framework:
|
||||||
|
- **Python**: Click (decorator-based), Typer (type-safe), argparse (stdlib), Fire (auto-generate)
|
||||||
|
- **TypeScript/Node.js**: Commander.js (simple), yargs (advanced), oclif (enterprise), gluegun (generators)
|
||||||
|
- **Go**: Cobra (production), cli (lightweight)
|
||||||
|
- **Rust**: clap (full-featured)
|
||||||
|
- Ask for package manager preference:
|
||||||
|
- Python: pip, poetry, pipenv
|
||||||
|
- Node.js: npm, yarn, pnpm
|
||||||
|
- Go: go modules (automatic)
|
||||||
|
- Rust: cargo (automatic)
|
||||||
|
|
||||||
|
Phase 3: Environment Validation
|
||||||
|
Goal: Verify required tools are available
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Check if selected language runtime is installed:
|
||||||
|
- Python: !{bash python3 --version 2>/dev/null || echo "Not found"}
|
||||||
|
- Node.js: !{bash node --version 2>/dev/null || echo "Not found"}
|
||||||
|
- Go: !{bash go version 2>/dev/null || echo "Not found"}
|
||||||
|
- Rust: !{bash rustc --version 2>/dev/null || echo "Not found"}
|
||||||
|
- Check if selected package manager is available
|
||||||
|
- If any tools missing, inform user and provide installation instructions
|
||||||
|
- If critical tools missing, stop and ask user to install them first
|
||||||
|
|
||||||
|
Phase 4: Delegate to CLI Setup Agent
|
||||||
|
Goal: Hand off implementation to specialized agent
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
**Invoke the cli-setup agent to perform the actual implementation:**
|
||||||
|
|
||||||
|
Task(
|
||||||
|
description="Initialize CLI project with chosen framework",
|
||||||
|
subagent_type="cli-tool-builder:cli-setup",
|
||||||
|
prompt="You are the cli-setup agent. Initialize a new CLI tool project with the specifications provided.
|
||||||
|
|
||||||
|
**Project Details:**
|
||||||
|
- Tool Name: {tool_name from Phase 2}
|
||||||
|
- Language: {language from Phase 2}
|
||||||
|
- Framework: {framework from Phase 2}
|
||||||
|
- Package Manager: {package_manager from Phase 2}
|
||||||
|
|
||||||
|
**User Requirements:**
|
||||||
|
Create a complete CLI tool project with:
|
||||||
|
1. Proper directory structure for the chosen framework
|
||||||
|
2. Entry point file with correct shebang
|
||||||
|
3. Package configuration (package.json, setup.py, Cargo.toml, or go.mod)
|
||||||
|
4. Executable bin/entry point configuration
|
||||||
|
5. Dependencies installation for chosen framework
|
||||||
|
6. Basic command structure following framework conventions
|
||||||
|
7. README with usage instructions
|
||||||
|
8. LICENSE file (MIT)
|
||||||
|
9. .gitignore for the language
|
||||||
|
10. Git repository initialization
|
||||||
|
|
||||||
|
**Skills Available:**
|
||||||
|
- Use Skill(cli-tool-builder:{framework}-patterns) for framework-specific patterns
|
||||||
|
- Example: Skill(cli-tool-builder:click-patterns) for Python Click
|
||||||
|
- Example: Skill(cli-tool-builder:commander-patterns) for Commander.js
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
Follow the phased approach in your agent definition:
|
||||||
|
1. Discovery: Confirm requirements
|
||||||
|
2. Analysis: Determine exact dependencies and structure
|
||||||
|
3. Planning: Design directory layout and files
|
||||||
|
4. Implementation: Create all files and install dependencies
|
||||||
|
5. Verification: Test that CLI works and passes validation
|
||||||
|
|
||||||
|
Deliverable: Complete working CLI project in ./{tool_name}/ directory"
|
||||||
|
)
|
||||||
|
|
||||||
|
Wait for agent to complete. Agent will use framework-specific skills and create the complete project.
|
||||||
|
|
||||||
|
Phase 5: Verification
|
||||||
|
Goal: Confirm project was created successfully
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Check that project directory exists: !{bash ls -la {tool_name}/}
|
||||||
|
- Verify key files were created:
|
||||||
|
- Entry point file
|
||||||
|
- Package configuration
|
||||||
|
- README
|
||||||
|
- LICENSE
|
||||||
|
- .gitignore
|
||||||
|
- Display project structure to user
|
||||||
|
|
||||||
|
Phase 6: Summary
|
||||||
|
Goal: Report success and provide next steps
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- Display project creation summary:
|
||||||
|
- Tool name
|
||||||
|
- Language and framework
|
||||||
|
- Project location
|
||||||
|
- Entry point file
|
||||||
|
- Show next steps:
|
||||||
|
1. cd {tool_name}
|
||||||
|
2. Test CLI: ./{tool_name} --help (or appropriate command)
|
||||||
|
3. Add subcommands: /cli-tool-builder:add-subcommand <command-name>
|
||||||
|
4. Add features: /cli-tool-builder:add-interactive, /cli-tool-builder:add-output-formatting
|
||||||
|
5. Package for distribution: /cli-tool-builder:add-package
|
||||||
|
- Encourage user to explore the generated code and customize it
|
||||||
10
hooks/hooks.json
Normal file
10
hooks/hooks.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [],
|
||||||
|
"PostToolUse": [],
|
||||||
|
"UserPromptSubmit": [],
|
||||||
|
"SessionStart": [],
|
||||||
|
"SessionEnd": [],
|
||||||
|
"PreCompact": []
|
||||||
|
}
|
||||||
|
}
|
||||||
1021
plugin.lock.json
Normal file
1021
plugin.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
201
skills/argparse-patterns/SKILL.md
Normal file
201
skills/argparse-patterns/SKILL.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
name: argparse-patterns
|
||||||
|
description: Standard library Python argparse examples with subparsers, choices, actions, and nested command patterns. Use when building Python CLIs without external dependencies, implementing argument parsing, creating subcommands, or when user mentions argparse, standard library CLI, subparsers, argument validation, or nested commands.
|
||||||
|
allowed-tools: Read, Write, Edit, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
# argparse-patterns
|
||||||
|
|
||||||
|
Python's built-in argparse module for CLI argument parsing - no external dependencies required.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Provides comprehensive argparse patterns using only Python standard library. Includes subparsers for nested commands, choices for validation, custom actions, argument groups, and mutually exclusive options.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### Basic Parser Setup
|
||||||
|
|
||||||
|
1. Import argparse and create parser with description
|
||||||
|
2. Add version info with `action='version'`
|
||||||
|
3. Set formatter_class for better help formatting
|
||||||
|
4. Parse arguments with `parser.parse_args()`
|
||||||
|
|
||||||
|
### Subparsers (Nested Commands)
|
||||||
|
|
||||||
|
1. Use `parser.add_subparsers(dest='command')` to create command groups
|
||||||
|
2. Add individual commands with `subparsers.add_parser('command-name')`
|
||||||
|
3. Each subparser can have its own arguments and options
|
||||||
|
4. Nest subparsers for multi-level commands (e.g., `mycli config get key`)
|
||||||
|
|
||||||
|
### Choices and Validation
|
||||||
|
|
||||||
|
1. Use `choices=['opt1', 'opt2']` to restrict values
|
||||||
|
2. Implement custom validation with type functions
|
||||||
|
3. Add validators using argparse types
|
||||||
|
4. Set defaults with `default=value`
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
1. `store_true/store_false` - Boolean flags
|
||||||
|
2. `store_const` - Store constant value
|
||||||
|
3. `append` - Collect multiple values
|
||||||
|
4. `count` - Count flag occurrences
|
||||||
|
5. `version` - Display version and exit
|
||||||
|
6. Custom actions with Action subclass
|
||||||
|
|
||||||
|
### Argument Types
|
||||||
|
|
||||||
|
1. Positional arguments - Required by default
|
||||||
|
2. Optional arguments - Prefix with `--` or `-`
|
||||||
|
3. Type coercion - `type=int`, `type=float`, `type=pathlib.Path`
|
||||||
|
4. Nargs - `'?'` (optional), `'*'` (zero or more), `'+'` (one or more)
|
||||||
|
|
||||||
|
## Available Templates
|
||||||
|
|
||||||
|
### Python Templates
|
||||||
|
|
||||||
|
- **basic-parser.py** - Simple parser with arguments and options
|
||||||
|
- **subparser-pattern.py** - Single-level subcommands
|
||||||
|
- **nested-subparser.py** - Multi-level nested commands (e.g., git config get)
|
||||||
|
- **choices-validation.py** - Argument choices and validation
|
||||||
|
- **boolean-flags.py** - Boolean flag patterns
|
||||||
|
- **custom-actions.py** - Custom action classes
|
||||||
|
- **mutually-exclusive.py** - Mutually exclusive groups
|
||||||
|
- **argument-groups.py** - Organizing related arguments
|
||||||
|
- **type-coercion.py** - Custom type converters
|
||||||
|
- **variadic-args.py** - Variable argument patterns
|
||||||
|
|
||||||
|
### TypeScript Templates
|
||||||
|
|
||||||
|
- **argparse-to-commander.ts** - argparse patterns translated to commander.js
|
||||||
|
- **argparse-to-yargs.ts** - argparse patterns translated to yargs
|
||||||
|
- **parser-comparison.ts** - Side-by-side argparse vs Node.js patterns
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
- **generate-parser.sh** - Generate argparse parser from specifications
|
||||||
|
- **validate-parser.sh** - Validate parser structure and completeness
|
||||||
|
- **test-parser.sh** - Test parser with various argument combinations
|
||||||
|
- **convert-to-click.sh** - Convert argparse code to Click decorators
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See `examples/` directory for comprehensive patterns:
|
||||||
|
|
||||||
|
- **basic-usage.md** - Simple CLI with arguments
|
||||||
|
- **subcommands.md** - Multi-command CLI (like git, docker)
|
||||||
|
- **nested-commands.md** - Deep command hierarchies
|
||||||
|
- **validation-patterns.md** - Argument validation strategies
|
||||||
|
- **advanced-parsing.md** - Complex parsing scenarios
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Simple CLI with Options
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser = argparse.ArgumentParser(description='Deploy application')
|
||||||
|
parser.add_argument('--env', choices=['dev', 'staging', 'prod'], default='dev')
|
||||||
|
parser.add_argument('--force', action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Subcommands (git-like)
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
subparsers = parser.add_subparsers(dest='command')
|
||||||
|
|
||||||
|
deploy_cmd = subparsers.add_parser('deploy')
|
||||||
|
deploy_cmd.add_argument('environment')
|
||||||
|
|
||||||
|
config_cmd = subparsers.add_parser('config')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Nested Subcommands (git config get/set)
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
subparsers = parser.add_subparsers(dest='command')
|
||||||
|
|
||||||
|
config = subparsers.add_parser('config')
|
||||||
|
config_subs = config.add_subparsers(dest='config_command')
|
||||||
|
|
||||||
|
config_get = config_subs.add_parser('get')
|
||||||
|
config_get.add_argument('key')
|
||||||
|
|
||||||
|
config_set = config_subs.add_parser('set')
|
||||||
|
config_set.add_argument('key')
|
||||||
|
config_set.add_argument('value')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Mutually Exclusive Options
|
||||||
|
|
||||||
|
```python
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument('--json', action='store_true')
|
||||||
|
group.add_argument('--yaml', action='store_true')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 5: Custom Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_port(value):
|
||||||
|
ivalue = int(value)
|
||||||
|
if ivalue < 1 or ivalue > 65535:
|
||||||
|
raise argparse.ArgumentTypeError(f"{value} is not a valid port")
|
||||||
|
return ivalue
|
||||||
|
|
||||||
|
parser.add_argument('--port', type=validate_port, default=8080)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always provide help text** - Use `help=` for every argument
|
||||||
|
2. **Set sensible defaults** - Use `default=` to avoid None values
|
||||||
|
3. **Use choices for fixed options** - Better than manual validation
|
||||||
|
4. **Group related arguments** - Use `add_argument_group()` for clarity
|
||||||
|
5. **Handle missing subcommands** - Check if `args.command` is None
|
||||||
|
6. **Use type coercion** - Prefer `type=int` over manual conversion
|
||||||
|
7. **Provide examples** - Use `epilog=` for usage examples
|
||||||
|
|
||||||
|
## Advantages Over External Libraries
|
||||||
|
|
||||||
|
- **No dependencies** - Built into Python standard library
|
||||||
|
- **Stable API** - Won't break with updates
|
||||||
|
- **Universal** - Works everywhere Python works
|
||||||
|
- **Well documented** - Extensive official documentation
|
||||||
|
- **Lightweight** - No installation or import overhead
|
||||||
|
|
||||||
|
## When to Use argparse
|
||||||
|
|
||||||
|
Use argparse when:
|
||||||
|
- Building simple to medium complexity CLIs
|
||||||
|
- Avoiding external dependencies is important
|
||||||
|
- Working in restricted environments
|
||||||
|
- Learning CLI patterns (clear, explicit API)
|
||||||
|
|
||||||
|
Consider alternatives when:
|
||||||
|
- Need decorator-based syntax (use Click/Typer)
|
||||||
|
- Want type safety and auto-completion (use Typer)
|
||||||
|
- Rapid prototyping from existing code (use Fire)
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
This skill integrates with:
|
||||||
|
- `cli-setup` agent - Initialize Python CLI projects
|
||||||
|
- `cli-feature-impl` agent - Implement command logic
|
||||||
|
- `cli-verifier-python` agent - Validate argparse structure
|
||||||
|
- `click-patterns` skill - Compare with Click patterns
|
||||||
|
- `typer-patterns` skill - Compare with Typer patterns
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.7+ (argparse included in standard library)
|
||||||
|
- No external dependencies required
|
||||||
|
- Works on all platforms (Windows, macOS, Linux)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Purpose**: Standard library Python CLI argument parsing patterns
|
||||||
|
**Used by**: Python CLI projects prioritizing zero dependencies
|
||||||
473
skills/argparse-patterns/examples/advanced-parsing.md
Normal file
473
skills/argparse-patterns/examples/advanced-parsing.md
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
# Advanced argparse Patterns
|
||||||
|
|
||||||
|
Complex argument parsing scenarios and advanced techniques.
|
||||||
|
|
||||||
|
## Templates Reference
|
||||||
|
|
||||||
|
- `templates/custom-actions.py`
|
||||||
|
- `templates/mutually-exclusive.py`
|
||||||
|
- `templates/argument-groups.py`
|
||||||
|
- `templates/variadic-args.py`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Advanced patterns:
|
||||||
|
- Custom action classes
|
||||||
|
- Mutually exclusive groups
|
||||||
|
- Argument groups (organization)
|
||||||
|
- Variadic arguments (nargs)
|
||||||
|
- Environment variable fallback
|
||||||
|
- Config file integration
|
||||||
|
- Subparser inheritance
|
||||||
|
|
||||||
|
## 1. Custom Actions
|
||||||
|
|
||||||
|
Create custom argument processing logic.
|
||||||
|
|
||||||
|
### Simple Custom Action
|
||||||
|
|
||||||
|
```python
|
||||||
|
class UpperCaseAction(argparse.Action):
|
||||||
|
"""Convert value to uppercase."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
setattr(namespace, self.dest, values.upper())
|
||||||
|
|
||||||
|
|
||||||
|
parser.add_argument('--name', action=UpperCaseAction)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key-Value Action
|
||||||
|
|
||||||
|
```python
|
||||||
|
class KeyValueAction(argparse.Action):
|
||||||
|
"""Parse key=value pairs."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
if '=' not in values:
|
||||||
|
parser.error(f"Must be key=value format: {values}")
|
||||||
|
|
||||||
|
key, value = values.split('=', 1)
|
||||||
|
items = getattr(namespace, self.dest, {}) or {}
|
||||||
|
items[key] = value
|
||||||
|
setattr(namespace, self.dest, items)
|
||||||
|
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--env', '-e',
|
||||||
|
action=KeyValueAction,
|
||||||
|
help='Environment variable (key=value)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Usage: --env API_KEY=abc123 --env DB_URL=postgres://...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load File Action
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LoadFileAction(argparse.Action):
|
||||||
|
"""Load and parse file content."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
try:
|
||||||
|
with open(values, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
setattr(namespace, self.dest, content)
|
||||||
|
except Exception as e:
|
||||||
|
parser.error(f"Cannot load file {values}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
parser.add_argument('--config', action=LoadFileAction)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Mutually Exclusive Groups
|
||||||
|
|
||||||
|
Ensure only one option from a group is used.
|
||||||
|
|
||||||
|
### Basic Exclusivity
|
||||||
|
|
||||||
|
```python
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument('--json', help='Output as JSON')
|
||||||
|
group.add_argument('--yaml', help='Output as YAML')
|
||||||
|
group.add_argument('--xml', help='Output as XML')
|
||||||
|
|
||||||
|
# Valid: --json output.json
|
||||||
|
# Valid: --yaml output.yaml
|
||||||
|
# Invalid: --json output.json --yaml output.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Exclusive Group
|
||||||
|
|
||||||
|
```python
|
||||||
|
mode_group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
mode_group.add_argument('--create', metavar='NAME')
|
||||||
|
mode_group.add_argument('--update', metavar='NAME')
|
||||||
|
mode_group.add_argument('--delete', metavar='NAME')
|
||||||
|
mode_group.add_argument('--list', action='store_true')
|
||||||
|
|
||||||
|
# Must specify exactly one: create, update, delete, or list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Exclusive Groups
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Output format group
|
||||||
|
output = parser.add_mutually_exclusive_group()
|
||||||
|
output.add_argument('--json', action='store_true')
|
||||||
|
output.add_argument('--yaml', action='store_true')
|
||||||
|
|
||||||
|
# Verbosity group
|
||||||
|
verbosity = parser.add_mutually_exclusive_group()
|
||||||
|
verbosity.add_argument('--verbose', action='store_true')
|
||||||
|
verbosity.add_argument('--quiet', action='store_true')
|
||||||
|
|
||||||
|
# Can use one from each group:
|
||||||
|
# Valid: --json --verbose
|
||||||
|
# Invalid: --json --yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Argument Groups
|
||||||
|
|
||||||
|
Organize arguments for better help display.
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
server_group = parser.add_argument_group(
|
||||||
|
'server configuration',
|
||||||
|
'Options for configuring the web server'
|
||||||
|
)
|
||||||
|
server_group.add_argument('--host', default='127.0.0.1')
|
||||||
|
server_group.add_argument('--port', type=int, default=8080)
|
||||||
|
server_group.add_argument('--workers', type=int, default=4)
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
db_group = parser.add_argument_group(
|
||||||
|
'database configuration',
|
||||||
|
'Options for database connection'
|
||||||
|
)
|
||||||
|
db_group.add_argument('--db-host', default='localhost')
|
||||||
|
db_group.add_argument('--db-port', type=int, default=5432)
|
||||||
|
db_group.add_argument('--db-name', required=True)
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
log_group = parser.add_argument_group(
|
||||||
|
'logging configuration',
|
||||||
|
'Options for logging and monitoring'
|
||||||
|
)
|
||||||
|
log_group.add_argument('--log-level',
|
||||||
|
choices=['debug', 'info', 'warning', 'error'],
|
||||||
|
default='info')
|
||||||
|
log_group.add_argument('--log-file', help='Log to file')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Help output groups arguments logically.**
|
||||||
|
|
||||||
|
## 4. Variadic Arguments (nargs)
|
||||||
|
|
||||||
|
Handle variable number of arguments.
|
||||||
|
|
||||||
|
### Optional Single Argument (?)
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--output',
|
||||||
|
nargs='?',
|
||||||
|
const='default.json', # Used if flag present, no value
|
||||||
|
default=None, # Used if flag not present
|
||||||
|
help='Output file'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --output → 'default.json'
|
||||||
|
# --output file.json → 'file.json'
|
||||||
|
# (no flag) → None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zero or More (*)
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--include',
|
||||||
|
nargs='*',
|
||||||
|
default=[],
|
||||||
|
help='Include patterns'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --include → []
|
||||||
|
# --include *.py → ['*.py']
|
||||||
|
# --include *.py *.md → ['*.py', '*.md']
|
||||||
|
```
|
||||||
|
|
||||||
|
### One or More (+)
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'files',
|
||||||
|
nargs='+',
|
||||||
|
help='Input files (at least one required)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# file1.txt → ['file1.txt']
|
||||||
|
# file1.txt file2.txt → ['file1.txt', 'file2.txt']
|
||||||
|
# (no files) → Error: required
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exact Number
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--range',
|
||||||
|
nargs=2,
|
||||||
|
type=int,
|
||||||
|
metavar=('START', 'END'),
|
||||||
|
help='Range as start end'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --range 1 10 → [1, 10]
|
||||||
|
# --range 1 → Error: expected 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remainder
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--command',
|
||||||
|
nargs=argparse.REMAINDER,
|
||||||
|
help='Pass-through command and args'
|
||||||
|
)
|
||||||
|
|
||||||
|
# mycli --command python script.py --arg1 --arg2
|
||||||
|
# → command = ['python', 'script.py', '--arg1', '--arg2']
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Environment Variable Fallback
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--api-key',
|
||||||
|
default=os.environ.get('API_KEY'),
|
||||||
|
help='API key (default: $API_KEY)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--db-url',
|
||||||
|
default=os.environ.get('DATABASE_URL'),
|
||||||
|
help='Database URL (default: $DATABASE_URL)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Precedence: CLI arg > Environment variable > Default
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Config File Integration
|
||||||
|
|
||||||
|
```python
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
"""Load configuration from INI file."""
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(config_file)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument('--config', help='Config file')
|
||||||
|
parser.add_argument('--host', help='Server host')
|
||||||
|
parser.add_argument('--port', type=int, help='Server port')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Load config file if specified
|
||||||
|
if args.config:
|
||||||
|
config = load_config(args.config)
|
||||||
|
|
||||||
|
# Use config values as defaults if not specified on CLI
|
||||||
|
if not args.host:
|
||||||
|
args.host = config.get('server', 'host', fallback='127.0.0.1')
|
||||||
|
|
||||||
|
if not args.port:
|
||||||
|
args.port = config.getint('server', 'port', fallback=8080)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Parent Parsers (Inheritance)
|
||||||
|
|
||||||
|
Share common arguments across subcommands.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Parent parser with common arguments
|
||||||
|
parent_parser = argparse.ArgumentParser(add_help=False)
|
||||||
|
parent_parser.add_argument('--verbose', action='store_true')
|
||||||
|
parent_parser.add_argument('--config', help='Config file')
|
||||||
|
|
||||||
|
# Main parser
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
subparsers = parser.add_subparsers(dest='command')
|
||||||
|
|
||||||
|
# Subcommands inherit from parent
|
||||||
|
deploy_parser = subparsers.add_parser(
|
||||||
|
'deploy',
|
||||||
|
parents=[parent_parser],
|
||||||
|
help='Deploy application'
|
||||||
|
)
|
||||||
|
deploy_parser.add_argument('environment')
|
||||||
|
|
||||||
|
build_parser = subparsers.add_parser(
|
||||||
|
'build',
|
||||||
|
parents=[parent_parser],
|
||||||
|
help='Build application'
|
||||||
|
)
|
||||||
|
build_parser.add_argument('--target')
|
||||||
|
|
||||||
|
# Both subcommands have --verbose and --config
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Argument Defaults from Dict
|
||||||
|
|
||||||
|
```python
|
||||||
|
defaults = {
|
||||||
|
'host': '127.0.0.1',
|
||||||
|
'port': 8080,
|
||||||
|
'workers': 4,
|
||||||
|
'timeout': 30.0
|
||||||
|
}
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--host')
|
||||||
|
parser.add_argument('--port', type=int)
|
||||||
|
parser.add_argument('--workers', type=int)
|
||||||
|
parser.add_argument('--timeout', type=float)
|
||||||
|
|
||||||
|
# Set all defaults at once
|
||||||
|
parser.set_defaults(**defaults)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Namespace Manipulation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Pre-populate namespace
|
||||||
|
defaults = argparse.Namespace(
|
||||||
|
host='127.0.0.1',
|
||||||
|
port=8080,
|
||||||
|
debug=False
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args(namespace=defaults)
|
||||||
|
|
||||||
|
# Or modify after parsing
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.computed_value = args.value1 + args.value2
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Conditional Arguments
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Add conditional validation
|
||||||
|
if args.ssl and not (args.cert and args.key):
|
||||||
|
parser.error("--ssl requires both --cert and --key")
|
||||||
|
|
||||||
|
# Add computed values
|
||||||
|
if args.workers == 'auto':
|
||||||
|
import os
|
||||||
|
args.workers = os.cpu_count()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Advanced Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class KeyValueAction(argparse.Action):
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
key, value = values.split('=', 1)
|
||||||
|
items = getattr(namespace, self.dest, {}) or {}
|
||||||
|
items[key] = value
|
||||||
|
setattr(namespace, self.dest, items)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Parent parser for common args
|
||||||
|
parent = argparse.ArgumentParser(add_help=False)
|
||||||
|
parent.add_argument('--verbose', action='store_true')
|
||||||
|
parent.add_argument('--config', help='Config file')
|
||||||
|
|
||||||
|
# Main parser
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Advanced argparse patterns'
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest='command', required=True)
|
||||||
|
|
||||||
|
# Deploy command
|
||||||
|
deploy = subparsers.add_parser(
|
||||||
|
'deploy',
|
||||||
|
parents=[parent],
|
||||||
|
help='Deploy application'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mutually exclusive group
|
||||||
|
format_group = deploy.add_mutually_exclusive_group()
|
||||||
|
format_group.add_argument('--json', action='store_true')
|
||||||
|
format_group.add_argument('--yaml', action='store_true')
|
||||||
|
|
||||||
|
# Custom action
|
||||||
|
deploy.add_argument(
|
||||||
|
'--env', '-e',
|
||||||
|
action=KeyValueAction,
|
||||||
|
help='Environment variable'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Variadic arguments
|
||||||
|
deploy.add_argument(
|
||||||
|
'targets',
|
||||||
|
nargs='+',
|
||||||
|
help='Deployment targets'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Environment fallback
|
||||||
|
deploy.add_argument(
|
||||||
|
'--api-key',
|
||||||
|
default=os.environ.get('API_KEY'),
|
||||||
|
help='API key'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Post-parse validation
|
||||||
|
if args.command == 'deploy':
|
||||||
|
if not args.api_key:
|
||||||
|
parser.error("API key required (use --api-key or $API_KEY)")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use parent parsers** for shared arguments
|
||||||
|
2. **Use argument groups** for organization
|
||||||
|
3. **Use mutually exclusive groups** when appropriate
|
||||||
|
4. **Validate after parsing** for complex logic
|
||||||
|
5. **Provide environment fallbacks** for sensitive data
|
||||||
|
6. **Use custom actions** for complex transformations
|
||||||
|
7. **Document nargs behavior** in help text
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Review template files for complete implementations
|
||||||
|
- Test patterns with `scripts/test-parser.sh`
|
||||||
|
- Compare with Click/Typer alternatives
|
||||||
230
skills/argparse-patterns/examples/basic-usage.md
Normal file
230
skills/argparse-patterns/examples/basic-usage.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# Basic argparse Usage
|
||||||
|
|
||||||
|
Simple CLI with positional and optional arguments using Python's standard library.
|
||||||
|
|
||||||
|
## Template Reference
|
||||||
|
|
||||||
|
`templates/basic-parser.py`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Demonstrates fundamental argparse patterns:
|
||||||
|
- Positional arguments (required)
|
||||||
|
- Optional arguments with flags
|
||||||
|
- Boolean flags
|
||||||
|
- Type coercion
|
||||||
|
- Default values
|
||||||
|
- Help text generation
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View help
|
||||||
|
python basic-parser.py --help
|
||||||
|
|
||||||
|
# Basic usage
|
||||||
|
python basic-parser.py deploy my-app
|
||||||
|
|
||||||
|
# With optional arguments
|
||||||
|
python basic-parser.py deploy my-app --env staging --timeout 60
|
||||||
|
|
||||||
|
# Boolean flags
|
||||||
|
python basic-parser.py deploy my-app --force
|
||||||
|
|
||||||
|
# Verbose mode (count occurrences)
|
||||||
|
python basic-parser.py deploy my-app -vvv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### 1. Create Parser
|
||||||
|
|
||||||
|
```python
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Deploy application to specified environment',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why `RawDescriptionHelpFormatter`?**
|
||||||
|
- Preserves formatting in epilog (usage examples)
|
||||||
|
- Better control over help text layout
|
||||||
|
|
||||||
|
### 2. Add Version
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version='%(prog)s 1.0.0'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:** `python mycli.py --version`
|
||||||
|
|
||||||
|
### 3. Positional Arguments
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'app_name',
|
||||||
|
help='Name of the application to deploy'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required by default** - no flag needed, just the value.
|
||||||
|
|
||||||
|
### 4. Optional Arguments
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--env', '-e',
|
||||||
|
default='development',
|
||||||
|
help='Deployment environment (default: %(default)s)'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** `%(default)s` automatically shows default value in help.
|
||||||
|
|
||||||
|
### 5. Type Coercion
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--timeout', '-t',
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help='Timeout in seconds'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Automatic validation** - argparse will error if non-integer provided.
|
||||||
|
|
||||||
|
### 6. Boolean Flags
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--force', '-f',
|
||||||
|
action='store_true',
|
||||||
|
help='Force deployment without confirmation'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
- Present: `args.force = True`
|
||||||
|
- Absent: `args.force = False`
|
||||||
|
|
||||||
|
### 7. Count Action
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose', '-v',
|
||||||
|
action='count',
|
||||||
|
default=0,
|
||||||
|
help='Increase verbosity (-v, -vv, -vvv)'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
- `-v`: verbosity = 1
|
||||||
|
- `-vv`: verbosity = 2
|
||||||
|
- `-vvv`: verbosity = 3
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Simple deployment tool'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('--version', action='version', version='1.0.0')
|
||||||
|
|
||||||
|
parser.add_argument('app_name', help='Application name')
|
||||||
|
parser.add_argument('--env', default='dev', help='Environment')
|
||||||
|
parser.add_argument('--timeout', type=int, default=30, help='Timeout')
|
||||||
|
parser.add_argument('--force', action='store_true', help='Force')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"Deploying {args.app_name} to {args.env}")
|
||||||
|
print(f"Timeout: {args.timeout}s")
|
||||||
|
print(f"Force: {args.force}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Help Output
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: basic-parser.py [-h] [--version] [--env ENV] [--timeout TIMEOUT]
|
||||||
|
[--force] [--verbose]
|
||||||
|
action app_name
|
||||||
|
|
||||||
|
Deploy application to specified environment
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
action Action to perform
|
||||||
|
app_name Name of the application to deploy
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--version show program's version number and exit
|
||||||
|
--env ENV, -e ENV Deployment environment (default: development)
|
||||||
|
--timeout TIMEOUT, -t TIMEOUT
|
||||||
|
Timeout in seconds (default: 30)
|
||||||
|
--force, -f Force deployment without confirmation
|
||||||
|
--verbose, -v Increase verbosity (-v, -vv, -vvv)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
### ❌ Wrong: Accessing before parsing
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(args.env) # ✓ Correct
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(args.env) # ✗ Wrong - args doesn't exist yet
|
||||||
|
args = parser.parse_args()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Wrong: Not checking boolean flags
|
||||||
|
|
||||||
|
```python
|
||||||
|
if args.force: # ✓ Correct
|
||||||
|
print("Force mode")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
if args.force == True: # ✗ Unnecessary comparison
|
||||||
|
print("Force mode")
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Wrong: Manual type conversion
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument('--port', type=int) # ✓ Let argparse handle it
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument('--port')
|
||||||
|
port = int(args.port) # ✗ Manual conversion (error-prone)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **Subcommands:** See `subcommands.md`
|
||||||
|
- **Validation:** See `validation-patterns.md`
|
||||||
|
- **Advanced:** See `advanced-parsing.md`
|
||||||
370
skills/argparse-patterns/examples/nested-commands.md
Normal file
370
skills/argparse-patterns/examples/nested-commands.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# Nested Subcommands
|
||||||
|
|
||||||
|
Multi-level command hierarchies like `git config get` or `kubectl config view`.
|
||||||
|
|
||||||
|
## Template Reference
|
||||||
|
|
||||||
|
`templates/nested-subparser.py`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Create deep command structures:
|
||||||
|
- `mycli config get key`
|
||||||
|
- `mycli config set key value`
|
||||||
|
- `mycli deploy start production`
|
||||||
|
- `mycli deploy stop production`
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Two-level commands
|
||||||
|
python nested-subparser.py config get database_url
|
||||||
|
python nested-subparser.py config set api_key abc123
|
||||||
|
python nested-subparser.py config list
|
||||||
|
|
||||||
|
# Deploy subcommands
|
||||||
|
python nested-subparser.py deploy start production --replicas 3
|
||||||
|
python nested-subparser.py deploy stop staging
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
mycli
|
||||||
|
├── config
|
||||||
|
│ ├── get <key>
|
||||||
|
│ ├── set <key> <value>
|
||||||
|
│ ├── list
|
||||||
|
│ └── delete <key>
|
||||||
|
└── deploy
|
||||||
|
├── start <environment>
|
||||||
|
├── stop <environment>
|
||||||
|
└── restart <environment>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Pattern
|
||||||
|
|
||||||
|
### 1. Main Parser
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser = argparse.ArgumentParser(description='Multi-level CLI')
|
||||||
|
subparsers = parser.add_subparsers(dest='command', required=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. First-Level Subcommand
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Create 'config' command group
|
||||||
|
config_parser = subparsers.add_parser(
|
||||||
|
'config',
|
||||||
|
help='Manage configuration'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create second-level subparsers under 'config'
|
||||||
|
config_subparsers = config_parser.add_subparsers(
|
||||||
|
dest='config_command',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Second-Level Subcommands
|
||||||
|
|
||||||
|
```python
|
||||||
|
# config get
|
||||||
|
config_get = config_subparsers.add_parser('get', help='Get value')
|
||||||
|
config_get.add_argument('key', help='Configuration key')
|
||||||
|
config_get.set_defaults(func=config_get_handler)
|
||||||
|
|
||||||
|
# config set
|
||||||
|
config_set = config_subparsers.add_parser('set', help='Set value')
|
||||||
|
config_set.add_argument('key', help='Configuration key')
|
||||||
|
config_set.add_argument('value', help='Configuration value')
|
||||||
|
config_set.add_argument('--force', action='store_true')
|
||||||
|
config_set.set_defaults(func=config_set_handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# Config handlers
|
||||||
|
def config_get(args):
|
||||||
|
print(f"Getting: {args.key}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def config_set(args):
|
||||||
|
print(f"Setting: {args.key} = {args.value}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# Deploy handlers
|
||||||
|
def deploy_start(args):
|
||||||
|
print(f"Starting deployment to {args.environment}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Nested CLI')
|
||||||
|
subparsers = parser.add_subparsers(dest='command', required=True)
|
||||||
|
|
||||||
|
# === Config group ===
|
||||||
|
config_parser = subparsers.add_parser('config', help='Configuration')
|
||||||
|
config_subs = config_parser.add_subparsers(
|
||||||
|
dest='config_command',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# config get
|
||||||
|
get_parser = config_subs.add_parser('get')
|
||||||
|
get_parser.add_argument('key')
|
||||||
|
get_parser.set_defaults(func=config_get)
|
||||||
|
|
||||||
|
# config set
|
||||||
|
set_parser = config_subs.add_parser('set')
|
||||||
|
set_parser.add_argument('key')
|
||||||
|
set_parser.add_argument('value')
|
||||||
|
set_parser.set_defaults(func=config_set)
|
||||||
|
|
||||||
|
# === Deploy group ===
|
||||||
|
deploy_parser = subparsers.add_parser('deploy', help='Deployment')
|
||||||
|
deploy_subs = deploy_parser.add_subparsers(
|
||||||
|
dest='deploy_command',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# deploy start
|
||||||
|
start_parser = deploy_subs.add_parser('start')
|
||||||
|
start_parser.add_argument('environment')
|
||||||
|
start_parser.set_defaults(func=deploy_start)
|
||||||
|
|
||||||
|
# Parse and dispatch
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing Nested Commands
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Top-level command
|
||||||
|
print(args.command) # 'config' or 'deploy'
|
||||||
|
|
||||||
|
# Second-level command
|
||||||
|
if args.command == 'config':
|
||||||
|
print(args.config_command) # 'get', 'set', 'list', 'delete'
|
||||||
|
elif args.command == 'deploy':
|
||||||
|
print(args.deploy_command) # 'start', 'stop', 'restart'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Help Output
|
||||||
|
|
||||||
|
### Top-Level Help
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: mycli [-h] {config,deploy} ...
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{config,deploy}
|
||||||
|
config Manage configuration
|
||||||
|
deploy Manage deployments
|
||||||
|
```
|
||||||
|
|
||||||
|
### Second-Level Help
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ python mycli.py config --help
|
||||||
|
|
||||||
|
usage: mycli config [-h] {get,set,list,delete} ...
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{get,set,list,delete}
|
||||||
|
get Get configuration value
|
||||||
|
set Set configuration value
|
||||||
|
list List all configuration
|
||||||
|
delete Delete configuration value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Third-Level Help
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ python mycli.py config set --help
|
||||||
|
|
||||||
|
usage: mycli config set [-h] [-f] key value
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
key Configuration key
|
||||||
|
value Configuration value
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-f, --force Overwrite existing value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dispatch Pattern
|
||||||
|
|
||||||
|
### Option 1: Manual Switch
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == 'config':
|
||||||
|
if args.config_command == 'get':
|
||||||
|
config_get(args)
|
||||||
|
elif args.config_command == 'set':
|
||||||
|
config_set(args)
|
||||||
|
elif args.command == 'deploy':
|
||||||
|
if args.deploy_command == 'start':
|
||||||
|
deploy_start(args)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Function Dispatch (Recommended)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set handlers when creating parsers
|
||||||
|
config_get.set_defaults(func=config_get_handler)
|
||||||
|
config_set.set_defaults(func=config_set_handler)
|
||||||
|
deploy_start.set_defaults(func=deploy_start_handler)
|
||||||
|
|
||||||
|
# Simple dispatch
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args.func(args)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Consistent Naming
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✓ Good - consistent dest naming
|
||||||
|
config_parser.add_subparsers(dest='config_command')
|
||||||
|
deploy_parser.add_subparsers(dest='deploy_command')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set Required
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✓ Good - require subcommand
|
||||||
|
config_subs = config_parser.add_subparsers(
|
||||||
|
dest='config_command',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Provide Help
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✓ Good - descriptive help at each level
|
||||||
|
config_parser = subparsers.add_parser(
|
||||||
|
'config',
|
||||||
|
help='Manage configuration',
|
||||||
|
description='Configuration management commands'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Use set_defaults
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✓ Good - easy dispatch
|
||||||
|
get_parser.set_defaults(func=config_get)
|
||||||
|
```
|
||||||
|
|
||||||
|
## How Deep Should You Go?
|
||||||
|
|
||||||
|
### ✓ Good: 2-3 Levels
|
||||||
|
|
||||||
|
```
|
||||||
|
mycli config get key
|
||||||
|
mycli deploy start production
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Consider alternatives: 4+ Levels
|
||||||
|
|
||||||
|
```
|
||||||
|
mycli server database config get key # Too deep
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternatives:**
|
||||||
|
- Flatten: `mycli db-config-get key`
|
||||||
|
- Split: Separate CLI tools
|
||||||
|
- Use flags: `mycli config get key --scope=server --type=database`
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
### ❌ Wrong: Same dest name
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Both use 'command' - second overwrites first
|
||||||
|
config_subs = config_parser.add_subparsers(dest='command')
|
||||||
|
deploy_subs = deploy_parser.add_subparsers(dest='command')
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✓ Correct - unique dest names
|
||||||
|
config_subs = config_parser.add_subparsers(dest='config_command')
|
||||||
|
deploy_subs = deploy_parser.add_subparsers(dest='deploy_command')
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Wrong: Accessing wrong level
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args(['config', 'get', 'key'])
|
||||||
|
|
||||||
|
print(args.command) # ✓ 'config'
|
||||||
|
print(args.config_command) # ✓ 'get'
|
||||||
|
print(args.deploy_command) # ✗ Error - not set
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Wrong: Not checking hierarchy
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✗ Assumes deploy command
|
||||||
|
print(args.deploy_command)
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✓ Check first
|
||||||
|
if args.command == 'deploy':
|
||||||
|
print(args.deploy_command)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-World Examples
|
||||||
|
|
||||||
|
### Git-style
|
||||||
|
|
||||||
|
```
|
||||||
|
git config --global user.name "Name"
|
||||||
|
git remote add origin url
|
||||||
|
git branch --list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubectl-style
|
||||||
|
|
||||||
|
```
|
||||||
|
kubectl config view
|
||||||
|
kubectl get pods --namespace default
|
||||||
|
kubectl logs pod-name --follow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker-style
|
||||||
|
|
||||||
|
```
|
||||||
|
docker container ls
|
||||||
|
docker image build -t name .
|
||||||
|
docker network create name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **Validation:** See `validation-patterns.md`
|
||||||
|
- **Advanced:** See `advanced-parsing.md`
|
||||||
|
- **Compare frameworks:** See templates for Click/Typer equivalents
|
||||||
283
skills/argparse-patterns/examples/subcommands.md
Normal file
283
skills/argparse-patterns/examples/subcommands.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
# Subcommands with argparse
|
||||||
|
|
||||||
|
Multi-command CLI like `git`, `docker`, or `kubectl` using subparsers.
|
||||||
|
|
||||||
|
## Template Reference
|
||||||
|
|
||||||
|
`templates/subparser-pattern.py`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Create CLIs with multiple commands:
|
||||||
|
- `mycli init` - Initialize project
|
||||||
|
- `mycli deploy production` - Deploy to environment
|
||||||
|
- `mycli status` - Show status
|
||||||
|
|
||||||
|
Each subcommand has its own arguments and options.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View main help
|
||||||
|
python subparser-pattern.py --help
|
||||||
|
|
||||||
|
# View subcommand help
|
||||||
|
python subparser-pattern.py init --help
|
||||||
|
python subparser-pattern.py deploy --help
|
||||||
|
|
||||||
|
# Execute subcommands
|
||||||
|
python subparser-pattern.py init --template react
|
||||||
|
python subparser-pattern.py deploy production --force
|
||||||
|
python subparser-pattern.py status --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### 1. Create Subparsers
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser = argparse.ArgumentParser(description='Multi-command CLI')
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(
|
||||||
|
dest='command', # Store command name in args.command
|
||||||
|
help='Available commands',
|
||||||
|
required=True # At least one command required (Python 3.7+)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Set `dest='command'` to access which command was used.
|
||||||
|
|
||||||
|
### 2. Add Subcommand
|
||||||
|
|
||||||
|
```python
|
||||||
|
init_parser = subparsers.add_parser(
|
||||||
|
'init',
|
||||||
|
help='Initialize a new project',
|
||||||
|
description='Initialize a new project with specified template'
|
||||||
|
)
|
||||||
|
|
||||||
|
init_parser.add_argument('--template', default='basic')
|
||||||
|
init_parser.add_argument('--path', default='.')
|
||||||
|
```
|
||||||
|
|
||||||
|
Each subcommand is a separate parser with its own arguments.
|
||||||
|
|
||||||
|
### 3. Set Command Handler
|
||||||
|
|
||||||
|
```python
|
||||||
|
def cmd_init(args):
|
||||||
|
"""Initialize project."""
|
||||||
|
print(f"Initializing with {args.template} template...")
|
||||||
|
|
||||||
|
init_parser.set_defaults(func=cmd_init)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dispatch pattern:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args.func(args) # Call the appropriate handler
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Subcommand with Choices
|
||||||
|
|
||||||
|
```python
|
||||||
|
deploy_parser = subparsers.add_parser('deploy')
|
||||||
|
|
||||||
|
deploy_parser.add_argument(
|
||||||
|
'environment',
|
||||||
|
choices=['development', 'staging', 'production'],
|
||||||
|
help='Target environment'
|
||||||
|
)
|
||||||
|
|
||||||
|
deploy_parser.add_argument(
|
||||||
|
'--mode',
|
||||||
|
choices=['fast', 'safe', 'rollback'],
|
||||||
|
default='safe'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_init(args):
|
||||||
|
print(f"Initializing with {args.template} template")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_deploy(args):
|
||||||
|
print(f"Deploying to {args.environment}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='My CLI Tool')
|
||||||
|
parser.add_argument('--version', action='version', version='1.0.0')
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest='command', required=True)
|
||||||
|
|
||||||
|
# Init command
|
||||||
|
init_parser = subparsers.add_parser('init', help='Initialize project')
|
||||||
|
init_parser.add_argument('--template', default='basic')
|
||||||
|
init_parser.set_defaults(func=cmd_init)
|
||||||
|
|
||||||
|
# Deploy command
|
||||||
|
deploy_parser = subparsers.add_parser('deploy', help='Deploy app')
|
||||||
|
deploy_parser.add_argument(
|
||||||
|
'environment',
|
||||||
|
choices=['dev', 'staging', 'prod']
|
||||||
|
)
|
||||||
|
deploy_parser.set_defaults(func=cmd_deploy)
|
||||||
|
|
||||||
|
# Parse and dispatch
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Help Output
|
||||||
|
|
||||||
|
### Main Help
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: mycli [-h] [--version] {init,deploy,status} ...
|
||||||
|
|
||||||
|
Multi-command CLI tool
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{init,deploy,status} Available commands
|
||||||
|
init Initialize a new project
|
||||||
|
deploy Deploy application to environment
|
||||||
|
status Show deployment status
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--version show program's version number and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subcommand Help
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ python mycli.py deploy --help
|
||||||
|
|
||||||
|
usage: mycli deploy [-h] [-f] [-m {fast,safe,rollback}]
|
||||||
|
{development,staging,production}
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{development,staging,production}
|
||||||
|
Target environment
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-f, --force Force deployment without confirmation
|
||||||
|
-m {fast,safe,rollback}, --mode {fast,safe,rollback}
|
||||||
|
Deployment mode (default: safe)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing Parsed Values
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Which command was used?
|
||||||
|
print(args.command) # 'init', 'deploy', or 'status'
|
||||||
|
|
||||||
|
# Command-specific arguments
|
||||||
|
if args.command == 'deploy':
|
||||||
|
print(args.environment) # 'production'
|
||||||
|
print(args.force) # True/False
|
||||||
|
print(args.mode) # 'safe'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Switch on Command
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == 'init':
|
||||||
|
init_project(args)
|
||||||
|
elif args.command == 'deploy':
|
||||||
|
deploy_app(args)
|
||||||
|
elif args.command == 'status':
|
||||||
|
show_status(args)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Function Dispatch (Better)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set handlers
|
||||||
|
init_parser.set_defaults(func=cmd_init)
|
||||||
|
deploy_parser.set_defaults(func=cmd_deploy)
|
||||||
|
|
||||||
|
# Dispatch
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args.func(args)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Check if Command Provided
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Use `required=True` in `add_subparsers()` to make this automatic.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
### ❌ Wrong: Forgetting dest
|
||||||
|
|
||||||
|
```python
|
||||||
|
subparsers = parser.add_subparsers(dest='command') # ✓ Can check args.command
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
subparsers = parser.add_subparsers() # ✗ Can't access which command
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Wrong: Accessing wrong argument
|
||||||
|
|
||||||
|
```python
|
||||||
|
# deploy_parser defines 'environment'
|
||||||
|
# init_parser defines 'template'
|
||||||
|
|
||||||
|
args = parser.parse_args(['deploy', 'prod'])
|
||||||
|
print(args.environment) # ✓ Correct
|
||||||
|
print(args.template) # ✗ Error - not defined for deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Wrong: No required=True (Python 3.7+)
|
||||||
|
|
||||||
|
```python
|
||||||
|
subparsers = parser.add_subparsers(dest='command', required=True) # ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
subparsers = parser.add_subparsers(dest='command') # ✗ Command optional
|
||||||
|
# User can run: python mycli.py (no command)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nested Subcommands
|
||||||
|
|
||||||
|
For multi-level commands like `git config get`, see:
|
||||||
|
- `nested-commands.md`
|
||||||
|
- `templates/nested-subparser.py`
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **Nested Commands:** See `nested-commands.md`
|
||||||
|
- **Validation:** See `validation-patterns.md`
|
||||||
|
- **Complex CLIs:** See `advanced-parsing.md`
|
||||||
424
skills/argparse-patterns/examples/validation-patterns.md
Normal file
424
skills/argparse-patterns/examples/validation-patterns.md
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
# Validation Patterns with argparse
|
||||||
|
|
||||||
|
Custom validators, type checking, and error handling.
|
||||||
|
|
||||||
|
## Template Reference
|
||||||
|
|
||||||
|
`templates/choices-validation.py`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Robust argument validation:
|
||||||
|
- Built-in choices validation
|
||||||
|
- Custom type validators
|
||||||
|
- Range validation
|
||||||
|
- Pattern matching (regex)
|
||||||
|
- File/path validation
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Valid inputs
|
||||||
|
python choices-validation.py --log-level debug --port 8080
|
||||||
|
python choices-validation.py --region us-east-1 --email user@example.com
|
||||||
|
|
||||||
|
# Invalid inputs (will error)
|
||||||
|
python choices-validation.py --log-level invalid # Not in choices
|
||||||
|
python choices-validation.py --port 99999 # Out of range
|
||||||
|
python choices-validation.py --email invalid # Invalid format
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Methods
|
||||||
|
|
||||||
|
### 1. Choices (Built-in)
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
'--log-level',
|
||||||
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
||||||
|
default='info',
|
||||||
|
help='Logging level'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Automatic validation** - argparse rejects invalid values.
|
||||||
|
|
||||||
|
### 2. Custom Type Validator
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_port(value):
|
||||||
|
"""Validate port number is 1-65535."""
|
||||||
|
try:
|
||||||
|
ivalue = int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(f"{value} is not a valid integer")
|
||||||
|
|
||||||
|
if ivalue < 1 or ivalue > 65535:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} is not a valid port (must be 1-65535)"
|
||||||
|
)
|
||||||
|
return ivalue
|
||||||
|
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--port',
|
||||||
|
type=validate_port,
|
||||||
|
default=8080,
|
||||||
|
help='Server port (1-65535)'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Regex Pattern Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
|
||||||
|
def validate_email(value):
|
||||||
|
"""Validate email address format."""
|
||||||
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||||
|
if not re.match(pattern, value):
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} is not a valid email address"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
parser.add_argument('--email', type=validate_email)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. IP Address Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_ip(value):
|
||||||
|
"""Validate IPv4 address."""
|
||||||
|
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||||
|
if not re.match(pattern, value):
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} is not a valid IP address"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check each octet is 0-255
|
||||||
|
octets = [int(x) for x in value.split('.')]
|
||||||
|
if any(o < 0 or o > 255 for o in octets):
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} contains invalid octets"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
parser.add_argument('--host', type=validate_ip)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Path Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def validate_path_exists(value):
|
||||||
|
"""Validate path exists."""
|
||||||
|
path = Path(value)
|
||||||
|
if not path.exists():
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Path does not exist: {value}"
|
||||||
|
)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
parser.add_argument('--config', type=validate_path_exists)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Range Validation Factory
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_range(min_val, max_val):
|
||||||
|
"""Factory function for range validators."""
|
||||||
|
def validator(value):
|
||||||
|
try:
|
||||||
|
ivalue = int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} is not a valid integer"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ivalue < min_val or ivalue > max_val:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} must be between {min_val} and {max_val}"
|
||||||
|
)
|
||||||
|
return ivalue
|
||||||
|
return validator
|
||||||
|
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
parser.add_argument(
|
||||||
|
'--workers',
|
||||||
|
type=validate_range(1, 32),
|
||||||
|
default=4,
|
||||||
|
help='Number of workers (1-32)'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Validation Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def validate_port(value):
|
||||||
|
ivalue = int(value)
|
||||||
|
if not (1 <= ivalue <= 65535):
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Port must be 1-65535, got {value}"
|
||||||
|
)
|
||||||
|
return ivalue
|
||||||
|
|
||||||
|
|
||||||
|
def validate_email(value):
|
||||||
|
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', value):
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Invalid email: {value}"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Validation examples'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Choices
|
||||||
|
parser.add_argument(
|
||||||
|
'--env',
|
||||||
|
choices=['dev', 'staging', 'prod'],
|
||||||
|
required=True,
|
||||||
|
help='Environment (required)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Custom validators
|
||||||
|
parser.add_argument('--port', type=validate_port, default=8080)
|
||||||
|
parser.add_argument('--email', type=validate_email)
|
||||||
|
|
||||||
|
# Path validation
|
||||||
|
parser.add_argument(
|
||||||
|
'--config',
|
||||||
|
type=lambda x: Path(x) if Path(x).exists() else
|
||||||
|
parser.error(f"File not found: {x}"),
|
||||||
|
help='Config file (must exist)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"Environment: {args.env}")
|
||||||
|
print(f"Port: {args.port}")
|
||||||
|
if args.email:
|
||||||
|
print(f"Email: {args.email}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Parse Validation
|
||||||
|
|
||||||
|
Sometimes you need to validate relationships between arguments:
|
||||||
|
|
||||||
|
```python
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Validate argument combinations
|
||||||
|
if args.ssl and not (args.cert and args.key):
|
||||||
|
parser.error("--ssl requires both --cert and --key")
|
||||||
|
|
||||||
|
if args.output and args.output.exists() and not args.force:
|
||||||
|
parser.error(f"Output file exists: {args.output}. Use --force to overwrite")
|
||||||
|
|
||||||
|
# Validate argument ranges
|
||||||
|
if args.start_date > args.end_date:
|
||||||
|
parser.error("Start date must be before end date")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Messages
|
||||||
|
|
||||||
|
### Built-in Error Format
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ python mycli.py --env invalid
|
||||||
|
usage: mycli.py [-h] --env {dev,staging,prod}
|
||||||
|
mycli.py: error: argument --env: invalid choice: 'invalid'
|
||||||
|
(choose from 'dev', 'staging', 'prod')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Error Format
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_port(value):
|
||||||
|
try:
|
||||||
|
ivalue = int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Port must be an integer (got '{value}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ivalue < 1 or ivalue > 65535:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Port {ivalue} is out of range (valid: 1-65535)"
|
||||||
|
)
|
||||||
|
return ivalue
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ python mycli.py --port 99999
|
||||||
|
usage: mycli.py [-h] [--port PORT]
|
||||||
|
mycli.py: error: argument --port: Port 99999 is out of range (valid: 1-65535)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Validation Patterns
|
||||||
|
|
||||||
|
### URL Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
|
||||||
|
def validate_url(value):
|
||||||
|
pattern = r'^https?://[\w\.-]+\.\w+(:\d+)?(/.*)?$'
|
||||||
|
if not re.match(pattern, value):
|
||||||
|
raise argparse.ArgumentTypeError(f"Invalid URL: {value}")
|
||||||
|
return value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Date Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def validate_date(value):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value, '%Y-%m-%d').date()
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Invalid date: {value} (expected YYYY-MM-DD)"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Extension Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_json_file(value):
|
||||||
|
path = Path(value)
|
||||||
|
if path.suffix != '.json':
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"File must have .json extension: {value}"
|
||||||
|
)
|
||||||
|
return path
|
||||||
|
```
|
||||||
|
|
||||||
|
### Percentage Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_percentage(value):
|
||||||
|
try:
|
||||||
|
pct = float(value.rstrip('%'))
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(f"Invalid percentage: {value}")
|
||||||
|
|
||||||
|
if not (0 <= pct <= 100):
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Percentage must be 0-100: {value}"
|
||||||
|
)
|
||||||
|
return pct
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### ✓ Do: Fail Early
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Validate during parsing
|
||||||
|
parser.add_argument('--port', type=validate_port)
|
||||||
|
|
||||||
|
# Not after parsing
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not valid_port(args.port): # ✗ Too late
|
||||||
|
sys.exit(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✓ Do: Provide Clear Messages
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✓ Clear, actionable error
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Port {value} is out of range (valid: 1-65535)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✗ Vague error
|
||||||
|
raise argparse.ArgumentTypeError("Invalid port")
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✓ Do: Use Choices When Possible
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✓ Let argparse handle it
|
||||||
|
parser.add_argument('--env', choices=['dev', 'staging', 'prod'])
|
||||||
|
|
||||||
|
# ✗ Manual validation
|
||||||
|
parser.add_argument('--env')
|
||||||
|
if args.env not in ['dev', 'staging', 'prod']:
|
||||||
|
parser.error("Invalid environment")
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✓ Do: Validate Type Before Range
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_port(value):
|
||||||
|
# First ensure it's an integer
|
||||||
|
try:
|
||||||
|
ivalue = int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(f"Not an integer: {value}")
|
||||||
|
|
||||||
|
# Then check range
|
||||||
|
if not (1 <= ivalue <= 65535):
|
||||||
|
raise argparse.ArgumentTypeError(f"Out of range: {ivalue}")
|
||||||
|
|
||||||
|
return ivalue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from io import StringIO
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_port():
|
||||||
|
"""Test valid port number."""
|
||||||
|
parser = create_parser()
|
||||||
|
args = parser.parse_args(['--port', '8080'])
|
||||||
|
assert args.port == 8080
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_port():
|
||||||
|
"""Test invalid port number."""
|
||||||
|
parser = create_parser()
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
parser.parse_args(['--port', '99999'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_choice():
|
||||||
|
"""Test invalid choice."""
|
||||||
|
parser = create_parser()
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
parser.parse_args(['--env', 'invalid'])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **Advanced Patterns:** See `advanced-parsing.md`
|
||||||
|
- **Type Coercion:** See `templates/type-coercion.py`
|
||||||
|
- **Custom Actions:** See `templates/custom-actions.py`
|
||||||
151
skills/argparse-patterns/scripts/convert-to-click.sh
Executable file
151
skills/argparse-patterns/scripts/convert-to-click.sh
Executable file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Convert argparse code to Click decorators
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Convert argparse parser to Click decorators
|
||||||
|
|
||||||
|
Usage: $(basename "$0") ARGPARSE_FILE [OUTPUT_FILE]
|
||||||
|
|
||||||
|
Performs basic conversion from argparse to Click:
|
||||||
|
- ArgumentParser → @click.group() or @click.command()
|
||||||
|
- add_argument() → @click.option() or @click.argument()
|
||||||
|
- add_subparsers() → @group.command()
|
||||||
|
- choices=[] → type=click.Choice([])
|
||||||
|
- action='store_true' → is_flag=True
|
||||||
|
|
||||||
|
Note: This is a basic converter. Manual refinement may be needed.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") mycli.py mycli_click.py
|
||||||
|
$(basename "$0") basic-parser.py
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARGPARSE_FILE="$1"
|
||||||
|
OUTPUT_FILE="${2:-}"
|
||||||
|
|
||||||
|
if [ ! -f "$ARGPARSE_FILE" ]; then
|
||||||
|
echo "Error: File not found: $ARGPARSE_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Converting argparse to Click: $ARGPARSE_FILE"
|
||||||
|
|
||||||
|
convert_to_click() {
|
||||||
|
cat <<'EOF'
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Converted from argparse to Click
|
||||||
|
|
||||||
|
This is a basic conversion. You may need to adjust:
|
||||||
|
- Argument order and grouping
|
||||||
|
- Type conversions
|
||||||
|
- Custom validators
|
||||||
|
- Error handling
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.version_option(version='1.0.0')
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
"""CLI tool converted from argparse"""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
|
||||||
|
|
||||||
|
# Convert your subcommands here
|
||||||
|
# Example pattern:
|
||||||
|
#
|
||||||
|
# @cli.command()
|
||||||
|
# @click.argument('target')
|
||||||
|
# @click.option('--env', type=click.Choice(['dev', 'staging', 'prod']), default='dev')
|
||||||
|
# @click.option('--force', is_flag=True, help='Force operation')
|
||||||
|
# def deploy(target, env, force):
|
||||||
|
# """Deploy to environment"""
|
||||||
|
# click.echo(f"Deploying {target} to {env}")
|
||||||
|
# if force:
|
||||||
|
# click.echo("Force mode enabled")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cli()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "# Detected argparse patterns:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Detect subcommands
|
||||||
|
if grep -q "add_subparsers(" "$ARGPARSE_FILE"; then
|
||||||
|
echo "# Subcommands found:"
|
||||||
|
grep -oP "add_parser\('\K[^']+(?=')" "$ARGPARSE_FILE" | while read -r cmd; do
|
||||||
|
echo "# - $cmd"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect arguments
|
||||||
|
if grep -q "add_argument(" "$ARGPARSE_FILE"; then
|
||||||
|
echo "# Arguments found:"
|
||||||
|
grep "add_argument(" "$ARGPARSE_FILE" | grep -oP "'[^']+'" | head -n1 | while read -r arg; do
|
||||||
|
echo "# $arg"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect choices
|
||||||
|
if grep -q "choices=" "$ARGPARSE_FILE"; then
|
||||||
|
echo "# Choices found (convert to click.Choice):"
|
||||||
|
grep -oP "choices=\[\K[^\]]+(?=\])" "$ARGPARSE_FILE" | while read -r choices; do
|
||||||
|
echo "# [$choices]"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Provide conversion hints
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
# Conversion Guide:
|
||||||
|
#
|
||||||
|
# argparse → Click
|
||||||
|
# ----------------------------------|--------------------------------
|
||||||
|
# parser.add_argument('arg') → @click.argument('arg')
|
||||||
|
# parser.add_argument('--opt') → @click.option('--opt')
|
||||||
|
# action='store_true' → is_flag=True
|
||||||
|
# choices=['a', 'b'] → type=click.Choice(['a', 'b'])
|
||||||
|
# type=int → type=int
|
||||||
|
# required=True → required=True
|
||||||
|
# default='value' → default='value'
|
||||||
|
# help='...' → help='...'
|
||||||
|
#
|
||||||
|
# For nested subcommands:
|
||||||
|
# Use @group.command() decorator
|
||||||
|
#
|
||||||
|
# For more info: https://click.palletsprojects.com/
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Output
|
||||||
|
if [ -n "$OUTPUT_FILE" ]; then
|
||||||
|
convert_to_click > "$OUTPUT_FILE"
|
||||||
|
chmod +x "$OUTPUT_FILE"
|
||||||
|
echo "Converted to Click: $OUTPUT_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Review the generated file"
|
||||||
|
echo " 2. Add your command implementations"
|
||||||
|
echo " 3. Install Click: pip install click"
|
||||||
|
echo " 4. Test: python $OUTPUT_FILE --help"
|
||||||
|
else
|
||||||
|
convert_to_click
|
||||||
|
fi
|
||||||
213
skills/argparse-patterns/scripts/generate-parser.sh
Executable file
213
skills/argparse-patterns/scripts/generate-parser.sh
Executable file
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate argparse parser from specification
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TEMPLATES_DIR="$(dirname "$SCRIPT_DIR")/templates"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Generate argparse parser from specification
|
||||||
|
|
||||||
|
Usage: $(basename "$0") [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-n, --name NAME Parser name (required)
|
||||||
|
-d, --description DESC Parser description
|
||||||
|
-s, --subcommands Include subcommands
|
||||||
|
-c, --choices Include choice validation
|
||||||
|
-g, --groups Include argument groups
|
||||||
|
-o, --output FILE Output file (default: stdout)
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") -n mycli -d "My CLI tool" -o mycli.py
|
||||||
|
$(basename "$0") -n deploy -s -c -o deploy.py
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
NAME=""
|
||||||
|
DESCRIPTION=""
|
||||||
|
SUBCOMMANDS=false
|
||||||
|
CHOICES=false
|
||||||
|
GROUPS=false
|
||||||
|
OUTPUT=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-n|--name)
|
||||||
|
NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-d|--description)
|
||||||
|
DESCRIPTION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-s|--subcommands)
|
||||||
|
SUBCOMMANDS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-c|--choices)
|
||||||
|
CHOICES=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-g|--groups)
|
||||||
|
GROUPS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-o|--output)
|
||||||
|
OUTPUT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown option $1"
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$NAME" ]; then
|
||||||
|
echo "Error: --name is required"
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set defaults
|
||||||
|
DESCRIPTION="${DESCRIPTION:-$NAME CLI tool}"
|
||||||
|
|
||||||
|
# Generate parser
|
||||||
|
generate_parser() {
|
||||||
|
cat <<EOF
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
$DESCRIPTION
|
||||||
|
|
||||||
|
Generated by generate-parser.sh
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='$DESCRIPTION',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version='1.0.0'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose', '-v',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable verbose output'
|
||||||
|
)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$GROUPS" = true ]; then
|
||||||
|
cat <<EOF
|
||||||
|
# Configuration group
|
||||||
|
config_group = parser.add_argument_group(
|
||||||
|
'configuration',
|
||||||
|
'Configuration options'
|
||||||
|
)
|
||||||
|
|
||||||
|
config_group.add_argument(
|
||||||
|
'--config',
|
||||||
|
help='Configuration file'
|
||||||
|
)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SUBCOMMANDS" = true ]; then
|
||||||
|
cat <<EOF
|
||||||
|
# Create subparsers
|
||||||
|
subparsers = parser.add_subparsers(
|
||||||
|
dest='command',
|
||||||
|
help='Available commands',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Example subcommand
|
||||||
|
cmd_parser = subparsers.add_parser(
|
||||||
|
'run',
|
||||||
|
help='Run the application'
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd_parser.add_argument(
|
||||||
|
'target',
|
||||||
|
help='Target to run'
|
||||||
|
)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$CHOICES" = true ]; then
|
||||||
|
cat <<EOF
|
||||||
|
cmd_parser.add_argument(
|
||||||
|
'--env',
|
||||||
|
choices=['development', 'staging', 'production'],
|
||||||
|
default='development',
|
||||||
|
help='Environment (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
cat <<EOF
|
||||||
|
# Arguments
|
||||||
|
parser.add_argument(
|
||||||
|
'target',
|
||||||
|
help='Target to process'
|
||||||
|
)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$CHOICES" = true ]; then
|
||||||
|
cat <<EOF
|
||||||
|
parser.add_argument(
|
||||||
|
'--env',
|
||||||
|
choices=['development', 'staging', 'production'],
|
||||||
|
default='development',
|
||||||
|
help='Environment (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Display configuration
|
||||||
|
if args.verbose:
|
||||||
|
print("Verbose mode enabled")
|
||||||
|
print(f"Arguments: {args}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Output
|
||||||
|
if [ -n "$OUTPUT" ]; then
|
||||||
|
generate_parser > "$OUTPUT"
|
||||||
|
chmod +x "$OUTPUT"
|
||||||
|
echo "Generated parser: $OUTPUT"
|
||||||
|
else
|
||||||
|
generate_parser
|
||||||
|
fi
|
||||||
149
skills/argparse-patterns/scripts/test-parser.sh
Executable file
149
skills/argparse-patterns/scripts/test-parser.sh
Executable file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Test argparse parser with various argument combinations
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Test argparse parser with various arguments
|
||||||
|
|
||||||
|
Usage: $(basename "$0") PARSER_FILE
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- Help display (--help)
|
||||||
|
- Version display (--version)
|
||||||
|
- Missing required arguments
|
||||||
|
- Invalid choices
|
||||||
|
- Type validation
|
||||||
|
- Subcommands (if present)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") mycli.py
|
||||||
|
$(basename "$0") ../templates/basic-parser.py
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
PARSER_FILE="$1"
|
||||||
|
|
||||||
|
if [ ! -f "$PARSER_FILE" ]; then
|
||||||
|
echo "Error: File not found: $PARSER_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make executable if needed
|
||||||
|
if [ ! -x "$PARSER_FILE" ]; then
|
||||||
|
chmod +x "$PARSER_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Testing argparse parser: $PARSER_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
PASSED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
local description="$1"
|
||||||
|
shift
|
||||||
|
local expected_result="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
echo -n "Testing: $description ... "
|
||||||
|
|
||||||
|
if "$PARSER_FILE" "$@" >/dev/null 2>&1; then
|
||||||
|
result="success"
|
||||||
|
else
|
||||||
|
result="failure"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$result" = "$expected_result" ]; then
|
||||||
|
echo "✓ PASS"
|
||||||
|
((PASSED++))
|
||||||
|
else
|
||||||
|
echo "✗ FAIL (expected $expected_result, got $result)"
|
||||||
|
((FAILED++))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test --help
|
||||||
|
run_test "Help display" "success" --help
|
||||||
|
|
||||||
|
# Test --version
|
||||||
|
if grep -q "action='version'" "$PARSER_FILE"; then
|
||||||
|
run_test "Version display" "success" --version
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test with no arguments
|
||||||
|
run_test "No arguments" "failure"
|
||||||
|
|
||||||
|
# Test invalid option
|
||||||
|
run_test "Invalid option" "failure" --invalid-option
|
||||||
|
|
||||||
|
# Detect and test subcommands
|
||||||
|
if grep -q "add_subparsers(" "$PARSER_FILE"; then
|
||||||
|
echo ""
|
||||||
|
echo "Subcommands detected, testing subcommand patterns..."
|
||||||
|
|
||||||
|
# Try to extract subcommand names
|
||||||
|
subcommands=$(grep -oP "add_parser\('\K[^']+(?=')" "$PARSER_FILE" || true)
|
||||||
|
|
||||||
|
if [ -n "$subcommands" ]; then
|
||||||
|
for cmd in $subcommands; do
|
||||||
|
run_test "Subcommand: $cmd --help" "success" "$cmd" --help
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test choices if present
|
||||||
|
if grep -q "choices=\[" "$PARSER_FILE"; then
|
||||||
|
echo ""
|
||||||
|
echo "Choices validation detected, testing..."
|
||||||
|
|
||||||
|
# Extract valid and invalid choices
|
||||||
|
valid_choice=$(grep -oP "choices=\[\s*'([^']+)" "$PARSER_FILE" | head -n1 | grep -oP "'[^']+'" | tr -d "'" || echo "valid")
|
||||||
|
invalid_choice="invalid_choice_12345"
|
||||||
|
|
||||||
|
if grep -q "add_subparsers(" "$PARSER_FILE" && [ -n "$subcommands" ]; then
|
||||||
|
first_cmd=$(echo "$subcommands" | head -n1)
|
||||||
|
run_test "Valid choice" "success" "$first_cmd" target --env "$valid_choice" 2>/dev/null || true
|
||||||
|
run_test "Invalid choice" "failure" "$first_cmd" target --env "$invalid_choice" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test type validation if present
|
||||||
|
if grep -q "type=int" "$PARSER_FILE"; then
|
||||||
|
echo ""
|
||||||
|
echo "Type validation detected, testing..."
|
||||||
|
|
||||||
|
run_test "Valid integer" "success" --port 8080 2>/dev/null || true
|
||||||
|
run_test "Invalid integer" "failure" --port invalid 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test boolean flags if present
|
||||||
|
if grep -q "action='store_true'" "$PARSER_FILE"; then
|
||||||
|
echo ""
|
||||||
|
echo "Boolean flags detected, testing..."
|
||||||
|
|
||||||
|
run_test "Boolean flag present" "success" --verbose 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "Test Summary:"
|
||||||
|
echo " Passed: $PASSED"
|
||||||
|
echo " Failed: $FAILED"
|
||||||
|
echo " Total: $((PASSED + FAILED))"
|
||||||
|
|
||||||
|
if [ $FAILED -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✓ All tests passed"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "✗ Some tests failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
173
skills/argparse-patterns/scripts/validate-parser.sh
Executable file
173
skills/argparse-patterns/scripts/validate-parser.sh
Executable file
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Validate argparse parser structure and completeness
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Validate argparse parser structure
|
||||||
|
|
||||||
|
Usage: $(basename "$0") PARSER_FILE
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
- Valid Python syntax
|
||||||
|
- Imports argparse
|
||||||
|
- Creates ArgumentParser
|
||||||
|
- Has main() function
|
||||||
|
- Calls parse_args()
|
||||||
|
- Has proper shebang
|
||||||
|
- Has help text
|
||||||
|
- Has version info
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") mycli.py
|
||||||
|
$(basename "$0") ../templates/basic-parser.py
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
PARSER_FILE="$1"
|
||||||
|
|
||||||
|
if [ ! -f "$PARSER_FILE" ]; then
|
||||||
|
echo "Error: File not found: $PARSER_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Validating argparse parser: $PARSER_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Check shebang
|
||||||
|
if head -n1 "$PARSER_FILE" | grep -q '^#!/usr/bin/env python'; then
|
||||||
|
echo "✓ Has proper Python shebang"
|
||||||
|
else
|
||||||
|
echo "✗ Missing or invalid shebang"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check syntax
|
||||||
|
if python3 -m py_compile "$PARSER_FILE" 2>/dev/null; then
|
||||||
|
echo "✓ Valid Python syntax"
|
||||||
|
else
|
||||||
|
echo "✗ Invalid Python syntax"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check imports
|
||||||
|
if grep -q "import argparse" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Imports argparse"
|
||||||
|
else
|
||||||
|
echo "✗ Does not import argparse"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check ArgumentParser creation
|
||||||
|
if grep -q "ArgumentParser(" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Creates ArgumentParser"
|
||||||
|
else
|
||||||
|
echo "✗ Does not create ArgumentParser"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check main function
|
||||||
|
if grep -q "^def main(" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Has main() function"
|
||||||
|
else
|
||||||
|
echo "⚠ No main() function found"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check parse_args call
|
||||||
|
if grep -q "\.parse_args()" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Calls parse_args()"
|
||||||
|
else
|
||||||
|
echo "✗ Does not call parse_args()"
|
||||||
|
((ERRORS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check version
|
||||||
|
if grep -q "action='version'" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Has version info"
|
||||||
|
else
|
||||||
|
echo "⚠ No version info found"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check help text
|
||||||
|
if grep -q "help=" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Has help text for arguments"
|
||||||
|
else
|
||||||
|
echo "⚠ No help text found"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check description
|
||||||
|
if grep -q "description=" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Has parser description"
|
||||||
|
else
|
||||||
|
echo "⚠ No parser description"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if executable
|
||||||
|
if [ -x "$PARSER_FILE" ]; then
|
||||||
|
echo "✓ File is executable"
|
||||||
|
else
|
||||||
|
echo "⚠ File is not executable (run: chmod +x $PARSER_FILE)"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check subparsers if present
|
||||||
|
if grep -q "add_subparsers(" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Has subparsers"
|
||||||
|
|
||||||
|
# Check if dest is set
|
||||||
|
if grep -q "add_subparsers(.*dest=" "$PARSER_FILE"; then
|
||||||
|
echo " ✓ Subparsers have dest set"
|
||||||
|
else
|
||||||
|
echo " ⚠ Subparsers missing dest parameter"
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for choices
|
||||||
|
if grep -q "choices=" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Uses choices for validation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for type coercion
|
||||||
|
if grep -q "type=" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Uses type coercion"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for argument groups
|
||||||
|
if grep -q "add_argument_group(" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Uses argument groups"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for mutually exclusive groups
|
||||||
|
if grep -q "add_mutually_exclusive_group(" "$PARSER_FILE"; then
|
||||||
|
echo "✓ Uses mutually exclusive groups"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "Validation Summary:"
|
||||||
|
echo " Errors: $ERRORS"
|
||||||
|
echo " Warnings: $WARNINGS"
|
||||||
|
|
||||||
|
if [ $ERRORS -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✓ Parser validation passed"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "✗ Parser validation failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
201
skills/argparse-patterns/templates/argparse-to-commander.ts
Normal file
201
skills/argparse-patterns/templates/argparse-to-commander.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* argparse patterns translated to commander.js
|
||||||
|
*
|
||||||
|
* Shows equivalent patterns between Python argparse and Node.js commander
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npm install commander
|
||||||
|
* node argparse-to-commander.ts deploy production --force
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Command, Option } from 'commander';
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
// ===== Basic Configuration (like ArgumentParser) =====
|
||||||
|
program
|
||||||
|
.name('mycli')
|
||||||
|
.description('A powerful CLI tool')
|
||||||
|
.version('1.0.0');
|
||||||
|
|
||||||
|
// ===== Subcommands (like add_subparsers) =====
|
||||||
|
|
||||||
|
// Init command (like subparsers.add_parser('init'))
|
||||||
|
program
|
||||||
|
.command('init')
|
||||||
|
.description('Initialize a new project')
|
||||||
|
.option('-t, --template <type>', 'project template', 'basic')
|
||||||
|
.option('-p, --path <path>', 'project path', '.')
|
||||||
|
.action((options) => {
|
||||||
|
console.log(`Initializing project with ${options.template} template...`);
|
||||||
|
console.log(`Path: ${options.path}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deploy command with choices (like choices=[...])
|
||||||
|
program
|
||||||
|
.command('deploy <environment>')
|
||||||
|
.description('Deploy to specified environment')
|
||||||
|
.addOption(
|
||||||
|
new Option('-m, --mode <mode>', 'deployment mode')
|
||||||
|
.choices(['fast', 'safe', 'rollback'])
|
||||||
|
.default('safe')
|
||||||
|
)
|
||||||
|
.option('-f, --force', 'force deployment', false)
|
||||||
|
.action((environment, options) => {
|
||||||
|
console.log(`Deploying to ${environment} in ${options.mode} mode`);
|
||||||
|
if (options.force) {
|
||||||
|
console.log('Warning: Force mode enabled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Nested Subcommands (like nested add_subparsers) =====
|
||||||
|
const config = program
|
||||||
|
.command('config')
|
||||||
|
.description('Manage configuration');
|
||||||
|
|
||||||
|
config
|
||||||
|
.command('get <key>')
|
||||||
|
.description('Get configuration value')
|
||||||
|
.action((key) => {
|
||||||
|
console.log(`Getting config: ${key}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
config
|
||||||
|
.command('set <key> <value>')
|
||||||
|
.description('Set configuration value')
|
||||||
|
.option('-f, --force', 'overwrite existing value')
|
||||||
|
.action((key, value, options) => {
|
||||||
|
console.log(`Setting ${key} = ${value}`);
|
||||||
|
if (options.force) {
|
||||||
|
console.log('(Overwriting existing value)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
config
|
||||||
|
.command('list')
|
||||||
|
.description('List all configuration values')
|
||||||
|
.option('--format <format>', 'output format', 'text')
|
||||||
|
.action((options) => {
|
||||||
|
console.log(`Listing configuration (format: ${options.format})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Boolean Flags (like action='store_true') =====
|
||||||
|
program
|
||||||
|
.command('build')
|
||||||
|
.description('Build the project')
|
||||||
|
.option('--verbose', 'enable verbose output')
|
||||||
|
.option('--debug', 'enable debug mode')
|
||||||
|
.option('--no-cache', 'disable cache (enabled by default)')
|
||||||
|
.action((options) => {
|
||||||
|
console.log('Building project...');
|
||||||
|
console.log(`Verbose: ${options.verbose || false}`);
|
||||||
|
console.log(`Debug: ${options.debug || false}`);
|
||||||
|
console.log(`Cache: ${options.cache}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Type Coercion (like type=int, type=float) =====
|
||||||
|
program
|
||||||
|
.command('server')
|
||||||
|
.description('Start server')
|
||||||
|
.option('-p, --port <number>', 'server port', parseInt, 8080)
|
||||||
|
.option('-t, --timeout <seconds>', 'timeout in seconds', parseFloat, 30.0)
|
||||||
|
.option('-w, --workers <number>', 'number of workers', parseInt, 4)
|
||||||
|
.action((options) => {
|
||||||
|
console.log(`Starting server on port ${options.port}`);
|
||||||
|
console.log(`Timeout: ${options.timeout}s`);
|
||||||
|
console.log(`Workers: ${options.workers}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Variadic Arguments (like nargs='+') =====
|
||||||
|
program
|
||||||
|
.command('process <files...>')
|
||||||
|
.description('Process multiple files')
|
||||||
|
.option('--format <format>', 'output format', 'json')
|
||||||
|
.action((files, options) => {
|
||||||
|
console.log(`Processing ${files.length} file(s):`);
|
||||||
|
files.forEach((file) => console.log(` - ${file}`));
|
||||||
|
console.log(`Output format: ${options.format}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Mutually Exclusive Options =====
|
||||||
|
// Note: Commander doesn't have built-in mutually exclusive groups
|
||||||
|
// You need to validate manually
|
||||||
|
program
|
||||||
|
.command('export')
|
||||||
|
.description('Export data')
|
||||||
|
.option('--json <file>', 'export as JSON')
|
||||||
|
.option('--yaml <file>', 'export as YAML')
|
||||||
|
.option('--xml <file>', 'export as XML')
|
||||||
|
.action((options) => {
|
||||||
|
const formats = [options.json, options.yaml, options.xml].filter(Boolean);
|
||||||
|
if (formats.length > 1) {
|
||||||
|
console.error('Error: --json, --yaml, and --xml are mutually exclusive');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.json) {
|
||||||
|
console.log(`Exporting as JSON to ${options.json}`);
|
||||||
|
} else if (options.yaml) {
|
||||||
|
console.log(`Exporting as YAML to ${options.yaml}`);
|
||||||
|
} else if (options.xml) {
|
||||||
|
console.log(`Exporting as XML to ${options.xml}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Required Options (like required=True) =====
|
||||||
|
program
|
||||||
|
.command('login')
|
||||||
|
.description('Login to service')
|
||||||
|
.requiredOption('--username <username>', 'username for authentication')
|
||||||
|
.requiredOption('--password <password>', 'password for authentication')
|
||||||
|
.option('--token <token>', 'authentication token (alternative to password)')
|
||||||
|
.action((options) => {
|
||||||
|
console.log(`Logging in as ${options.username}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Custom Validation =====
|
||||||
|
function validatePort(value: string): number {
|
||||||
|
const port = parseInt(value, 10);
|
||||||
|
if (isNaN(port) || port < 1 || port > 65535) {
|
||||||
|
throw new Error(`Invalid port: ${value} (must be 1-65535)`);
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('connect')
|
||||||
|
.description('Connect to server')
|
||||||
|
.option('-p, --port <number>', 'server port', validatePort, 8080)
|
||||||
|
.action((options) => {
|
||||||
|
console.log(`Connecting to port ${options.port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Argument Groups (display organization) =====
|
||||||
|
// Note: Commander doesn't have argument groups for help display
|
||||||
|
// You can organize with comments or separate commands
|
||||||
|
|
||||||
|
// ===== Parse Arguments =====
|
||||||
|
program.parse();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COMPARISON SUMMARY:
|
||||||
|
*
|
||||||
|
* argparse Pattern | commander.js Equivalent
|
||||||
|
* ---------------------------------|--------------------------------
|
||||||
|
* ArgumentParser() | new Command()
|
||||||
|
* add_argument() | .option() or .argument()
|
||||||
|
* add_subparsers() | .command()
|
||||||
|
* choices=[...] | .choices([...])
|
||||||
|
* action='store_true' | .option('--flag')
|
||||||
|
* action='store_false' | .option('--no-flag')
|
||||||
|
* type=int | parseInt
|
||||||
|
* type=float | parseFloat
|
||||||
|
* nargs='+' | <arg...>
|
||||||
|
* nargs='*' | [arg...]
|
||||||
|
* required=True | .requiredOption()
|
||||||
|
* default=value | option(..., default)
|
||||||
|
* help='...' | .description('...')
|
||||||
|
* mutually_exclusive_group() | Manual validation
|
||||||
|
* add_argument_group() | Organize with subcommands
|
||||||
|
*/
|
||||||
243
skills/argparse-patterns/templates/argument-groups.py
Executable file
243
skills/argparse-patterns/templates/argument-groups.py
Executable file
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Argument groups for better organization and help output.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python argument-groups.py --host 192.168.1.1 --port 8080 --ssl
|
||||||
|
python argument-groups.py --db-host localhost --db-port 5432 --db-name mydb
|
||||||
|
python argument-groups.py --log-level debug --log-file app.log
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Organized arguments with groups',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Server Configuration Group =====
|
||||||
|
server_group = parser.add_argument_group(
|
||||||
|
'server configuration',
|
||||||
|
'Options for configuring the web server'
|
||||||
|
)
|
||||||
|
|
||||||
|
server_group.add_argument(
|
||||||
|
'--host',
|
||||||
|
default='127.0.0.1',
|
||||||
|
help='Server host address (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
server_group.add_argument(
|
||||||
|
'--port', '-p',
|
||||||
|
type=int,
|
||||||
|
default=8080,
|
||||||
|
help='Server port (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
server_group.add_argument(
|
||||||
|
'--workers',
|
||||||
|
type=int,
|
||||||
|
default=4,
|
||||||
|
help='Number of worker processes (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
server_group.add_argument(
|
||||||
|
'--ssl',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable SSL/TLS'
|
||||||
|
)
|
||||||
|
|
||||||
|
server_group.add_argument(
|
||||||
|
'--cert',
|
||||||
|
help='Path to SSL certificate (required if --ssl is set)'
|
||||||
|
)
|
||||||
|
|
||||||
|
server_group.add_argument(
|
||||||
|
'--key',
|
||||||
|
help='Path to SSL private key (required if --ssl is set)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Database Configuration Group =====
|
||||||
|
db_group = parser.add_argument_group(
|
||||||
|
'database configuration',
|
||||||
|
'Options for database connection'
|
||||||
|
)
|
||||||
|
|
||||||
|
db_group.add_argument(
|
||||||
|
'--db-host',
|
||||||
|
default='localhost',
|
||||||
|
help='Database host (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
db_group.add_argument(
|
||||||
|
'--db-port',
|
||||||
|
type=int,
|
||||||
|
default=5432,
|
||||||
|
help='Database port (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
db_group.add_argument(
|
||||||
|
'--db-name',
|
||||||
|
required=True,
|
||||||
|
help='Database name (required)'
|
||||||
|
)
|
||||||
|
|
||||||
|
db_group.add_argument(
|
||||||
|
'--db-user',
|
||||||
|
help='Database username'
|
||||||
|
)
|
||||||
|
|
||||||
|
db_group.add_argument(
|
||||||
|
'--db-password',
|
||||||
|
help='Database password'
|
||||||
|
)
|
||||||
|
|
||||||
|
db_group.add_argument(
|
||||||
|
'--db-pool-size',
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
help='Database connection pool size (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Logging Configuration Group =====
|
||||||
|
log_group = parser.add_argument_group(
|
||||||
|
'logging configuration',
|
||||||
|
'Options for logging and monitoring'
|
||||||
|
)
|
||||||
|
|
||||||
|
log_group.add_argument(
|
||||||
|
'--log-level',
|
||||||
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
||||||
|
default='info',
|
||||||
|
help='Logging level (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
log_group.add_argument(
|
||||||
|
'--log-file',
|
||||||
|
help='Log to file instead of stdout'
|
||||||
|
)
|
||||||
|
|
||||||
|
log_group.add_argument(
|
||||||
|
'--log-format',
|
||||||
|
choices=['text', 'json'],
|
||||||
|
default='text',
|
||||||
|
help='Log format (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
log_group.add_argument(
|
||||||
|
'--access-log',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable access logging'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Cache Configuration Group =====
|
||||||
|
cache_group = parser.add_argument_group(
|
||||||
|
'cache configuration',
|
||||||
|
'Options for caching layer'
|
||||||
|
)
|
||||||
|
|
||||||
|
cache_group.add_argument(
|
||||||
|
'--cache-backend',
|
||||||
|
choices=['redis', 'memcached', 'memory'],
|
||||||
|
default='memory',
|
||||||
|
help='Cache backend (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
cache_group.add_argument(
|
||||||
|
'--cache-host',
|
||||||
|
default='localhost',
|
||||||
|
help='Cache server host (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
cache_group.add_argument(
|
||||||
|
'--cache-port',
|
||||||
|
type=int,
|
||||||
|
default=6379,
|
||||||
|
help='Cache server port (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
cache_group.add_argument(
|
||||||
|
'--cache-ttl',
|
||||||
|
type=int,
|
||||||
|
default=300,
|
||||||
|
help='Default cache TTL in seconds (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Security Configuration Group =====
|
||||||
|
security_group = parser.add_argument_group(
|
||||||
|
'security configuration',
|
||||||
|
'Security and authentication options'
|
||||||
|
)
|
||||||
|
|
||||||
|
security_group.add_argument(
|
||||||
|
'--auth-required',
|
||||||
|
action='store_true',
|
||||||
|
help='Require authentication for all requests'
|
||||||
|
)
|
||||||
|
|
||||||
|
security_group.add_argument(
|
||||||
|
'--jwt-secret',
|
||||||
|
help='JWT secret key'
|
||||||
|
)
|
||||||
|
|
||||||
|
security_group.add_argument(
|
||||||
|
'--cors-origins',
|
||||||
|
nargs='+',
|
||||||
|
help='Allowed CORS origins'
|
||||||
|
)
|
||||||
|
|
||||||
|
security_group.add_argument(
|
||||||
|
'--rate-limit',
|
||||||
|
type=int,
|
||||||
|
default=100,
|
||||||
|
help='Rate limit (requests per minute, default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Validate SSL configuration
|
||||||
|
if args.ssl and (not args.cert or not args.key):
|
||||||
|
parser.error("--cert and --key are required when --ssl is enabled")
|
||||||
|
|
||||||
|
# Display configuration
|
||||||
|
print("Configuration Summary:")
|
||||||
|
|
||||||
|
print("\nServer:")
|
||||||
|
print(f" Host: {args.host}:{args.port}")
|
||||||
|
print(f" Workers: {args.workers}")
|
||||||
|
print(f" SSL: {'Enabled' if args.ssl else 'Disabled'}")
|
||||||
|
if args.ssl:
|
||||||
|
print(f" Certificate: {args.cert}")
|
||||||
|
print(f" Key: {args.key}")
|
||||||
|
|
||||||
|
print("\nDatabase:")
|
||||||
|
print(f" Host: {args.db_host}:{args.db_port}")
|
||||||
|
print(f" Database: {args.db_name}")
|
||||||
|
print(f" User: {args.db_user or '(not set)'}")
|
||||||
|
print(f" Pool Size: {args.db_pool_size}")
|
||||||
|
|
||||||
|
print("\nLogging:")
|
||||||
|
print(f" Level: {args.log_level}")
|
||||||
|
print(f" File: {args.log_file or 'stdout'}")
|
||||||
|
print(f" Format: {args.log_format}")
|
||||||
|
print(f" Access Log: {'Enabled' if args.access_log else 'Disabled'}")
|
||||||
|
|
||||||
|
print("\nCache:")
|
||||||
|
print(f" Backend: {args.cache_backend}")
|
||||||
|
print(f" Host: {args.cache_host}:{args.cache_port}")
|
||||||
|
print(f" TTL: {args.cache_ttl}s")
|
||||||
|
|
||||||
|
print("\nSecurity:")
|
||||||
|
print(f" Auth Required: {'Yes' if args.auth_required else 'No'}")
|
||||||
|
print(f" CORS Origins: {args.cors_origins or '(not set)'}")
|
||||||
|
print(f" Rate Limit: {args.rate_limit} req/min")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
93
skills/argparse-patterns/templates/basic-parser.py
Executable file
93
skills/argparse-patterns/templates/basic-parser.py
Executable file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Basic argparse parser with common argument types.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python basic-parser.py --help
|
||||||
|
python basic-parser.py deploy app1 --env production --force
|
||||||
|
python basic-parser.py deploy app2 --env staging --timeout 60
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Deploy application to specified environment',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog='''
|
||||||
|
Examples:
|
||||||
|
%(prog)s deploy my-app --env production
|
||||||
|
%(prog)s deploy my-app --env staging --force
|
||||||
|
%(prog)s deploy my-app --env dev --timeout 120
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
# Version info
|
||||||
|
parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version='%(prog)s 1.0.0'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Required positional argument
|
||||||
|
parser.add_argument(
|
||||||
|
'action',
|
||||||
|
help='Action to perform'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'app_name',
|
||||||
|
help='Name of the application to deploy'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional arguments with different types
|
||||||
|
parser.add_argument(
|
||||||
|
'--env', '-e',
|
||||||
|
default='development',
|
||||||
|
help='Deployment environment (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--timeout', '-t',
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help='Timeout in seconds (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Boolean flag
|
||||||
|
parser.add_argument(
|
||||||
|
'--force', '-f',
|
||||||
|
action='store_true',
|
||||||
|
help='Force deployment without confirmation'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verbose flag (count occurrences)
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose', '-v',
|
||||||
|
action='count',
|
||||||
|
default=0,
|
||||||
|
help='Increase verbosity (-v, -vv, -vvv)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Use parsed arguments
|
||||||
|
print(f"Action: {args.action}")
|
||||||
|
print(f"App Name: {args.app_name}")
|
||||||
|
print(f"Environment: {args.env}")
|
||||||
|
print(f"Timeout: {args.timeout}s")
|
||||||
|
print(f"Force: {args.force}")
|
||||||
|
print(f"Verbosity Level: {args.verbose}")
|
||||||
|
|
||||||
|
# Example validation
|
||||||
|
if args.timeout < 1:
|
||||||
|
parser.error("Timeout must be at least 1 second")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
162
skills/argparse-patterns/templates/boolean-flags.py
Executable file
162
skills/argparse-patterns/templates/boolean-flags.py
Executable file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Boolean flag patterns with store_true, store_false, and count actions.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python boolean-flags.py --verbose
|
||||||
|
python boolean-flags.py -vvv --debug --force
|
||||||
|
python boolean-flags.py --no-cache --quiet
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Boolean flag patterns',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== store_true (False by default) =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable verbose output'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable debug mode'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--force', '-f',
|
||||||
|
action='store_true',
|
||||||
|
help='Force operation without confirmation'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
action='store_true',
|
||||||
|
help='Perform a dry run without making changes'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== store_false (True by default) =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-cache',
|
||||||
|
action='store_false',
|
||||||
|
dest='cache',
|
||||||
|
help='Disable caching (enabled by default)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-color',
|
||||||
|
action='store_false',
|
||||||
|
dest='color',
|
||||||
|
help='Disable colored output (enabled by default)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== count action (count occurrences) =====
|
||||||
|
parser.add_argument(
|
||||||
|
'-v',
|
||||||
|
action='count',
|
||||||
|
default=0,
|
||||||
|
dest='verbosity',
|
||||||
|
help='Increase verbosity (-v, -vv, -vvv)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-q', '--quiet',
|
||||||
|
action='count',
|
||||||
|
default=0,
|
||||||
|
help='Decrease verbosity (-q, -qq, -qqq)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== store_const action =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--fast',
|
||||||
|
action='store_const',
|
||||||
|
const='fast',
|
||||||
|
dest='mode',
|
||||||
|
help='Use fast mode'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--safe',
|
||||||
|
action='store_const',
|
||||||
|
const='safe',
|
||||||
|
dest='mode',
|
||||||
|
help='Use safe mode (default)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.set_defaults(mode='safe')
|
||||||
|
|
||||||
|
# ===== Combined short flags =====
|
||||||
|
parser.add_argument(
|
||||||
|
'-a', '--all',
|
||||||
|
action='store_true',
|
||||||
|
help='Process all items'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-r', '--recursive',
|
||||||
|
action='store_true',
|
||||||
|
help='Process recursively'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-i', '--interactive',
|
||||||
|
action='store_true',
|
||||||
|
help='Run in interactive mode'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Calculate effective verbosity
|
||||||
|
effective_verbosity = args.verbosity - args.quiet
|
||||||
|
|
||||||
|
# Display configuration
|
||||||
|
print("Boolean Flags Configuration:")
|
||||||
|
print(f" Verbose: {args.verbose}")
|
||||||
|
print(f" Debug: {args.debug}")
|
||||||
|
print(f" Force: {args.force}")
|
||||||
|
print(f" Dry Run: {args.dry_run}")
|
||||||
|
print(f" Cache: {args.cache}")
|
||||||
|
print(f" Color: {args.color}")
|
||||||
|
print(f" Verbosity Level: {effective_verbosity}")
|
||||||
|
print(f" Mode: {args.mode}")
|
||||||
|
print(f" All: {args.all}")
|
||||||
|
print(f" Recursive: {args.recursive}")
|
||||||
|
print(f" Interactive: {args.interactive}")
|
||||||
|
|
||||||
|
# Example usage based on flags
|
||||||
|
if args.debug:
|
||||||
|
print("\nDebug mode enabled - showing detailed information")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("\nDry run mode - no changes will be made")
|
||||||
|
|
||||||
|
if effective_verbosity > 0:
|
||||||
|
print(f"\nVerbosity level: {effective_verbosity}")
|
||||||
|
if effective_verbosity >= 3:
|
||||||
|
print("Maximum verbosity - showing everything")
|
||||||
|
elif effective_verbosity < 0:
|
||||||
|
print(f"\nQuiet level: {abs(effective_verbosity)}")
|
||||||
|
|
||||||
|
if args.force:
|
||||||
|
print("\nForce mode - skipping confirmations")
|
||||||
|
|
||||||
|
if not args.cache:
|
||||||
|
print("\nCache disabled")
|
||||||
|
|
||||||
|
if not args.color:
|
||||||
|
print("\nColor output disabled")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
197
skills/argparse-patterns/templates/choices-validation.py
Executable file
197
skills/argparse-patterns/templates/choices-validation.py
Executable file
@@ -0,0 +1,197 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Argument choices and custom validation patterns.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python choices-validation.py --log-level debug
|
||||||
|
python choices-validation.py --port 8080 --host 192.168.1.1
|
||||||
|
python choices-validation.py --region us-east-1 --instance-type t2.micro
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def validate_port(value):
|
||||||
|
"""Custom validator for port numbers."""
|
||||||
|
try:
|
||||||
|
ivalue = int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(f"{value} is not a valid integer")
|
||||||
|
|
||||||
|
if ivalue < 1 or ivalue > 65535:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} is not a valid port (must be 1-65535)"
|
||||||
|
)
|
||||||
|
return ivalue
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ip(value):
|
||||||
|
"""Custom validator for IP addresses."""
|
||||||
|
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||||
|
if not re.match(pattern, value):
|
||||||
|
raise argparse.ArgumentTypeError(f"{value} is not a valid IP address")
|
||||||
|
|
||||||
|
# Check each octet is 0-255
|
||||||
|
octets = [int(x) for x in value.split('.')]
|
||||||
|
if any(o < 0 or o > 255 for o in octets):
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} contains invalid octets (must be 0-255)"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_email(value):
|
||||||
|
"""Custom validator for email addresses."""
|
||||||
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||||
|
if not re.match(pattern, value):
|
||||||
|
raise argparse.ArgumentTypeError(f"{value} is not a valid email address")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_path_exists(value):
|
||||||
|
"""Custom validator to check if path exists."""
|
||||||
|
path = Path(value)
|
||||||
|
if not path.exists():
|
||||||
|
raise argparse.ArgumentTypeError(f"Path does not exist: {value}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def validate_range(min_val, max_val):
|
||||||
|
"""Factory function for range validators."""
|
||||||
|
def validator(value):
|
||||||
|
try:
|
||||||
|
ivalue = int(value)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(f"{value} is not a valid integer")
|
||||||
|
|
||||||
|
if ivalue < min_val or ivalue > max_val:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} must be between {min_val} and {max_val}"
|
||||||
|
)
|
||||||
|
return ivalue
|
||||||
|
return validator
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Demonstrate choices and validation patterns',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== String Choices =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--log-level',
|
||||||
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
||||||
|
default='info',
|
||||||
|
help='Logging level (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--region',
|
||||||
|
choices=[
|
||||||
|
'us-east-1', 'us-west-1', 'us-west-2',
|
||||||
|
'eu-west-1', 'eu-central-1',
|
||||||
|
'ap-southeast-1', 'ap-northeast-1'
|
||||||
|
],
|
||||||
|
default='us-east-1',
|
||||||
|
help='AWS region (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--format',
|
||||||
|
choices=['json', 'yaml', 'toml', 'xml'],
|
||||||
|
default='json',
|
||||||
|
help='Output format (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Custom Validators =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--port',
|
||||||
|
type=validate_port,
|
||||||
|
default=8080,
|
||||||
|
help='Server port (1-65535, default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--host',
|
||||||
|
type=validate_ip,
|
||||||
|
default='127.0.0.1',
|
||||||
|
help='Server host IP (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--email',
|
||||||
|
type=validate_email,
|
||||||
|
help='Email address for notifications'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--config',
|
||||||
|
type=validate_path_exists,
|
||||||
|
help='Path to configuration file (must exist)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Range Validators =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--workers',
|
||||||
|
type=validate_range(1, 32),
|
||||||
|
default=4,
|
||||||
|
help='Number of worker processes (1-32, default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--timeout',
|
||||||
|
type=validate_range(1, 3600),
|
||||||
|
default=30,
|
||||||
|
help='Request timeout in seconds (1-3600, default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Integer Choices =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--instance-type',
|
||||||
|
choices=['t2.micro', 't2.small', 't2.medium', 't3.large'],
|
||||||
|
default='t2.micro',
|
||||||
|
help='EC2 instance type (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Type Coercion =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--memory',
|
||||||
|
type=float,
|
||||||
|
default=1.0,
|
||||||
|
help='Memory limit in GB (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--retry-count',
|
||||||
|
type=int,
|
||||||
|
default=3,
|
||||||
|
help='Number of retries (default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Display parsed values
|
||||||
|
print("Configuration:")
|
||||||
|
print(f" Log Level: {args.log_level}")
|
||||||
|
print(f" Region: {args.region}")
|
||||||
|
print(f" Format: {args.format}")
|
||||||
|
print(f" Port: {args.port}")
|
||||||
|
print(f" Host: {args.host}")
|
||||||
|
print(f" Email: {args.email}")
|
||||||
|
print(f" Config: {args.config}")
|
||||||
|
print(f" Workers: {args.workers}")
|
||||||
|
print(f" Timeout: {args.timeout}s")
|
||||||
|
print(f" Instance Type: {args.instance_type}")
|
||||||
|
print(f" Memory: {args.memory}GB")
|
||||||
|
print(f" Retry Count: {args.retry_count}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
188
skills/argparse-patterns/templates/custom-actions.py
Executable file
188
skills/argparse-patterns/templates/custom-actions.py
Executable file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Custom action classes for advanced argument processing.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python custom-actions.py --env-file .env
|
||||||
|
python custom-actions.py --key API_KEY --key DB_URL
|
||||||
|
python custom-actions.py --range 1-10 --range 20-30
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class LoadEnvFileAction(argparse.Action):
|
||||||
|
"""Custom action to load environment variables from file."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
env_file = Path(values)
|
||||||
|
if not env_file.exists():
|
||||||
|
parser.error(f"Environment file does not exist: {values}")
|
||||||
|
|
||||||
|
env_vars = {}
|
||||||
|
with open(env_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
if '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
env_vars[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
setattr(namespace, self.dest, env_vars)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyValueAction(argparse.Action):
|
||||||
|
"""Custom action to parse key=value pairs."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
if '=' not in values:
|
||||||
|
parser.error(f"Argument must be in key=value format: {values}")
|
||||||
|
|
||||||
|
key, value = values.split('=', 1)
|
||||||
|
items = getattr(namespace, self.dest, None) or {}
|
||||||
|
items[key] = value
|
||||||
|
setattr(namespace, self.dest, items)
|
||||||
|
|
||||||
|
|
||||||
|
class RangeAction(argparse.Action):
|
||||||
|
"""Custom action to parse ranges like 1-10."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
if '-' not in values:
|
||||||
|
parser.error(f"Range must be in format start-end: {values}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
start, end = values.split('-')
|
||||||
|
start = int(start)
|
||||||
|
end = int(end)
|
||||||
|
except ValueError:
|
||||||
|
parser.error(f"Invalid range format: {values}")
|
||||||
|
|
||||||
|
if start > end:
|
||||||
|
parser.error(f"Start must be less than or equal to end: {values}")
|
||||||
|
|
||||||
|
ranges = getattr(namespace, self.dest, None) or []
|
||||||
|
ranges.append((start, end))
|
||||||
|
setattr(namespace, self.dest, ranges)
|
||||||
|
|
||||||
|
|
||||||
|
class AppendUniqueAction(argparse.Action):
|
||||||
|
"""Custom action to append unique values only."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
items = getattr(namespace, self.dest, None) or []
|
||||||
|
if values not in items:
|
||||||
|
items.append(values)
|
||||||
|
setattr(namespace, self.dest, items)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateAndStoreAction(argparse.Action):
|
||||||
|
"""Custom action that validates before storing."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
# Custom validation logic
|
||||||
|
if values.startswith('test-'):
|
||||||
|
print(f"Warning: Using test value: {values}")
|
||||||
|
|
||||||
|
# Transform value
|
||||||
|
transformed = values.upper()
|
||||||
|
|
||||||
|
setattr(namespace, self.dest, transformed)
|
||||||
|
|
||||||
|
|
||||||
|
class IncrementAction(argparse.Action):
|
||||||
|
"""Custom action to increment a value."""
|
||||||
|
|
||||||
|
def __init__(self, option_strings, dest, default=0, **kwargs):
|
||||||
|
super().__init__(option_strings, dest, nargs=0, default=default, **kwargs)
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
current = getattr(namespace, self.dest, self.default)
|
||||||
|
setattr(namespace, self.dest, current + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Custom action demonstrations',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load environment file
|
||||||
|
parser.add_argument(
|
||||||
|
'--env-file',
|
||||||
|
action=LoadEnvFileAction,
|
||||||
|
help='Load environment variables from file'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Key-value pairs
|
||||||
|
parser.add_argument(
|
||||||
|
'--config', '-c',
|
||||||
|
action=KeyValueAction,
|
||||||
|
help='Configuration in key=value format (can be used multiple times)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Range parsing
|
||||||
|
parser.add_argument(
|
||||||
|
'--range', '-r',
|
||||||
|
action=RangeAction,
|
||||||
|
help='Range in start-end format (e.g., 1-10)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unique append
|
||||||
|
parser.add_argument(
|
||||||
|
'--tag',
|
||||||
|
action=AppendUniqueAction,
|
||||||
|
help='Add unique tag (duplicates ignored)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate and transform
|
||||||
|
parser.add_argument(
|
||||||
|
'--key',
|
||||||
|
action=ValidateAndStoreAction,
|
||||||
|
help='Key to transform to uppercase'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Custom increment
|
||||||
|
parser.add_argument(
|
||||||
|
'--increment',
|
||||||
|
action=IncrementAction,
|
||||||
|
help='Increment counter'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
print("Custom Actions Results:")
|
||||||
|
|
||||||
|
if args.env_file:
|
||||||
|
print(f"\nEnvironment Variables:")
|
||||||
|
for key, value in args.env_file.items():
|
||||||
|
print(f" {key}={value}")
|
||||||
|
|
||||||
|
if args.config:
|
||||||
|
print(f"\nConfiguration:")
|
||||||
|
for key, value in args.config.items():
|
||||||
|
print(f" {key}={value}")
|
||||||
|
|
||||||
|
if args.range:
|
||||||
|
print(f"\nRanges:")
|
||||||
|
for start, end in args.range:
|
||||||
|
print(f" {start}-{end} (includes {end - start + 1} values)")
|
||||||
|
|
||||||
|
if args.tag:
|
||||||
|
print(f"\nUnique Tags: {', '.join(args.tag)}")
|
||||||
|
|
||||||
|
if args.key:
|
||||||
|
print(f"\nTransformed Key: {args.key}")
|
||||||
|
|
||||||
|
if args.increment:
|
||||||
|
print(f"\nIncrement Count: {args.increment}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
175
skills/argparse-patterns/templates/mutually-exclusive.py
Executable file
175
skills/argparse-patterns/templates/mutually-exclusive.py
Executable file
@@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Mutually exclusive argument groups.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python mutually-exclusive.py --json output.json
|
||||||
|
python mutually-exclusive.py --yaml output.yaml
|
||||||
|
python mutually-exclusive.py --verbose
|
||||||
|
python mutually-exclusive.py --quiet
|
||||||
|
python mutually-exclusive.py --create resource
|
||||||
|
python mutually-exclusive.py --delete resource
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Mutually exclusive argument groups',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Output Format (mutually exclusive) =====
|
||||||
|
output_group = parser.add_mutually_exclusive_group()
|
||||||
|
output_group.add_argument(
|
||||||
|
'--json',
|
||||||
|
metavar='FILE',
|
||||||
|
help='Output in JSON format'
|
||||||
|
)
|
||||||
|
output_group.add_argument(
|
||||||
|
'--yaml',
|
||||||
|
metavar='FILE',
|
||||||
|
help='Output in YAML format'
|
||||||
|
)
|
||||||
|
output_group.add_argument(
|
||||||
|
'--xml',
|
||||||
|
metavar='FILE',
|
||||||
|
help='Output in XML format'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Verbosity (mutually exclusive) =====
|
||||||
|
verbosity_group = parser.add_mutually_exclusive_group()
|
||||||
|
verbosity_group.add_argument(
|
||||||
|
'--verbose', '-v',
|
||||||
|
action='store_true',
|
||||||
|
help='Increase verbosity'
|
||||||
|
)
|
||||||
|
verbosity_group.add_argument(
|
||||||
|
'--quiet', '-q',
|
||||||
|
action='store_true',
|
||||||
|
help='Suppress output'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Operation Mode (mutually exclusive, required) =====
|
||||||
|
operation_group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
operation_group.add_argument(
|
||||||
|
'--create',
|
||||||
|
metavar='RESOURCE',
|
||||||
|
help='Create a resource'
|
||||||
|
)
|
||||||
|
operation_group.add_argument(
|
||||||
|
'--update',
|
||||||
|
metavar='RESOURCE',
|
||||||
|
help='Update a resource'
|
||||||
|
)
|
||||||
|
operation_group.add_argument(
|
||||||
|
'--delete',
|
||||||
|
metavar='RESOURCE',
|
||||||
|
help='Delete a resource'
|
||||||
|
)
|
||||||
|
operation_group.add_argument(
|
||||||
|
'--list',
|
||||||
|
action='store_true',
|
||||||
|
help='List all resources'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Authentication Method (mutually exclusive) =====
|
||||||
|
auth_group = parser.add_mutually_exclusive_group()
|
||||||
|
auth_group.add_argument(
|
||||||
|
'--token',
|
||||||
|
metavar='TOKEN',
|
||||||
|
help='Authenticate with token'
|
||||||
|
)
|
||||||
|
auth_group.add_argument(
|
||||||
|
'--api-key',
|
||||||
|
metavar='KEY',
|
||||||
|
help='Authenticate with API key'
|
||||||
|
)
|
||||||
|
auth_group.add_argument(
|
||||||
|
'--credentials',
|
||||||
|
metavar='FILE',
|
||||||
|
help='Authenticate with credentials file'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Deployment Strategy (mutually exclusive with default) =====
|
||||||
|
strategy_group = parser.add_mutually_exclusive_group()
|
||||||
|
strategy_group.add_argument(
|
||||||
|
'--rolling',
|
||||||
|
action='store_true',
|
||||||
|
help='Use rolling deployment'
|
||||||
|
)
|
||||||
|
strategy_group.add_argument(
|
||||||
|
'--blue-green',
|
||||||
|
action='store_true',
|
||||||
|
help='Use blue-green deployment'
|
||||||
|
)
|
||||||
|
strategy_group.add_argument(
|
||||||
|
'--canary',
|
||||||
|
action='store_true',
|
||||||
|
help='Use canary deployment'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set default strategy if none specified
|
||||||
|
parser.set_defaults(rolling=False, blue_green=False, canary=False)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Display configuration
|
||||||
|
print("Mutually Exclusive Groups Configuration:")
|
||||||
|
|
||||||
|
# Output format
|
||||||
|
if args.json:
|
||||||
|
print(f" Output Format: JSON to {args.json}")
|
||||||
|
elif args.yaml:
|
||||||
|
print(f" Output Format: YAML to {args.yaml}")
|
||||||
|
elif args.xml:
|
||||||
|
print(f" Output Format: XML to {args.xml}")
|
||||||
|
else:
|
||||||
|
print(" Output Format: None (default stdout)")
|
||||||
|
|
||||||
|
# Verbosity
|
||||||
|
if args.verbose:
|
||||||
|
print(" Verbosity: Verbose")
|
||||||
|
elif args.quiet:
|
||||||
|
print(" Verbosity: Quiet")
|
||||||
|
else:
|
||||||
|
print(" Verbosity: Normal")
|
||||||
|
|
||||||
|
# Operation
|
||||||
|
if args.create:
|
||||||
|
print(f" Operation: Create {args.create}")
|
||||||
|
elif args.update:
|
||||||
|
print(f" Operation: Update {args.update}")
|
||||||
|
elif args.delete:
|
||||||
|
print(f" Operation: Delete {args.delete}")
|
||||||
|
elif args.list:
|
||||||
|
print(" Operation: List resources")
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
if args.token:
|
||||||
|
print(f" Auth Method: Token")
|
||||||
|
elif args.api_key:
|
||||||
|
print(f" Auth Method: API Key")
|
||||||
|
elif args.credentials:
|
||||||
|
print(f" Auth Method: Credentials file ({args.credentials})")
|
||||||
|
else:
|
||||||
|
print(" Auth Method: None")
|
||||||
|
|
||||||
|
# Deployment strategy
|
||||||
|
if args.rolling:
|
||||||
|
print(" Deployment: Rolling")
|
||||||
|
elif args.blue_green:
|
||||||
|
print(" Deployment: Blue-Green")
|
||||||
|
elif args.canary:
|
||||||
|
print(" Deployment: Canary")
|
||||||
|
else:
|
||||||
|
print(" Deployment: Default")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
219
skills/argparse-patterns/templates/nested-subparser.py
Executable file
219
skills/argparse-patterns/templates/nested-subparser.py
Executable file
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Nested subcommands pattern (like git config get/set, kubectl config view).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python nested-subparser.py config get database_url
|
||||||
|
python nested-subparser.py config set api_key abc123
|
||||||
|
python nested-subparser.py config list
|
||||||
|
python nested-subparser.py deploy start production --replicas 3
|
||||||
|
python nested-subparser.py deploy stop production
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# Config command handlers
|
||||||
|
def config_get(args):
|
||||||
|
"""Get configuration value."""
|
||||||
|
print(f"Getting config: {args.key}")
|
||||||
|
# Simulate getting config
|
||||||
|
print(f"{args.key} = example_value")
|
||||||
|
|
||||||
|
|
||||||
|
def config_set(args):
|
||||||
|
"""Set configuration value."""
|
||||||
|
print(f"Setting config: {args.key} = {args.value}")
|
||||||
|
if args.force:
|
||||||
|
print("(Overwriting existing value)")
|
||||||
|
|
||||||
|
|
||||||
|
def config_list(args):
|
||||||
|
"""List all configuration values."""
|
||||||
|
print(f"Listing all configuration (format: {args.format})")
|
||||||
|
|
||||||
|
|
||||||
|
def config_delete(args):
|
||||||
|
"""Delete configuration value."""
|
||||||
|
if not args.force:
|
||||||
|
response = input(f"Delete {args.key}? (y/n): ")
|
||||||
|
if response.lower() != 'y':
|
||||||
|
print("Cancelled")
|
||||||
|
return 1
|
||||||
|
print(f"Deleted: {args.key}")
|
||||||
|
|
||||||
|
|
||||||
|
# Deploy command handlers
|
||||||
|
def deploy_start(args):
|
||||||
|
"""Start deployment."""
|
||||||
|
print(f"Starting deployment to {args.environment}")
|
||||||
|
print(f"Replicas: {args.replicas}")
|
||||||
|
print(f"Wait: {args.wait}")
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_stop(args):
|
||||||
|
"""Stop deployment."""
|
||||||
|
print(f"Stopping deployment in {args.environment}")
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_restart(args):
|
||||||
|
"""Restart deployment."""
|
||||||
|
print(f"Restarting deployment in {args.environment}")
|
||||||
|
if args.hard:
|
||||||
|
print("(Hard restart)")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Main parser
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Multi-level CLI tool with nested subcommands',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('--version', action='version', version='1.0.0')
|
||||||
|
|
||||||
|
# Top-level subparsers
|
||||||
|
subparsers = parser.add_subparsers(
|
||||||
|
dest='command',
|
||||||
|
help='Top-level commands',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Config command group =====
|
||||||
|
config_parser = subparsers.add_parser(
|
||||||
|
'config',
|
||||||
|
help='Manage configuration',
|
||||||
|
description='Configuration management commands'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Config subcommands
|
||||||
|
config_subparsers = config_parser.add_subparsers(
|
||||||
|
dest='config_command',
|
||||||
|
help='Config operations',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# config get
|
||||||
|
config_get_parser = config_subparsers.add_parser(
|
||||||
|
'get',
|
||||||
|
help='Get configuration value'
|
||||||
|
)
|
||||||
|
config_get_parser.add_argument('key', help='Configuration key')
|
||||||
|
config_get_parser.set_defaults(func=config_get)
|
||||||
|
|
||||||
|
# config set
|
||||||
|
config_set_parser = config_subparsers.add_parser(
|
||||||
|
'set',
|
||||||
|
help='Set configuration value'
|
||||||
|
)
|
||||||
|
config_set_parser.add_argument('key', help='Configuration key')
|
||||||
|
config_set_parser.add_argument('value', help='Configuration value')
|
||||||
|
config_set_parser.add_argument(
|
||||||
|
'--force', '-f',
|
||||||
|
action='store_true',
|
||||||
|
help='Overwrite existing value'
|
||||||
|
)
|
||||||
|
config_set_parser.set_defaults(func=config_set)
|
||||||
|
|
||||||
|
# config list
|
||||||
|
config_list_parser = config_subparsers.add_parser(
|
||||||
|
'list',
|
||||||
|
help='List all configuration values'
|
||||||
|
)
|
||||||
|
config_list_parser.add_argument(
|
||||||
|
'--format',
|
||||||
|
choices=['text', 'json', 'yaml'],
|
||||||
|
default='text',
|
||||||
|
help='Output format (default: %(default)s)'
|
||||||
|
)
|
||||||
|
config_list_parser.set_defaults(func=config_list)
|
||||||
|
|
||||||
|
# config delete
|
||||||
|
config_delete_parser = config_subparsers.add_parser(
|
||||||
|
'delete',
|
||||||
|
help='Delete configuration value'
|
||||||
|
)
|
||||||
|
config_delete_parser.add_argument('key', help='Configuration key')
|
||||||
|
config_delete_parser.add_argument(
|
||||||
|
'--force', '-f',
|
||||||
|
action='store_true',
|
||||||
|
help='Delete without confirmation'
|
||||||
|
)
|
||||||
|
config_delete_parser.set_defaults(func=config_delete)
|
||||||
|
|
||||||
|
# ===== Deploy command group =====
|
||||||
|
deploy_parser = subparsers.add_parser(
|
||||||
|
'deploy',
|
||||||
|
help='Manage deployments',
|
||||||
|
description='Deployment management commands'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deploy subcommands
|
||||||
|
deploy_subparsers = deploy_parser.add_subparsers(
|
||||||
|
dest='deploy_command',
|
||||||
|
help='Deploy operations',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# deploy start
|
||||||
|
deploy_start_parser = deploy_subparsers.add_parser(
|
||||||
|
'start',
|
||||||
|
help='Start deployment'
|
||||||
|
)
|
||||||
|
deploy_start_parser.add_argument(
|
||||||
|
'environment',
|
||||||
|
choices=['development', 'staging', 'production'],
|
||||||
|
help='Target environment'
|
||||||
|
)
|
||||||
|
deploy_start_parser.add_argument(
|
||||||
|
'--replicas', '-r',
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help='Number of replicas (default: %(default)s)'
|
||||||
|
)
|
||||||
|
deploy_start_parser.add_argument(
|
||||||
|
'--wait',
|
||||||
|
action='store_true',
|
||||||
|
help='Wait for deployment to complete'
|
||||||
|
)
|
||||||
|
deploy_start_parser.set_defaults(func=deploy_start)
|
||||||
|
|
||||||
|
# deploy stop
|
||||||
|
deploy_stop_parser = deploy_subparsers.add_parser(
|
||||||
|
'stop',
|
||||||
|
help='Stop deployment'
|
||||||
|
)
|
||||||
|
deploy_stop_parser.add_argument(
|
||||||
|
'environment',
|
||||||
|
choices=['development', 'staging', 'production'],
|
||||||
|
help='Target environment'
|
||||||
|
)
|
||||||
|
deploy_stop_parser.set_defaults(func=deploy_stop)
|
||||||
|
|
||||||
|
# deploy restart
|
||||||
|
deploy_restart_parser = deploy_subparsers.add_parser(
|
||||||
|
'restart',
|
||||||
|
help='Restart deployment'
|
||||||
|
)
|
||||||
|
deploy_restart_parser.add_argument(
|
||||||
|
'environment',
|
||||||
|
choices=['development', 'staging', 'production'],
|
||||||
|
help='Target environment'
|
||||||
|
)
|
||||||
|
deploy_restart_parser.add_argument(
|
||||||
|
'--hard',
|
||||||
|
action='store_true',
|
||||||
|
help='Perform hard restart'
|
||||||
|
)
|
||||||
|
deploy_restart_parser.set_defaults(func=deploy_restart)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Call the appropriate command function
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main() or 0)
|
||||||
123
skills/argparse-patterns/templates/subparser-pattern.py
Executable file
123
skills/argparse-patterns/templates/subparser-pattern.py
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Single-level subcommands pattern (like docker, kubectl).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python subparser-pattern.py init --template react
|
||||||
|
python subparser-pattern.py deploy production --force
|
||||||
|
python subparser-pattern.py status --format json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_init(args):
|
||||||
|
"""Initialize a new project."""
|
||||||
|
print(f"Initializing project with {args.template} template...")
|
||||||
|
print(f"Path: {args.path}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_deploy(args):
|
||||||
|
"""Deploy application."""
|
||||||
|
print(f"Deploying to {args.environment} in {args.mode} mode")
|
||||||
|
if args.force:
|
||||||
|
print("Warning: Force mode enabled")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_status(args):
|
||||||
|
"""Show deployment status."""
|
||||||
|
print(f"Status format: {args.format}")
|
||||||
|
print("Fetching status...")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Main parser
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Multi-command CLI tool',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version='1.0.0'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create subparsers
|
||||||
|
subparsers = parser.add_subparsers(
|
||||||
|
dest='command',
|
||||||
|
help='Available commands',
|
||||||
|
required=True # Python 3.7+
|
||||||
|
)
|
||||||
|
|
||||||
|
# Init command
|
||||||
|
init_parser = subparsers.add_parser(
|
||||||
|
'init',
|
||||||
|
help='Initialize a new project',
|
||||||
|
description='Initialize a new project with specified template'
|
||||||
|
)
|
||||||
|
init_parser.add_argument(
|
||||||
|
'--template', '-t',
|
||||||
|
default='basic',
|
||||||
|
help='Project template (default: %(default)s)'
|
||||||
|
)
|
||||||
|
init_parser.add_argument(
|
||||||
|
'--path', '-p',
|
||||||
|
default='.',
|
||||||
|
help='Project path (default: %(default)s)'
|
||||||
|
)
|
||||||
|
init_parser.set_defaults(func=cmd_init)
|
||||||
|
|
||||||
|
# Deploy command
|
||||||
|
deploy_parser = subparsers.add_parser(
|
||||||
|
'deploy',
|
||||||
|
help='Deploy application to environment',
|
||||||
|
description='Deploy application to specified environment'
|
||||||
|
)
|
||||||
|
deploy_parser.add_argument(
|
||||||
|
'environment',
|
||||||
|
choices=['development', 'staging', 'production'],
|
||||||
|
help='Target environment'
|
||||||
|
)
|
||||||
|
deploy_parser.add_argument(
|
||||||
|
'--force', '-f',
|
||||||
|
action='store_true',
|
||||||
|
help='Force deployment without confirmation'
|
||||||
|
)
|
||||||
|
deploy_parser.add_argument(
|
||||||
|
'--mode', '-m',
|
||||||
|
choices=['fast', 'safe', 'rollback'],
|
||||||
|
default='safe',
|
||||||
|
help='Deployment mode (default: %(default)s)'
|
||||||
|
)
|
||||||
|
deploy_parser.set_defaults(func=cmd_deploy)
|
||||||
|
|
||||||
|
# Status command
|
||||||
|
status_parser = subparsers.add_parser(
|
||||||
|
'status',
|
||||||
|
help='Show deployment status',
|
||||||
|
description='Display current deployment status'
|
||||||
|
)
|
||||||
|
status_parser.add_argument(
|
||||||
|
'--format',
|
||||||
|
choices=['text', 'json', 'yaml'],
|
||||||
|
default='text',
|
||||||
|
help='Output format (default: %(default)s)'
|
||||||
|
)
|
||||||
|
status_parser.add_argument(
|
||||||
|
'--service',
|
||||||
|
action='append',
|
||||||
|
help='Filter by service (can be used multiple times)'
|
||||||
|
)
|
||||||
|
status_parser.set_defaults(func=cmd_status)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Call the appropriate command function
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main() or 0)
|
||||||
257
skills/argparse-patterns/templates/type-coercion.py
Executable file
257
skills/argparse-patterns/templates/type-coercion.py
Executable file
@@ -0,0 +1,257 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Type coercion and custom type converters.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python type-coercion.py --port 8080 --timeout 30.5 --date 2024-01-15
|
||||||
|
python type-coercion.py --url https://api.example.com --size 1.5GB
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(value):
|
||||||
|
"""Parse date in YYYY-MM-DD format."""
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value, '%Y-%m-%d').date()
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Invalid date format: {value} (expected YYYY-MM-DD)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_datetime(value):
|
||||||
|
"""Parse datetime in ISO format."""
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(value)
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Invalid datetime format: {value} (expected ISO format)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_url(value):
|
||||||
|
"""Parse and validate URL."""
|
||||||
|
pattern = r'^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$'
|
||||||
|
if not re.match(pattern, value):
|
||||||
|
raise argparse.ArgumentTypeError(f"Invalid URL: {value}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_size(value):
|
||||||
|
"""Parse size with units (e.g., 1.5GB, 500MB)."""
|
||||||
|
pattern = r'^(\d+\.?\d*)(B|KB|MB|GB|TB)$'
|
||||||
|
match = re.match(pattern, value, re.IGNORECASE)
|
||||||
|
if not match:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Invalid size format: {value} (expected number with unit)"
|
||||||
|
)
|
||||||
|
|
||||||
|
size, unit = match.groups()
|
||||||
|
size = float(size)
|
||||||
|
|
||||||
|
units = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4}
|
||||||
|
return int(size * units[unit.upper()])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_duration(value):
|
||||||
|
"""Parse duration (e.g., 1h, 30m, 90s)."""
|
||||||
|
pattern = r'^(\d+)(s|m|h|d)$'
|
||||||
|
match = re.match(pattern, value, re.IGNORECASE)
|
||||||
|
if not match:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Invalid duration format: {value} (expected number with s/m/h/d)"
|
||||||
|
)
|
||||||
|
|
||||||
|
amount, unit = match.groups()
|
||||||
|
amount = int(amount)
|
||||||
|
|
||||||
|
units = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
|
||||||
|
return amount * units[unit.lower()]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_percentage(value):
|
||||||
|
"""Parse percentage (0-100)."""
|
||||||
|
try:
|
||||||
|
pct = float(value.rstrip('%'))
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(f"Invalid percentage: {value}")
|
||||||
|
|
||||||
|
if pct < 0 or pct > 100:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"Percentage must be between 0 and 100: {value}"
|
||||||
|
)
|
||||||
|
return pct
|
||||||
|
|
||||||
|
|
||||||
|
def parse_comma_separated(value):
|
||||||
|
"""Parse comma-separated list."""
|
||||||
|
return [item.strip() for item in value.split(',') if item.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_key_value_pairs(value):
|
||||||
|
"""Parse semicolon-separated key=value pairs."""
|
||||||
|
pairs = {}
|
||||||
|
for pair in value.split(';'):
|
||||||
|
if '=' in pair:
|
||||||
|
key, val = pair.split('=', 1)
|
||||||
|
pairs[key.strip()] = val.strip()
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Type coercion demonstrations',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Built-in Types =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--port',
|
||||||
|
type=int,
|
||||||
|
default=8080,
|
||||||
|
help='Port number (integer)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--timeout',
|
||||||
|
type=float,
|
||||||
|
default=30.0,
|
||||||
|
help='Timeout in seconds (float)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--config',
|
||||||
|
type=Path,
|
||||||
|
help='Configuration file path'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--output',
|
||||||
|
type=argparse.FileType('w'),
|
||||||
|
help='Output file (opened for writing)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--input',
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
help='Input file (opened for reading)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Custom Types =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--date',
|
||||||
|
type=parse_date,
|
||||||
|
help='Date in YYYY-MM-DD format'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--datetime',
|
||||||
|
type=parse_datetime,
|
||||||
|
help='Datetime in ISO format'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--url',
|
||||||
|
type=parse_url,
|
||||||
|
help='URL to connect to'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--size',
|
||||||
|
type=parse_size,
|
||||||
|
help='Size with unit (e.g., 1.5GB, 500MB)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--duration',
|
||||||
|
type=parse_duration,
|
||||||
|
help='Duration (e.g., 1h, 30m, 90s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--percentage',
|
||||||
|
type=parse_percentage,
|
||||||
|
help='Percentage (0-100)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--tags',
|
||||||
|
type=parse_comma_separated,
|
||||||
|
help='Comma-separated tags'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--env',
|
||||||
|
type=parse_key_value_pairs,
|
||||||
|
help='Environment variables as key=value;key2=value2'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== List Types =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--ids',
|
||||||
|
type=int,
|
||||||
|
nargs='+',
|
||||||
|
help='List of integer IDs'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--ratios',
|
||||||
|
type=float,
|
||||||
|
nargs='*',
|
||||||
|
help='List of float ratios'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Display parsed values
|
||||||
|
print("Type Coercion Results:")
|
||||||
|
|
||||||
|
print("\nBuilt-in Types:")
|
||||||
|
print(f" Port (int): {args.port} - type: {type(args.port).__name__}")
|
||||||
|
print(f" Timeout (float): {args.timeout} - type: {type(args.timeout).__name__}")
|
||||||
|
if args.config:
|
||||||
|
print(f" Config (Path): {args.config} - type: {type(args.config).__name__}")
|
||||||
|
|
||||||
|
print("\nCustom Types:")
|
||||||
|
if args.date:
|
||||||
|
print(f" Date: {args.date} - type: {type(args.date).__name__}")
|
||||||
|
if args.datetime:
|
||||||
|
print(f" Datetime: {args.datetime}")
|
||||||
|
if args.url:
|
||||||
|
print(f" URL: {args.url}")
|
||||||
|
if args.size:
|
||||||
|
print(f" Size: {args.size} bytes ({args.size / (1024**3):.2f} GB)")
|
||||||
|
if args.duration:
|
||||||
|
print(f" Duration: {args.duration} seconds ({args.duration / 3600:.2f} hours)")
|
||||||
|
if args.percentage is not None:
|
||||||
|
print(f" Percentage: {args.percentage}%")
|
||||||
|
if args.tags:
|
||||||
|
print(f" Tags: {args.tags}")
|
||||||
|
if args.env:
|
||||||
|
print(f" Environment:")
|
||||||
|
for key, value in args.env.items():
|
||||||
|
print(f" {key} = {value}")
|
||||||
|
|
||||||
|
print("\nList Types:")
|
||||||
|
if args.ids:
|
||||||
|
print(f" IDs: {args.ids}")
|
||||||
|
if args.ratios:
|
||||||
|
print(f" Ratios: {args.ratios}")
|
||||||
|
|
||||||
|
# Clean up file handles
|
||||||
|
if args.output:
|
||||||
|
args.output.close()
|
||||||
|
if args.input:
|
||||||
|
args.input.close()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
164
skills/argparse-patterns/templates/variadic-args.py
Executable file
164
skills/argparse-patterns/templates/variadic-args.py
Executable file
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Variadic argument patterns (nargs: ?, *, +, number).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python variadic-args.py file1.txt file2.txt file3.txt
|
||||||
|
python variadic-args.py --output result.json file1.txt file2.txt
|
||||||
|
python variadic-args.py --include *.py --exclude test_*.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Variadic argument patterns',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== nargs='?' (optional, 0 or 1) =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--output',
|
||||||
|
nargs='?',
|
||||||
|
const='default.json', # Used if flag present but no value
|
||||||
|
default=None, # Used if flag not present
|
||||||
|
help='Output file (default: stdout, or default.json if flag present)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--config',
|
||||||
|
nargs='?',
|
||||||
|
const='config.yaml',
|
||||||
|
help='Configuration file (default: config.yaml if flag present)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== nargs='*' (zero or more) =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--include',
|
||||||
|
nargs='*',
|
||||||
|
default=[],
|
||||||
|
help='Include patterns (zero or more)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--exclude',
|
||||||
|
nargs='*',
|
||||||
|
default=[],
|
||||||
|
help='Exclude patterns (zero or more)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--tags',
|
||||||
|
nargs='*',
|
||||||
|
metavar='TAG',
|
||||||
|
help='Tags to apply'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== nargs='+' (one or more, required) =====
|
||||||
|
parser.add_argument(
|
||||||
|
'files',
|
||||||
|
nargs='+',
|
||||||
|
type=Path,
|
||||||
|
help='Input files (at least one required)'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--servers',
|
||||||
|
nargs='+',
|
||||||
|
metavar='SERVER',
|
||||||
|
help='Server addresses (at least one required if specified)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== nargs=N (exact number) =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--coordinates',
|
||||||
|
nargs=2,
|
||||||
|
type=float,
|
||||||
|
metavar=('LAT', 'LON'),
|
||||||
|
help='Coordinates as latitude longitude'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--range',
|
||||||
|
nargs=2,
|
||||||
|
type=int,
|
||||||
|
metavar=('START', 'END'),
|
||||||
|
help='Range as start end'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--rgb',
|
||||||
|
nargs=3,
|
||||||
|
type=int,
|
||||||
|
metavar=('R', 'G', 'B'),
|
||||||
|
help='RGB color values (0-255)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== Remainder arguments =====
|
||||||
|
parser.add_argument(
|
||||||
|
'--command',
|
||||||
|
nargs=argparse.REMAINDER,
|
||||||
|
help='Command and arguments to pass through'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
print("Variadic Arguments Results:")
|
||||||
|
|
||||||
|
print("\nnargs='?' (optional):")
|
||||||
|
print(f" Output: {args.output}")
|
||||||
|
print(f" Config: {args.config}")
|
||||||
|
|
||||||
|
print("\nnargs='*' (zero or more):")
|
||||||
|
print(f" Include Patterns: {args.include if args.include else '(none)'}")
|
||||||
|
print(f" Exclude Patterns: {args.exclude if args.exclude else '(none)'}")
|
||||||
|
print(f" Tags: {args.tags if args.tags else '(none)'}")
|
||||||
|
|
||||||
|
print("\nnargs='+' (one or more):")
|
||||||
|
print(f" Files ({len(args.files)}):")
|
||||||
|
for f in args.files:
|
||||||
|
print(f" - {f}")
|
||||||
|
if args.servers:
|
||||||
|
print(f" Servers ({len(args.servers)}):")
|
||||||
|
for s in args.servers:
|
||||||
|
print(f" - {s}")
|
||||||
|
|
||||||
|
print("\nnargs=N (exact number):")
|
||||||
|
if args.coordinates:
|
||||||
|
lat, lon = args.coordinates
|
||||||
|
print(f" Coordinates: {lat}, {lon}")
|
||||||
|
if args.range:
|
||||||
|
start, end = args.range
|
||||||
|
print(f" Range: {start} to {end}")
|
||||||
|
if args.rgb:
|
||||||
|
r, g, b = args.rgb
|
||||||
|
print(f" RGB Color: rgb({r}, {g}, {b})")
|
||||||
|
|
||||||
|
print("\nRemaining arguments:")
|
||||||
|
if args.command:
|
||||||
|
print(f" Command: {' '.join(args.command)}")
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
print("\nExample Processing:")
|
||||||
|
print(f"Processing {len(args.files)} file(s)...")
|
||||||
|
|
||||||
|
if args.include:
|
||||||
|
print(f"Including patterns: {', '.join(args.include)}")
|
||||||
|
if args.exclude:
|
||||||
|
print(f"Excluding patterns: {', '.join(args.exclude)}")
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
print(f"Output will be written to: {args.output}")
|
||||||
|
else:
|
||||||
|
print("Output will be written to: stdout")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
249
skills/clap-patterns/SKILL.md
Normal file
249
skills/clap-patterns/SKILL.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
---
|
||||||
|
name: clap-patterns
|
||||||
|
description: Modern type-safe Rust CLI patterns with Clap derive macros, Parser trait, Subcommand enums, validation, and value parsers. Use when building CLI applications, creating Clap commands, implementing type-safe Rust CLIs, or when user mentions Clap, CLI patterns, Rust command-line, derive macros, Parser trait, Subcommands, or command-line interfaces.
|
||||||
|
allowed-tools: Read, Write, Edit, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
# clap-patterns
|
||||||
|
|
||||||
|
Provides modern type-safe Rust CLI patterns using Clap 4.x with derive macros, Parser trait, Subcommand enums, custom validation, value parsers, and environment variable integration for building maintainable command-line applications.
|
||||||
|
|
||||||
|
## Core Patterns
|
||||||
|
|
||||||
|
### 1. Basic Parser with Derive Macros
|
||||||
|
|
||||||
|
Use derive macros for automatic CLI parsing with type safety:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "myapp")]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
/// Input file path
|
||||||
|
#[arg(short, long, value_name = "FILE")]
|
||||||
|
input: std::path::PathBuf,
|
||||||
|
|
||||||
|
/// Optional output file
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<std::path::PathBuf>,
|
||||||
|
|
||||||
|
/// Verbose mode
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
|
||||||
|
/// Number of items to process
|
||||||
|
#[arg(short, long, default_value_t = 10)]
|
||||||
|
count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
if cli.verbose {
|
||||||
|
println!("Processing: {:?}", cli.input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Subcommand Enums
|
||||||
|
|
||||||
|
Organize complex CLIs with nested subcommands:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "git")]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Add files to staging
|
||||||
|
Add {
|
||||||
|
/// Files to add
|
||||||
|
#[arg(value_name = "FILE")]
|
||||||
|
files: Vec<String>,
|
||||||
|
},
|
||||||
|
/// Commit changes
|
||||||
|
Commit {
|
||||||
|
/// Commit message
|
||||||
|
#[arg(short, long)]
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Value Parsers and Validation
|
||||||
|
|
||||||
|
Implement custom parsing and validation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use clap::Parser;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
const PORT_RANGE: RangeInclusive<usize> = 1..=65535;
|
||||||
|
|
||||||
|
fn port_in_range(s: &str) -> Result<u16, String> {
|
||||||
|
let port: usize = s
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("`{s}` isn't a valid port number"))?;
|
||||||
|
if PORT_RANGE.contains(&port) {
|
||||||
|
Ok(port as u16)
|
||||||
|
} else {
|
||||||
|
Err(format!("port not in range {}-{}", PORT_RANGE.start(), PORT_RANGE.end()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
/// Port to listen on
|
||||||
|
#[arg(short, long, value_parser = port_in_range)]
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Environment Variable Integration
|
||||||
|
|
||||||
|
Support environment variables with fallback:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
/// API key (or set API_KEY env var)
|
||||||
|
#[arg(long, env = "API_KEY")]
|
||||||
|
api_key: String,
|
||||||
|
|
||||||
|
/// Database URL
|
||||||
|
#[arg(long, env = "DATABASE_URL")]
|
||||||
|
database_url: String,
|
||||||
|
|
||||||
|
/// Optional log level
|
||||||
|
#[arg(long, env = "LOG_LEVEL", default_value = "info")]
|
||||||
|
log_level: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. ValueEnum for Constrained Choices
|
||||||
|
|
||||||
|
Use ValueEnum for type-safe option selection:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Format {
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
Toml,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
/// Output format
|
||||||
|
#[arg(value_enum, short, long, default_value_t = Format::Json)]
|
||||||
|
format: Format,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Templates
|
||||||
|
|
||||||
|
The following Rust templates demonstrate Clap patterns:
|
||||||
|
|
||||||
|
- **basic-parser.rs**: Simple CLI with Parser derive macro
|
||||||
|
- **subcommands.rs**: Multi-level subcommand structure
|
||||||
|
- **value-parser.rs**: Custom validation with value parsers
|
||||||
|
- **env-variables.rs**: Environment variable integration
|
||||||
|
- **value-enum.rs**: Type-safe enums for options
|
||||||
|
- **builder-pattern.rs**: Manual builder API (for complex cases)
|
||||||
|
- **full-featured-cli.rs**: Complete CLI with all patterns
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
Helper scripts for Clap development:
|
||||||
|
|
||||||
|
- **generate-completions.sh**: Generate shell completions (bash, zsh, fish)
|
||||||
|
- **validate-cargo.sh**: Check Cargo.toml for correct Clap dependencies
|
||||||
|
- **test-cli.sh**: Test CLI with various argument combinations
|
||||||
|
|
||||||
|
## Usage Instructions
|
||||||
|
|
||||||
|
1. **Choose the appropriate template** based on your CLI complexity:
|
||||||
|
- Simple single-command → `basic-parser.rs`
|
||||||
|
- Multiple subcommands → `subcommands.rs`
|
||||||
|
- Need validation → `value-parser.rs`
|
||||||
|
- Environment config → `env-variables.rs`
|
||||||
|
|
||||||
|
2. **Add Clap to Cargo.toml**:
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Implement your CLI** using the selected template as a starting point
|
||||||
|
|
||||||
|
4. **Generate completions** using the provided script for better UX
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Use derive macros for most cases (cleaner, less boilerplate)
|
||||||
|
- Add help text with doc comments (shows in `--help`)
|
||||||
|
- Validate early with value parsers
|
||||||
|
- Use ValueEnum for constrained choices
|
||||||
|
- Support environment variables for sensitive data
|
||||||
|
- Provide sensible defaults with `default_value_t`
|
||||||
|
- Use PathBuf for file/directory arguments
|
||||||
|
- Add version and author metadata
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Multiple Values
|
||||||
|
```rust
|
||||||
|
#[arg(short, long, num_args = 1..)]
|
||||||
|
files: Vec<PathBuf>,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Unless Present
|
||||||
|
```rust
|
||||||
|
#[arg(long, required_unless_present = "config")]
|
||||||
|
database_url: Option<String>,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conflicting Arguments
|
||||||
|
```rust
|
||||||
|
#[arg(long, conflicts_with = "json")]
|
||||||
|
yaml: bool,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Arguments (for subcommands)
|
||||||
|
```rust
|
||||||
|
#[arg(global = true, short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Your CLI
|
||||||
|
|
||||||
|
Run the test script to validate your CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/test-cli.sh your-binary
|
||||||
|
```
|
||||||
|
|
||||||
|
This tests:
|
||||||
|
- Help output (`--help`)
|
||||||
|
- Version flag (`--version`)
|
||||||
|
- Invalid arguments
|
||||||
|
- Subcommand routing
|
||||||
|
- Environment variable precedence
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Templates: `skills/clap-patterns/templates/`
|
||||||
|
- Scripts: `skills/clap-patterns/scripts/`
|
||||||
|
- Examples: `skills/clap-patterns/examples/`
|
||||||
|
- Clap Documentation: https://docs.rs/clap/latest/clap/
|
||||||
164
skills/clap-patterns/examples/quick-start.md
Normal file
164
skills/clap-patterns/examples/quick-start.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Clap Quick Start Guide
|
||||||
|
|
||||||
|
This guide will help you build your first Clap CLI application in minutes.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Rust installed (1.70.0 or newer)
|
||||||
|
- Cargo (comes with Rust)
|
||||||
|
|
||||||
|
## Step 1: Create a New Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo new my-cli
|
||||||
|
cd my-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Add Clap Dependency
|
||||||
|
|
||||||
|
Edit `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Write Your First CLI
|
||||||
|
|
||||||
|
Replace `src/main.rs` with:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// Simple program to greet a person
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Name of the person to greet
|
||||||
|
#[arg(short, long)]
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// Number of times to greet
|
||||||
|
#[arg(short, long, default_value_t = 1)]
|
||||||
|
count: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
for _ in 0..args.count {
|
||||||
|
println!("Hello {}!", args.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Build and Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the project
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run with arguments
|
||||||
|
./target/release/my-cli --name Alice --count 3
|
||||||
|
|
||||||
|
# Check help output
|
||||||
|
./target/release/my-cli --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./target/release/my-cli --name Alice --count 3
|
||||||
|
Hello Alice!
|
||||||
|
Hello Alice!
|
||||||
|
Hello Alice!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Help Output
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./target/release/my-cli --help
|
||||||
|
Simple program to greet a person
|
||||||
|
|
||||||
|
Usage: my-cli --name <NAME> [--count <COUNT>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-n, --name <NAME> Name of the person to greet
|
||||||
|
-c, --count <COUNT> Number of times to greet [default: 1]
|
||||||
|
-h, --help Print help
|
||||||
|
-V, --version Print version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Add Subcommands**: See `subcommands.rs` template
|
||||||
|
2. **Add Validation**: See `value-parser.rs` template
|
||||||
|
3. **Environment Variables**: See `env-variables.rs` template
|
||||||
|
4. **Type-Safe Options**: See `value-enum.rs` template
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Optional Arguments
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<String>,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Values
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(short, long, num_args = 1..)]
|
||||||
|
files: Vec<PathBuf>,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Boolean Flags
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Default Value
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(short, long, default_value = "config.toml")]
|
||||||
|
config: String,
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Unless Present
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(long, required_unless_present = "config")]
|
||||||
|
database_url: Option<String>,
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Parser trait not found"
|
||||||
|
|
||||||
|
Add the import:
|
||||||
|
```rust
|
||||||
|
use clap::Parser;
|
||||||
|
```
|
||||||
|
|
||||||
|
### "derive feature not enabled"
|
||||||
|
|
||||||
|
Update `Cargo.toml`:
|
||||||
|
```toml
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Help text not showing
|
||||||
|
|
||||||
|
Add doc comments above fields:
|
||||||
|
```rust
|
||||||
|
/// This shows up in --help output
|
||||||
|
#[arg(short, long)]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- Full templates: `skills/clap-patterns/templates/`
|
||||||
|
- Helper scripts: `skills/clap-patterns/scripts/`
|
||||||
|
- Official docs: https://docs.rs/clap/latest/clap/
|
||||||
474
skills/clap-patterns/examples/real-world-cli.md
Normal file
474
skills/clap-patterns/examples/real-world-cli.md
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
# Real-World Clap CLI Example
|
||||||
|
|
||||||
|
A complete, production-ready CLI application demonstrating best practices.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
my-tool/
|
||||||
|
├── Cargo.toml
|
||||||
|
├── src/
|
||||||
|
│ ├── main.rs # CLI definition and entry point
|
||||||
|
│ ├── commands/ # Command implementations
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── init.rs
|
||||||
|
│ │ ├── build.rs
|
||||||
|
│ │ └── deploy.rs
|
||||||
|
│ ├── config.rs # Configuration management
|
||||||
|
│ └── utils.rs # Helper functions
|
||||||
|
├── tests/
|
||||||
|
│ └── cli_tests.rs # Integration tests
|
||||||
|
└── completions/ # Generated shell completions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cargo.toml
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[package]
|
||||||
|
name = "my-tool"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5", features = ["derive", "env", "cargo"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
colored = "2.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
assert_cmd = "2.0"
|
||||||
|
predicates = "3.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
## main.rs - Complete CLI Definition
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
mod commands;
|
||||||
|
mod config;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "my-tool")]
|
||||||
|
#[command(author, version, about = "A production-ready CLI tool", long_about = None)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
struct Cli {
|
||||||
|
/// Configuration file
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
env = "MY_TOOL_CONFIG",
|
||||||
|
global = true,
|
||||||
|
default_value = "config.json"
|
||||||
|
)]
|
||||||
|
config: PathBuf,
|
||||||
|
|
||||||
|
/// Enable verbose output
|
||||||
|
#[arg(short, long, global = true)]
|
||||||
|
verbose: bool,
|
||||||
|
|
||||||
|
/// Output format
|
||||||
|
#[arg(short = 'F', long, value_enum, global = true, default_value_t = OutputFormat::Text)]
|
||||||
|
format: OutputFormat,
|
||||||
|
|
||||||
|
/// Log file path
|
||||||
|
#[arg(long, env = "MY_TOOL_LOG", global = true)]
|
||||||
|
log_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Initialize a new project
|
||||||
|
Init {
|
||||||
|
/// Project directory
|
||||||
|
#[arg(default_value = ".")]
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Project name
|
||||||
|
#[arg(short, long)]
|
||||||
|
name: Option<String>,
|
||||||
|
|
||||||
|
/// Project template
|
||||||
|
#[arg(short, long, value_enum, default_value_t = Template::Default)]
|
||||||
|
template: Template,
|
||||||
|
|
||||||
|
/// Skip interactive prompts
|
||||||
|
#[arg(short = 'y', long)]
|
||||||
|
yes: bool,
|
||||||
|
|
||||||
|
/// Git repository URL
|
||||||
|
#[arg(short, long)]
|
||||||
|
git: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Build the project
|
||||||
|
Build {
|
||||||
|
/// Build profile
|
||||||
|
#[arg(short, long, value_enum, default_value_t = Profile::Debug)]
|
||||||
|
profile: Profile,
|
||||||
|
|
||||||
|
/// Number of parallel jobs
|
||||||
|
#[arg(short, long, value_parser = clap::value_parser!(u8).range(1..=32), default_value_t = 4)]
|
||||||
|
jobs: u8,
|
||||||
|
|
||||||
|
/// Target directory
|
||||||
|
#[arg(short, long, default_value = "target")]
|
||||||
|
target: PathBuf,
|
||||||
|
|
||||||
|
/// Clean before building
|
||||||
|
#[arg(long)]
|
||||||
|
clean: bool,
|
||||||
|
|
||||||
|
/// Watch for changes
|
||||||
|
#[arg(short, long)]
|
||||||
|
watch: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Deploy to environment
|
||||||
|
Deploy {
|
||||||
|
/// Target environment
|
||||||
|
#[arg(value_enum)]
|
||||||
|
environment: Environment,
|
||||||
|
|
||||||
|
/// Deployment version/tag
|
||||||
|
#[arg(short, long)]
|
||||||
|
version: String,
|
||||||
|
|
||||||
|
/// Dry run (don't actually deploy)
|
||||||
|
#[arg(short = 'n', long)]
|
||||||
|
dry_run: bool,
|
||||||
|
|
||||||
|
/// Skip pre-deployment checks
|
||||||
|
#[arg(long)]
|
||||||
|
skip_checks: bool,
|
||||||
|
|
||||||
|
/// Deployment timeout in seconds
|
||||||
|
#[arg(short, long, default_value_t = 300)]
|
||||||
|
timeout: u64,
|
||||||
|
|
||||||
|
/// Rollback on failure
|
||||||
|
#[arg(long)]
|
||||||
|
rollback: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Manage configuration
|
||||||
|
Config {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: ConfigAction,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Generate shell completions
|
||||||
|
Completions {
|
||||||
|
/// Shell type
|
||||||
|
#[arg(value_enum)]
|
||||||
|
shell: Shell,
|
||||||
|
|
||||||
|
/// Output directory
|
||||||
|
#[arg(short, long, default_value = "completions")]
|
||||||
|
output: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum ConfigAction {
|
||||||
|
/// Show current configuration
|
||||||
|
Show,
|
||||||
|
|
||||||
|
/// Set a configuration value
|
||||||
|
Set {
|
||||||
|
/// Configuration key
|
||||||
|
key: String,
|
||||||
|
|
||||||
|
/// Configuration value
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Get a configuration value
|
||||||
|
Get {
|
||||||
|
/// Configuration key
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Reset configuration to defaults
|
||||||
|
Reset {
|
||||||
|
/// Confirm reset
|
||||||
|
#[arg(short = 'y', long)]
|
||||||
|
yes: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum OutputFormat {
|
||||||
|
Text,
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Template {
|
||||||
|
Default,
|
||||||
|
Minimal,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Profile {
|
||||||
|
Debug,
|
||||||
|
Release,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Environment {
|
||||||
|
Dev,
|
||||||
|
Staging,
|
||||||
|
Prod,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Shell {
|
||||||
|
Bash,
|
||||||
|
Zsh,
|
||||||
|
Fish,
|
||||||
|
PowerShell,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
if let Some(log_file) = &cli.log_file {
|
||||||
|
utils::init_file_logging(log_file, cli.verbose)?;
|
||||||
|
} else {
|
||||||
|
utils::init_console_logging(cli.verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
let config = config::load(&cli.config)?;
|
||||||
|
|
||||||
|
// Execute command
|
||||||
|
match &cli.command {
|
||||||
|
Commands::Init {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
template,
|
||||||
|
yes,
|
||||||
|
git,
|
||||||
|
} => {
|
||||||
|
commands::init::execute(path, name.as_deref(), *template, *yes, git.as_deref()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Build {
|
||||||
|
profile,
|
||||||
|
jobs,
|
||||||
|
target,
|
||||||
|
clean,
|
||||||
|
watch,
|
||||||
|
} => {
|
||||||
|
commands::build::execute(*profile, *jobs, target, *clean, *watch).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Deploy {
|
||||||
|
environment,
|
||||||
|
version,
|
||||||
|
dry_run,
|
||||||
|
skip_checks,
|
||||||
|
timeout,
|
||||||
|
rollback,
|
||||||
|
} => {
|
||||||
|
commands::deploy::execute(
|
||||||
|
*environment,
|
||||||
|
version,
|
||||||
|
*dry_run,
|
||||||
|
*skip_checks,
|
||||||
|
*timeout,
|
||||||
|
*rollback,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Config { action } => match action {
|
||||||
|
ConfigAction::Show => config::show(&config, cli.format),
|
||||||
|
ConfigAction::Set { key, value } => config::set(&cli.config, key, value)?,
|
||||||
|
ConfigAction::Get { key } => config::get(&config, key, cli.format)?,
|
||||||
|
ConfigAction::Reset { yes } => config::reset(&cli.config, *yes)?,
|
||||||
|
},
|
||||||
|
|
||||||
|
Commands::Completions { shell, output } => {
|
||||||
|
commands::completions::generate(*shell, output)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features Demonstrated
|
||||||
|
|
||||||
|
### 1. Global Arguments
|
||||||
|
|
||||||
|
Arguments available to all subcommands:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(short, long, global = true)]
|
||||||
|
verbose: bool,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Environment Variables
|
||||||
|
|
||||||
|
Fallback to environment variables:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(short, long, env = "MY_TOOL_CONFIG")]
|
||||||
|
config: PathBuf,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Validation
|
||||||
|
|
||||||
|
Numeric range validation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(short, long, value_parser = clap::value_parser!(u8).range(1..=32))]
|
||||||
|
jobs: u8,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Type-Safe Enums
|
||||||
|
|
||||||
|
Constrained choices with ValueEnum:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(ValueEnum)]
|
||||||
|
enum Environment {
|
||||||
|
Dev,
|
||||||
|
Staging,
|
||||||
|
Prod,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Nested Subcommands
|
||||||
|
|
||||||
|
Multi-level command structure:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Config {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: ConfigAction,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Default Values
|
||||||
|
|
||||||
|
Sensible defaults for all options:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[arg(short, long, default_value = "config.json")]
|
||||||
|
config: PathBuf,
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
`tests/cli_tests.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use assert_cmd::Command;
|
||||||
|
use predicates::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_help() {
|
||||||
|
let mut cmd = Command::cargo_bin("my-tool").unwrap();
|
||||||
|
cmd.arg("--help")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("A production-ready CLI tool"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version() {
|
||||||
|
let mut cmd = Command::cargo_bin("my-tool").unwrap();
|
||||||
|
cmd.arg("--version")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("1.0.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_command() {
|
||||||
|
let mut cmd = Command::cargo_bin("my-tool").unwrap();
|
||||||
|
cmd.arg("init")
|
||||||
|
.arg("--name")
|
||||||
|
.arg("test-project")
|
||||||
|
.arg("--yes")
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build release binary
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Generate completions
|
||||||
|
./target/release/my-tool completions bash
|
||||||
|
./target/release/my-tool completions zsh
|
||||||
|
./target/release/my-tool completions fish
|
||||||
|
|
||||||
|
# Install locally
|
||||||
|
cargo install --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
### Cross-Platform Binaries
|
||||||
|
|
||||||
|
Use `cross` for cross-compilation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install cross
|
||||||
|
cross build --release --target x86_64-unknown-linux-gnu
|
||||||
|
cross build --release --target x86_64-pc-windows-gnu
|
||||||
|
cross build --release --target x86_64-apple-darwin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package for Distribution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/macOS tar.gz
|
||||||
|
tar czf my-tool-linux-x64.tar.gz -C target/release my-tool
|
||||||
|
|
||||||
|
# Windows zip
|
||||||
|
zip my-tool-windows-x64.zip target/release/my-tool.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices Checklist
|
||||||
|
|
||||||
|
- ✓ Clear, descriptive help text
|
||||||
|
- ✓ Sensible default values
|
||||||
|
- ✓ Environment variable support
|
||||||
|
- ✓ Input validation
|
||||||
|
- ✓ Type-safe options (ValueEnum)
|
||||||
|
- ✓ Global arguments for common options
|
||||||
|
- ✓ Proper error handling (anyhow)
|
||||||
|
- ✓ Integration tests
|
||||||
|
- ✓ Shell completion generation
|
||||||
|
- ✓ Version information
|
||||||
|
- ✓ Verbose/quiet modes
|
||||||
|
- ✓ Configuration file support
|
||||||
|
- ✓ Dry-run mode for destructive operations
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- Full templates: `skills/clap-patterns/templates/`
|
||||||
|
- Validation examples: `examples/validation-examples.md`
|
||||||
|
- Test scripts: `scripts/test-cli.sh`
|
||||||
300
skills/clap-patterns/examples/validation-examples.md
Normal file
300
skills/clap-patterns/examples/validation-examples.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# Clap Validation Examples
|
||||||
|
|
||||||
|
Comprehensive examples for validating CLI input with Clap value parsers.
|
||||||
|
|
||||||
|
## 1. Port Number Validation
|
||||||
|
|
||||||
|
Validate port numbers are in the valid range (1-65535):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
const PORT_RANGE: RangeInclusive<usize> = 1..=65535;
|
||||||
|
|
||||||
|
fn port_in_range(s: &str) -> Result<u16, String> {
|
||||||
|
let port: usize = s
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("`{}` isn't a valid port number", s))?;
|
||||||
|
|
||||||
|
if PORT_RANGE.contains(&port) {
|
||||||
|
Ok(port as u16)
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"port not in range {}-{}",
|
||||||
|
PORT_RANGE.start(),
|
||||||
|
PORT_RANGE.end()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(short, long, value_parser = port_in_range)]
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
$ my-cli --port 8080 # ✓ Valid
|
||||||
|
$ my-cli --port 80000 # ❌ Error: port not in range 1-65535
|
||||||
|
$ my-cli --port abc # ❌ Error: `abc` isn't a valid port number
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Email Validation
|
||||||
|
|
||||||
|
Basic email format validation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn validate_email(s: &str) -> Result<String, String> {
|
||||||
|
if s.contains('@') && s.contains('.') && s.len() > 5 {
|
||||||
|
Ok(s.to_string())
|
||||||
|
} else {
|
||||||
|
Err(format!("`{}` is not a valid email address", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(short, long, value_parser = validate_email)]
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. File/Directory Existence
|
||||||
|
|
||||||
|
Validate that files or directories exist:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn file_exists(s: &str) -> Result<PathBuf, String> {
|
||||||
|
let path = PathBuf::from(s);
|
||||||
|
if path.exists() && path.is_file() {
|
||||||
|
Ok(path)
|
||||||
|
} else {
|
||||||
|
Err(format!("file does not exist: {}", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir_exists(s: &str) -> Result<PathBuf, String> {
|
||||||
|
let path = PathBuf::from(s);
|
||||||
|
if path.exists() && path.is_dir() {
|
||||||
|
Ok(path)
|
||||||
|
} else {
|
||||||
|
Err(format!("directory does not exist: {}", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(short, long, value_parser = file_exists)]
|
||||||
|
input: PathBuf,
|
||||||
|
|
||||||
|
#[arg(short, long, value_parser = dir_exists)]
|
||||||
|
output_dir: PathBuf,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. URL Validation
|
||||||
|
|
||||||
|
Validate URL format:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn validate_url(s: &str) -> Result<String, String> {
|
||||||
|
if s.starts_with("http://") || s.starts_with("https://") {
|
||||||
|
Ok(s.to_string())
|
||||||
|
} else {
|
||||||
|
Err(format!("`{}` must start with http:// or https://", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(short, long, value_parser = validate_url)]
|
||||||
|
endpoint: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Numeric Range Validation
|
||||||
|
|
||||||
|
Use built-in range validation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
/// Port (1-65535)
|
||||||
|
#[arg(long, value_parser = clap::value_parser!(u16).range(1..=65535))]
|
||||||
|
port: u16,
|
||||||
|
|
||||||
|
/// Threads (1-32)
|
||||||
|
#[arg(long, value_parser = clap::value_parser!(u8).range(1..=32))]
|
||||||
|
threads: u8,
|
||||||
|
|
||||||
|
/// Percentage (0-100)
|
||||||
|
#[arg(long, value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||||
|
percentage: u8,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Regex Pattern Validation
|
||||||
|
|
||||||
|
Validate against regex patterns:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
fn validate_version(s: &str) -> Result<String, String> {
|
||||||
|
let re = Regex::new(r"^\d+\.\d+\.\d+$").unwrap();
|
||||||
|
if re.is_match(s) {
|
||||||
|
Ok(s.to_string())
|
||||||
|
} else {
|
||||||
|
Err(format!("`{}` is not a valid semantic version (e.g., 1.2.3)", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(short, long, value_parser = validate_version)]
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Add `regex = "1"` to `Cargo.toml` for this example.
|
||||||
|
|
||||||
|
## 7. Multiple Validation Rules
|
||||||
|
|
||||||
|
Combine multiple validation rules:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn validate_username(s: &str) -> Result<String, String> {
|
||||||
|
// Must be 3-20 characters
|
||||||
|
if s.len() < 3 || s.len() > 20 {
|
||||||
|
return Err("username must be 3-20 characters".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must start with letter
|
||||||
|
if !s.chars().next().unwrap().is_alphabetic() {
|
||||||
|
return Err("username must start with a letter".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only alphanumeric and underscore
|
||||||
|
if !s.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
||||||
|
return Err("username can only contain letters, numbers, and underscores".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(short, long, value_parser = validate_username)]
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Conditional Validation
|
||||||
|
|
||||||
|
Validate based on other arguments:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
/// Enable SSL
|
||||||
|
#[arg(long)]
|
||||||
|
ssl: bool,
|
||||||
|
|
||||||
|
/// SSL certificate (required if --ssl is set)
|
||||||
|
#[arg(long, required_if_eq("ssl", "true"))]
|
||||||
|
cert: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// SSL key (required if --ssl is set)
|
||||||
|
#[arg(long, required_if_eq("ssl", "true"))]
|
||||||
|
key: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Mutually Exclusive Arguments
|
||||||
|
|
||||||
|
Ensure only one option is provided:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
/// Use JSON format
|
||||||
|
#[arg(long, conflicts_with = "yaml")]
|
||||||
|
json: bool,
|
||||||
|
|
||||||
|
/// Use YAML format
|
||||||
|
#[arg(long, conflicts_with = "json")]
|
||||||
|
yaml: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Custom Type with FromStr
|
||||||
|
|
||||||
|
Implement `FromStr` for automatic parsing:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
struct IpPort {
|
||||||
|
ip: std::net::IpAddr,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for IpPort {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let parts: Vec<&str> = s.split(':').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err("format must be IP:PORT (e.g., 127.0.0.1:8080)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip = parts[0]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("invalid IP address: {}", parts[0]))?;
|
||||||
|
|
||||||
|
let port = parts[1]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("invalid port: {}", parts[1]))?;
|
||||||
|
|
||||||
|
Ok(IpPort { ip, port })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
/// Bind address (IP:PORT)
|
||||||
|
#[arg(short, long)]
|
||||||
|
bind: IpPort,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
$ my-cli --bind 127.0.0.1:8080 # ✓ Valid
|
||||||
|
$ my-cli --bind 192.168.1.1:3000 # ✓ Valid
|
||||||
|
$ my-cli --bind invalid # ❌ Error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Validation
|
||||||
|
|
||||||
|
Use the provided test script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/test-cli.sh ./target/debug/my-cli validation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Provide Clear Error Messages**: Tell users what went wrong and how to fix it
|
||||||
|
2. **Validate Early**: Use value parsers instead of validating after parsing
|
||||||
|
3. **Use Type System**: Leverage Rust's type system for compile-time safety
|
||||||
|
4. **Document Constraints**: Add constraints to help text
|
||||||
|
5. **Test Edge Cases**: Test boundary values and invalid inputs
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- Value parser template: `templates/value-parser.rs`
|
||||||
|
- Test script: `scripts/test-cli.sh`
|
||||||
|
- Clap docs: https://docs.rs/clap/latest/clap/
|
||||||
94
skills/clap-patterns/scripts/generate-completions.sh
Executable file
94
skills/clap-patterns/scripts/generate-completions.sh
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate shell completions for Clap CLI applications
|
||||||
|
#
|
||||||
|
# Usage: ./generate-completions.sh <binary-name> [output-dir]
|
||||||
|
#
|
||||||
|
# This script generates shell completion scripts for bash, zsh, fish, and powershell.
|
||||||
|
# The CLI binary must support the --generate-completions flag (built with Clap).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BINARY="${1:-}"
|
||||||
|
OUTPUT_DIR="${2:-completions}"
|
||||||
|
|
||||||
|
if [ -z "$BINARY" ]; then
|
||||||
|
echo "Error: Binary name required"
|
||||||
|
echo "Usage: $0 <binary-name> [output-dir]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
echo "Generating shell completions for: $BINARY"
|
||||||
|
echo "Output directory: $OUTPUT_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if binary exists
|
||||||
|
if ! command -v "$BINARY" &> /dev/null; then
|
||||||
|
echo "Warning: Binary '$BINARY' not found in PATH"
|
||||||
|
echo "Make sure to build and install it first: cargo install --path ."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate completions for each shell
|
||||||
|
for shell in bash zsh fish powershell elvish; do
|
||||||
|
echo "Generating $shell completions..."
|
||||||
|
|
||||||
|
case "$shell" in
|
||||||
|
bash)
|
||||||
|
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/${BINARY}.bash" 2>/dev/null || {
|
||||||
|
echo " ⚠️ Failed (CLI may not support --generate-completion)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
echo " ✓ Generated: $OUTPUT_DIR/${BINARY}.bash"
|
||||||
|
;;
|
||||||
|
zsh)
|
||||||
|
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/_${BINARY}" 2>/dev/null || {
|
||||||
|
echo " ⚠️ Failed"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
echo " ✓ Generated: $OUTPUT_DIR/_${BINARY}"
|
||||||
|
;;
|
||||||
|
fish)
|
||||||
|
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/${BINARY}.fish" 2>/dev/null || {
|
||||||
|
echo " ⚠️ Failed"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
echo " ✓ Generated: $OUTPUT_DIR/${BINARY}.fish"
|
||||||
|
;;
|
||||||
|
powershell)
|
||||||
|
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/_${BINARY}.ps1" 2>/dev/null || {
|
||||||
|
echo " ⚠️ Failed"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
echo " ✓ Generated: $OUTPUT_DIR/_${BINARY}.ps1"
|
||||||
|
;;
|
||||||
|
elvish)
|
||||||
|
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/${BINARY}.elv" 2>/dev/null || {
|
||||||
|
echo " ⚠️ Failed"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
echo " ✓ Generated: $OUTPUT_DIR/${BINARY}.elv"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Completion generation complete!"
|
||||||
|
echo ""
|
||||||
|
echo "Installation instructions:"
|
||||||
|
echo ""
|
||||||
|
echo "Bash:"
|
||||||
|
echo " sudo cp $OUTPUT_DIR/${BINARY}.bash /etc/bash_completion.d/"
|
||||||
|
echo " Or: echo 'source $PWD/$OUTPUT_DIR/${BINARY}.bash' >> ~/.bashrc"
|
||||||
|
echo ""
|
||||||
|
echo "Zsh:"
|
||||||
|
echo " cp $OUTPUT_DIR/_${BINARY} /usr/local/share/zsh/site-functions/"
|
||||||
|
echo " Or add to fpath: fpath=($PWD/$OUTPUT_DIR \$fpath)"
|
||||||
|
echo ""
|
||||||
|
echo "Fish:"
|
||||||
|
echo " cp $OUTPUT_DIR/${BINARY}.fish ~/.config/fish/completions/"
|
||||||
|
echo ""
|
||||||
|
echo "PowerShell:"
|
||||||
|
echo " Add to profile: . $PWD/$OUTPUT_DIR/_${BINARY}.ps1"
|
||||||
159
skills/clap-patterns/scripts/test-cli.sh
Executable file
159
skills/clap-patterns/scripts/test-cli.sh
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Test a Clap CLI application with various argument combinations
|
||||||
|
#
|
||||||
|
# Usage: ./test-cli.sh <binary-path> [test-suite]
|
||||||
|
#
|
||||||
|
# Test suites: basic, subcommands, validation, env, all (default)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BINARY="${1:-}"
|
||||||
|
TEST_SUITE="${2:-all}"
|
||||||
|
|
||||||
|
if [ -z "$BINARY" ]; then
|
||||||
|
echo "Error: Binary path required"
|
||||||
|
echo "Usage: $0 <binary-path> [test-suite]"
|
||||||
|
echo ""
|
||||||
|
echo "Test suites:"
|
||||||
|
echo " basic - Test help, version, basic flags"
|
||||||
|
echo " subcommands - Test subcommand routing"
|
||||||
|
echo " validation - Test input validation"
|
||||||
|
echo " env - Test environment variables"
|
||||||
|
echo " all - Run all tests (default)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$BINARY" ]; then
|
||||||
|
echo "Error: Binary not found or not executable: $BINARY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
local name="$1"
|
||||||
|
local expected_exit="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
echo -n "Testing: $name ... "
|
||||||
|
|
||||||
|
if "$BINARY" "$@" &>/dev/null; then
|
||||||
|
actual_exit=0
|
||||||
|
else
|
||||||
|
actual_exit=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
||||||
|
echo "✓ PASS"
|
||||||
|
((PASS++))
|
||||||
|
else
|
||||||
|
echo "❌ FAIL (expected exit $expected_exit, got $actual_exit)"
|
||||||
|
((FAIL++))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_basic() {
|
||||||
|
echo ""
|
||||||
|
echo "=== Basic Tests ==="
|
||||||
|
|
||||||
|
run_test "Help output" 0 --help
|
||||||
|
run_test "Version output" 0 --version
|
||||||
|
run_test "Short help" 0 -h
|
||||||
|
run_test "Invalid flag" 1 --invalid-flag
|
||||||
|
run_test "No arguments (might fail for some CLIs)" 0
|
||||||
|
}
|
||||||
|
|
||||||
|
test_subcommands() {
|
||||||
|
echo ""
|
||||||
|
echo "=== Subcommand Tests ==="
|
||||||
|
|
||||||
|
run_test "Subcommand help" 0 help
|
||||||
|
run_test "Invalid subcommand" 1 invalid-command
|
||||||
|
|
||||||
|
# Try common subcommands
|
||||||
|
for cmd in init add build test deploy; do
|
||||||
|
if "$BINARY" help 2>&1 | grep -q "$cmd"; then
|
||||||
|
run_test "Subcommand '$cmd' help" 0 "$cmd" --help
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
test_validation() {
|
||||||
|
echo ""
|
||||||
|
echo "=== Validation Tests ==="
|
||||||
|
|
||||||
|
# Test file arguments with non-existent files
|
||||||
|
run_test "Non-existent file" 1 --input /nonexistent/file.txt
|
||||||
|
|
||||||
|
# Test numeric ranges
|
||||||
|
run_test "Invalid number" 1 --count abc
|
||||||
|
run_test "Negative number" 1 --count -5
|
||||||
|
|
||||||
|
# Test conflicting flags
|
||||||
|
if "$BINARY" --help 2>&1 | grep -q "conflicts with"; then
|
||||||
|
echo " (Found conflicting arguments in help text)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_env() {
|
||||||
|
echo ""
|
||||||
|
echo "=== Environment Variable Tests ==="
|
||||||
|
|
||||||
|
# Check if binary supports environment variables
|
||||||
|
if "$BINARY" --help 2>&1 | grep -q "\[env:"; then
|
||||||
|
echo "✓ Environment variable support detected"
|
||||||
|
|
||||||
|
# Extract env vars from help text
|
||||||
|
ENV_VARS=$("$BINARY" --help 2>&1 | grep -o '\[env: [A-Z_]*\]' | sed 's/\[env: \(.*\)\]/\1/' || true)
|
||||||
|
|
||||||
|
if [ -n "$ENV_VARS" ]; then
|
||||||
|
echo "Found environment variables:"
|
||||||
|
echo "$ENV_VARS" | while read -r var; do
|
||||||
|
echo " - $var"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " No environment variable support detected"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run requested test suite
|
||||||
|
case "$TEST_SUITE" in
|
||||||
|
basic)
|
||||||
|
test_basic
|
||||||
|
;;
|
||||||
|
subcommands)
|
||||||
|
test_subcommands
|
||||||
|
;;
|
||||||
|
validation)
|
||||||
|
test_validation
|
||||||
|
;;
|
||||||
|
env)
|
||||||
|
test_env
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
test_basic
|
||||||
|
test_subcommands
|
||||||
|
test_validation
|
||||||
|
test_env
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown test suite: $TEST_SUITE"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Test Summary ==="
|
||||||
|
echo "Passed: $PASS"
|
||||||
|
echo "Failed: $FAIL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
echo "❌ Some tests failed"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✓ All tests passed!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
113
skills/clap-patterns/scripts/validate-cargo.sh
Executable file
113
skills/clap-patterns/scripts/validate-cargo.sh
Executable file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Validate Cargo.toml for correct Clap configuration
|
||||||
|
#
|
||||||
|
# Usage: ./validate-cargo.sh [path-to-Cargo.toml]
|
||||||
|
#
|
||||||
|
# Checks:
|
||||||
|
# - Clap dependency exists
|
||||||
|
# - Clap version is 4.x or newer
|
||||||
|
# - Required features are enabled (derive)
|
||||||
|
# - Optional features (env, cargo) are present if needed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CARGO_TOML="${1:-Cargo.toml}"
|
||||||
|
|
||||||
|
if [ ! -f "$CARGO_TOML" ]; then
|
||||||
|
echo "❌ Error: $CARGO_TOML not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Validating Clap configuration in: $CARGO_TOML"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if clap is listed as a dependency
|
||||||
|
if ! grep -q "clap" "$CARGO_TOML"; then
|
||||||
|
echo "❌ Clap not found in dependencies"
|
||||||
|
echo ""
|
||||||
|
echo "Add to $CARGO_TOML:"
|
||||||
|
echo ""
|
||||||
|
echo '[dependencies]'
|
||||||
|
echo 'clap = { version = "4.5", features = ["derive"] }'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ Clap dependency found"
|
||||||
|
|
||||||
|
# Extract clap version
|
||||||
|
VERSION=$(grep -A 5 '^\[dependencies\]' "$CARGO_TOML" | grep 'clap' | head -1)
|
||||||
|
|
||||||
|
# Check version
|
||||||
|
if echo "$VERSION" | grep -q '"4\.' || echo "$VERSION" | grep -q "'4\."; then
|
||||||
|
echo "✓ Clap version 4.x detected"
|
||||||
|
elif echo "$VERSION" | grep -q '"3\.' || echo "$VERSION" | grep -q "'3\."; then
|
||||||
|
echo "⚠️ Warning: Clap version 3.x detected"
|
||||||
|
echo " Consider upgrading to 4.x for latest features"
|
||||||
|
else
|
||||||
|
echo "⚠️ Warning: Could not determine Clap version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for derive feature
|
||||||
|
if echo "$VERSION" | grep -q 'features.*derive' || echo "$VERSION" | grep -q 'derive.*features'; then
|
||||||
|
echo "✓ 'derive' feature enabled"
|
||||||
|
else
|
||||||
|
echo "❌ 'derive' feature not found"
|
||||||
|
echo " Add: features = [\"derive\"]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for optional but recommended features
|
||||||
|
echo ""
|
||||||
|
echo "Optional features:"
|
||||||
|
|
||||||
|
if echo "$VERSION" | grep -q '"env"' || echo "$VERSION" | grep -q "'env'"; then
|
||||||
|
echo "✓ 'env' feature enabled (environment variable support)"
|
||||||
|
else
|
||||||
|
echo " 'env' feature not enabled"
|
||||||
|
echo " Add for environment variable support: features = [\"derive\", \"env\"]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$VERSION" | grep -q '"cargo"' || echo "$VERSION" | grep -q "'cargo'"; then
|
||||||
|
echo "✓ 'cargo' feature enabled (automatic version from Cargo.toml)"
|
||||||
|
else
|
||||||
|
echo " 'cargo' feature not enabled"
|
||||||
|
echo " Add for automatic version: features = [\"derive\", \"cargo\"]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$VERSION" | grep -q '"color"' || echo "$VERSION" | grep -q "'color'"; then
|
||||||
|
echo "✓ 'color' feature enabled (colored output)"
|
||||||
|
else
|
||||||
|
echo " 'color' feature not enabled"
|
||||||
|
echo " Add for colored help: features = [\"derive\", \"color\"]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for common patterns in src/
|
||||||
|
if [ -d "src" ]; then
|
||||||
|
echo "Checking source files for Clap usage patterns..."
|
||||||
|
|
||||||
|
if grep -r "use clap::Parser" src/ &>/dev/null; then
|
||||||
|
echo "✓ Parser trait usage found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -r "use clap::Subcommand" src/ &>/dev/null; then
|
||||||
|
echo "✓ Subcommand trait usage found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -r "use clap::ValueEnum" src/ &>/dev/null; then
|
||||||
|
echo "✓ ValueEnum trait usage found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -r "#\[derive(Parser)\]" src/ &>/dev/null; then
|
||||||
|
echo "✓ Parser derive macro usage found"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Validation complete!"
|
||||||
|
echo ""
|
||||||
|
echo "Recommended Cargo.toml configuration:"
|
||||||
|
echo ""
|
||||||
|
echo '[dependencies]'
|
||||||
|
echo 'clap = { version = "4.5", features = ["derive", "env", "cargo"] }'
|
||||||
66
skills/clap-patterns/templates/basic-parser.rs
Normal file
66
skills/clap-patterns/templates/basic-parser.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/// Basic Parser Template with Clap Derive Macros
|
||||||
|
///
|
||||||
|
/// This template demonstrates:
|
||||||
|
/// - Parser derive macro
|
||||||
|
/// - Argument attributes (short, long, default_value)
|
||||||
|
/// - PathBuf for file handling
|
||||||
|
/// - Boolean flags
|
||||||
|
/// - Doc comments as help text
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "myapp")]
|
||||||
|
#[command(author = "Your Name <you@example.com>")]
|
||||||
|
#[command(version = "1.0.0")]
|
||||||
|
#[command(about = "A simple CLI application", long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
/// Input file to process
|
||||||
|
#[arg(short, long, value_name = "FILE")]
|
||||||
|
input: PathBuf,
|
||||||
|
|
||||||
|
/// Optional output file
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Enable verbose output
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
|
||||||
|
/// Number of items to process
|
||||||
|
#[arg(short = 'c', long, default_value_t = 10)]
|
||||||
|
count: usize,
|
||||||
|
|
||||||
|
/// Dry run mode (don't make changes)
|
||||||
|
#[arg(short = 'n', long)]
|
||||||
|
dry_run: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
if cli.verbose {
|
||||||
|
println!("Input file: {:?}", cli.input);
|
||||||
|
println!("Output file: {:?}", cli.output);
|
||||||
|
println!("Count: {}", cli.count);
|
||||||
|
println!("Dry run: {}", cli.dry_run);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if input file exists
|
||||||
|
if !cli.input.exists() {
|
||||||
|
eprintln!("Error: Input file does not exist: {:?}", cli.input);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your processing logic here
|
||||||
|
println!("Processing {} with count {}...", cli.input.display(), cli.count);
|
||||||
|
|
||||||
|
if let Some(output) = cli.output {
|
||||||
|
if !cli.dry_run {
|
||||||
|
println!("Would write to: {}", output.display());
|
||||||
|
} else {
|
||||||
|
println!("Dry run: Skipping write to {}", output.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
skills/clap-patterns/templates/builder-pattern.rs
Normal file
112
skills/clap-patterns/templates/builder-pattern.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/// Builder Pattern Template (Manual API)
|
||||||
|
///
|
||||||
|
/// This template demonstrates the builder API for advanced use cases:
|
||||||
|
/// - Dynamic CLI construction
|
||||||
|
/// - Runtime configuration
|
||||||
|
/// - Custom help templates
|
||||||
|
/// - Complex validation logic
|
||||||
|
///
|
||||||
|
/// Note: Prefer derive macros unless you need this level of control.
|
||||||
|
|
||||||
|
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn build_cli() -> Command {
|
||||||
|
Command::new("advanced-cli")
|
||||||
|
.version("1.0.0")
|
||||||
|
.author("Your Name <you@example.com>")
|
||||||
|
.about("Advanced CLI using builder pattern")
|
||||||
|
.arg(
|
||||||
|
Arg::new("input")
|
||||||
|
.short('i')
|
||||||
|
.long("input")
|
||||||
|
.value_name("FILE")
|
||||||
|
.help("Input file to process")
|
||||||
|
.required(true)
|
||||||
|
.value_parser(clap::value_parser!(PathBuf)),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("output")
|
||||||
|
.short('o')
|
||||||
|
.long("output")
|
||||||
|
.value_name("FILE")
|
||||||
|
.help("Output file (optional)")
|
||||||
|
.value_parser(clap::value_parser!(PathBuf)),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("verbose")
|
||||||
|
.short('v')
|
||||||
|
.long("verbose")
|
||||||
|
.help("Enable verbose output")
|
||||||
|
.action(ArgAction::SetTrue),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("count")
|
||||||
|
.short('c')
|
||||||
|
.long("count")
|
||||||
|
.value_name("NUM")
|
||||||
|
.help("Number of items to process")
|
||||||
|
.default_value("10")
|
||||||
|
.value_parser(clap::value_parser!(usize)),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("format")
|
||||||
|
.short('f')
|
||||||
|
.long("format")
|
||||||
|
.value_name("FORMAT")
|
||||||
|
.help("Output format")
|
||||||
|
.value_parser(["json", "yaml", "toml"])
|
||||||
|
.default_value("json"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("tags")
|
||||||
|
.short('t')
|
||||||
|
.long("tag")
|
||||||
|
.value_name("TAG")
|
||||||
|
.help("Tags to apply (can be specified multiple times)")
|
||||||
|
.action(ArgAction::Append),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_args(matches: &ArgMatches) {
|
||||||
|
let input = matches.get_one::<PathBuf>("input").unwrap();
|
||||||
|
let output = matches.get_one::<PathBuf>("output");
|
||||||
|
let verbose = matches.get_flag("verbose");
|
||||||
|
let count = *matches.get_one::<usize>("count").unwrap();
|
||||||
|
let format = matches.get_one::<String>("format").unwrap();
|
||||||
|
let tags: Vec<_> = matches
|
||||||
|
.get_many::<String>("tags")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!("Configuration:");
|
||||||
|
println!(" Input: {:?}", input);
|
||||||
|
println!(" Output: {:?}", output);
|
||||||
|
println!(" Count: {}", count);
|
||||||
|
println!(" Format: {}", format);
|
||||||
|
println!(" Tags: {:?}", tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your processing logic here
|
||||||
|
println!("Processing {} items from {}", count, input.display());
|
||||||
|
|
||||||
|
if !tags.is_empty() {
|
||||||
|
println!("Applying tags: {}", tags.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(output_path) = output {
|
||||||
|
println!("Writing {} format to {}", format, output_path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let matches = build_cli().get_matches();
|
||||||
|
process_args(&matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// cargo run -- -i input.txt -o output.json -v -c 20 -f yaml -t alpha -t beta
|
||||||
|
// cargo run -- --input data.txt --format toml --tag important
|
||||||
99
skills/clap-patterns/templates/env-variables.rs
Normal file
99
skills/clap-patterns/templates/env-variables.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/// Environment Variable Integration Template
|
||||||
|
///
|
||||||
|
/// This template demonstrates:
|
||||||
|
/// - Reading from environment variables
|
||||||
|
/// - Fallback to CLI arguments
|
||||||
|
/// - Default values
|
||||||
|
/// - Sensitive data handling (API keys, tokens)
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "envapp")]
|
||||||
|
#[command(about = "CLI with environment variable support")]
|
||||||
|
struct Cli {
|
||||||
|
/// API key (or set API_KEY env var)
|
||||||
|
///
|
||||||
|
/// Sensitive data like API keys should preferably be set via environment
|
||||||
|
/// variables to avoid exposing them in shell history or process lists.
|
||||||
|
#[arg(long, env = "API_KEY", hide_env_values = true)]
|
||||||
|
api_key: String,
|
||||||
|
|
||||||
|
/// Database URL (or set DATABASE_URL env var)
|
||||||
|
#[arg(long, env = "DATABASE_URL")]
|
||||||
|
database_url: String,
|
||||||
|
|
||||||
|
/// Log level: debug, info, warn, error
|
||||||
|
///
|
||||||
|
/// Defaults to "info" if not provided via CLI or LOG_LEVEL env var.
|
||||||
|
#[arg(long, env = "LOG_LEVEL", default_value = "info")]
|
||||||
|
log_level: String,
|
||||||
|
|
||||||
|
/// Configuration file path
|
||||||
|
///
|
||||||
|
/// Reads from CONFIG_FILE env var, or uses default if not specified.
|
||||||
|
#[arg(long, env = "CONFIG_FILE", default_value = "config.toml")]
|
||||||
|
config: PathBuf,
|
||||||
|
|
||||||
|
/// Number of workers (default from env or 4)
|
||||||
|
#[arg(long, env = "WORKER_COUNT", default_value_t = 4)]
|
||||||
|
workers: usize,
|
||||||
|
|
||||||
|
/// Enable debug mode
|
||||||
|
///
|
||||||
|
/// Can be set via DEBUG=1 or --debug flag
|
||||||
|
#[arg(long, env = "DEBUG", value_parser = clap::value_parser!(bool))]
|
||||||
|
debug: bool,
|
||||||
|
|
||||||
|
/// Host to bind to
|
||||||
|
#[arg(long, env = "HOST", default_value = "127.0.0.1")]
|
||||||
|
host: String,
|
||||||
|
|
||||||
|
/// Port to listen on
|
||||||
|
#[arg(short, long, env = "PORT", default_value_t = 8080)]
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
println!("Configuration loaded:");
|
||||||
|
println!(" Database URL: {}", cli.database_url);
|
||||||
|
println!(" API Key: {}...", &cli.api_key[..4.min(cli.api_key.len())]);
|
||||||
|
println!(" Log level: {}", cli.log_level);
|
||||||
|
println!(" Config file: {}", cli.config.display());
|
||||||
|
println!(" Workers: {}", cli.workers);
|
||||||
|
println!(" Debug mode: {}", cli.debug);
|
||||||
|
println!(" Host: {}", cli.host);
|
||||||
|
println!(" Port: {}", cli.port);
|
||||||
|
|
||||||
|
// Initialize logging based on log_level
|
||||||
|
match cli.log_level.to_lowercase().as_str() {
|
||||||
|
"debug" => println!("Log level set to DEBUG"),
|
||||||
|
"info" => println!("Log level set to INFO"),
|
||||||
|
"warn" => println!("Log level set to WARN"),
|
||||||
|
"error" => println!("Log level set to ERROR"),
|
||||||
|
_ => println!("Unknown log level: {}", cli.log_level),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your application logic here
|
||||||
|
println!("\nStarting application...");
|
||||||
|
println!("Listening on {}:{}", cli.host, cli.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// 1. Set environment variables:
|
||||||
|
// export API_KEY="sk-1234567890abcdef"
|
||||||
|
// export DATABASE_URL="postgres://localhost/mydb"
|
||||||
|
// export LOG_LEVEL="debug"
|
||||||
|
// export WORKER_COUNT="8"
|
||||||
|
// cargo run
|
||||||
|
//
|
||||||
|
// 2. Override with CLI arguments:
|
||||||
|
// cargo run -- --api-key "other-key" --workers 16
|
||||||
|
//
|
||||||
|
// 3. Mix environment and CLI:
|
||||||
|
// export DATABASE_URL="postgres://localhost/mydb"
|
||||||
|
// cargo run -- --api-key "sk-1234" --debug
|
||||||
290
skills/clap-patterns/templates/full-featured-cli.rs
Normal file
290
skills/clap-patterns/templates/full-featured-cli.rs
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
/// Full-Featured CLI Template
|
||||||
|
///
|
||||||
|
/// This template combines all patterns:
|
||||||
|
/// - Parser derive with subcommands
|
||||||
|
/// - ValueEnum for type-safe options
|
||||||
|
/// - Environment variable support
|
||||||
|
/// - Custom value parsers
|
||||||
|
/// - Global arguments
|
||||||
|
/// - Comprehensive help text
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "myapp")]
|
||||||
|
#[command(author = "Your Name <you@example.com>")]
|
||||||
|
#[command(version = "1.0.0")]
|
||||||
|
#[command(about = "A full-featured CLI application", long_about = None)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
struct Cli {
|
||||||
|
/// Configuration file path
|
||||||
|
#[arg(short, long, env = "CONFIG_FILE", global = true)]
|
||||||
|
config: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Enable verbose output
|
||||||
|
#[arg(short, long, global = true)]
|
||||||
|
verbose: bool,
|
||||||
|
|
||||||
|
/// Output format
|
||||||
|
#[arg(short, long, value_enum, global = true, default_value_t = Format::Text)]
|
||||||
|
format: Format,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Initialize a new project
|
||||||
|
Init {
|
||||||
|
/// Project directory
|
||||||
|
#[arg(default_value = ".")]
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Project template
|
||||||
|
#[arg(short, long, value_enum, default_value_t = Template::Basic)]
|
||||||
|
template: Template,
|
||||||
|
|
||||||
|
/// Skip interactive prompts
|
||||||
|
#[arg(short = 'y', long)]
|
||||||
|
yes: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Build the project
|
||||||
|
Build {
|
||||||
|
/// Build mode
|
||||||
|
#[arg(short, long, value_enum, default_value_t = BuildMode::Debug)]
|
||||||
|
mode: BuildMode,
|
||||||
|
|
||||||
|
/// Number of parallel jobs
|
||||||
|
#[arg(short, long, value_parser = clap::value_parser!(u8).range(1..=32), default_value_t = 4)]
|
||||||
|
jobs: u8,
|
||||||
|
|
||||||
|
/// Target directory
|
||||||
|
#[arg(short, long, default_value = "target")]
|
||||||
|
target_dir: PathBuf,
|
||||||
|
|
||||||
|
/// Clean before building
|
||||||
|
#[arg(long)]
|
||||||
|
clean: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Test the project
|
||||||
|
Test {
|
||||||
|
/// Test name pattern
|
||||||
|
pattern: Option<String>,
|
||||||
|
|
||||||
|
/// Run ignored tests
|
||||||
|
#[arg(long)]
|
||||||
|
ignored: bool,
|
||||||
|
|
||||||
|
/// Number of test threads
|
||||||
|
#[arg(long, value_parser = clap::value_parser!(usize).range(1..))]
|
||||||
|
test_threads: Option<usize>,
|
||||||
|
|
||||||
|
/// Show output for passing tests
|
||||||
|
#[arg(long)]
|
||||||
|
nocapture: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Deploy the project
|
||||||
|
Deploy {
|
||||||
|
/// Deployment environment
|
||||||
|
#[arg(value_enum)]
|
||||||
|
environment: Environment,
|
||||||
|
|
||||||
|
/// Skip pre-deployment checks
|
||||||
|
#[arg(long)]
|
||||||
|
skip_checks: bool,
|
||||||
|
|
||||||
|
/// Deployment tag/version
|
||||||
|
#[arg(short, long)]
|
||||||
|
tag: Option<String>,
|
||||||
|
|
||||||
|
/// Deployment configuration
|
||||||
|
#[command(subcommand)]
|
||||||
|
config: Option<DeployConfig>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum DeployConfig {
|
||||||
|
/// Configure database settings
|
||||||
|
Database {
|
||||||
|
/// Database URL
|
||||||
|
#[arg(long, env = "DATABASE_URL")]
|
||||||
|
url: String,
|
||||||
|
|
||||||
|
/// Run migrations
|
||||||
|
#[arg(long)]
|
||||||
|
migrate: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Configure server settings
|
||||||
|
Server {
|
||||||
|
/// Server host
|
||||||
|
#[arg(long, default_value = "0.0.0.0")]
|
||||||
|
host: String,
|
||||||
|
|
||||||
|
/// Server port
|
||||||
|
#[arg(long, default_value_t = 8080, value_parser = port_in_range)]
|
||||||
|
port: u16,
|
||||||
|
|
||||||
|
/// Number of workers
|
||||||
|
#[arg(long, default_value_t = 4)]
|
||||||
|
workers: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Format {
|
||||||
|
/// Human-readable text
|
||||||
|
Text,
|
||||||
|
/// JSON output
|
||||||
|
Json,
|
||||||
|
/// YAML output
|
||||||
|
Yaml,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Template {
|
||||||
|
/// Basic template
|
||||||
|
Basic,
|
||||||
|
/// Full-featured template
|
||||||
|
Full,
|
||||||
|
/// Minimal template
|
||||||
|
Minimal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum BuildMode {
|
||||||
|
/// Debug build with symbols
|
||||||
|
Debug,
|
||||||
|
/// Release build with optimizations
|
||||||
|
Release,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Environment {
|
||||||
|
/// Development environment
|
||||||
|
Dev,
|
||||||
|
/// Staging environment
|
||||||
|
Staging,
|
||||||
|
/// Production environment
|
||||||
|
Prod,
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
const PORT_RANGE: RangeInclusive<usize> = 1..=65535;
|
||||||
|
|
||||||
|
fn port_in_range(s: &str) -> Result<u16, String> {
|
||||||
|
let port: usize = s
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("`{}` isn't a valid port number", s))?;
|
||||||
|
|
||||||
|
if PORT_RANGE.contains(&port) {
|
||||||
|
Ok(port as u16)
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"port not in range {}-{}",
|
||||||
|
PORT_RANGE.start(),
|
||||||
|
PORT_RANGE.end()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
if cli.verbose {
|
||||||
|
println!("Verbose mode enabled");
|
||||||
|
if let Some(config) = &cli.config {
|
||||||
|
println!("Using config: {}", config.display());
|
||||||
|
}
|
||||||
|
println!("Output format: {:?}", cli.format);
|
||||||
|
}
|
||||||
|
|
||||||
|
match &cli.command {
|
||||||
|
Commands::Init { path, template, yes } => {
|
||||||
|
println!("Initializing project at {}", path.display());
|
||||||
|
println!("Template: {:?}", template);
|
||||||
|
if *yes {
|
||||||
|
println!("Skipping prompts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Build {
|
||||||
|
mode,
|
||||||
|
jobs,
|
||||||
|
target_dir,
|
||||||
|
clean,
|
||||||
|
} => {
|
||||||
|
if *clean {
|
||||||
|
println!("Cleaning target directory");
|
||||||
|
}
|
||||||
|
println!("Building in {:?} mode", mode);
|
||||||
|
println!("Using {} parallel jobs", jobs);
|
||||||
|
println!("Target directory: {}", target_dir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Test {
|
||||||
|
pattern,
|
||||||
|
ignored,
|
||||||
|
test_threads,
|
||||||
|
nocapture,
|
||||||
|
} => {
|
||||||
|
println!("Running tests");
|
||||||
|
if let Some(pat) = pattern {
|
||||||
|
println!("Pattern: {}", pat);
|
||||||
|
}
|
||||||
|
if *ignored {
|
||||||
|
println!("Including ignored tests");
|
||||||
|
}
|
||||||
|
if let Some(threads) = test_threads {
|
||||||
|
println!("Test threads: {}", threads);
|
||||||
|
}
|
||||||
|
if *nocapture {
|
||||||
|
println!("Showing test output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Deploy {
|
||||||
|
environment,
|
||||||
|
skip_checks,
|
||||||
|
tag,
|
||||||
|
config,
|
||||||
|
} => {
|
||||||
|
println!("Deploying to {:?}", environment);
|
||||||
|
if *skip_checks {
|
||||||
|
println!("⚠️ Skipping pre-deployment checks");
|
||||||
|
}
|
||||||
|
if let Some(version) = tag {
|
||||||
|
println!("Version: {}", version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(deploy_config) = config {
|
||||||
|
match deploy_config {
|
||||||
|
DeployConfig::Database { url, migrate } => {
|
||||||
|
println!("Database URL: {}", url);
|
||||||
|
if *migrate {
|
||||||
|
println!("Running migrations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeployConfig::Server { host, port, workers } => {
|
||||||
|
println!("Server: {}:{}", host, port);
|
||||||
|
println!("Workers: {}", workers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// myapp init --template full
|
||||||
|
// myapp build --mode release --jobs 8 --clean
|
||||||
|
// myapp test integration --test-threads 4
|
||||||
|
// myapp deploy prod --tag v1.0.0 server --host 0.0.0.0 --port 443 --workers 16
|
||||||
139
skills/clap-patterns/templates/subcommands.rs
Normal file
139
skills/clap-patterns/templates/subcommands.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/// Subcommand Template with Clap
|
||||||
|
///
|
||||||
|
/// This template demonstrates:
|
||||||
|
/// - Subcommand derive macro
|
||||||
|
/// - Nested command structure
|
||||||
|
/// - Per-subcommand arguments
|
||||||
|
/// - Enum-based command routing
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "git-like")]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
struct Cli {
|
||||||
|
/// Enable verbose output
|
||||||
|
#[arg(global = true, short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Initialize a new repository
|
||||||
|
Init {
|
||||||
|
/// Directory to initialize
|
||||||
|
#[arg(value_name = "DIR", default_value = ".")]
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Create a bare repository
|
||||||
|
#[arg(long)]
|
||||||
|
bare: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Add files to staging area
|
||||||
|
Add {
|
||||||
|
/// Files to add
|
||||||
|
#[arg(value_name = "FILE", required = true)]
|
||||||
|
files: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// Add all files
|
||||||
|
#[arg(short = 'A', long)]
|
||||||
|
all: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Commit staged changes
|
||||||
|
Commit {
|
||||||
|
/// Commit message
|
||||||
|
#[arg(short, long)]
|
||||||
|
message: String,
|
||||||
|
|
||||||
|
/// Amend previous commit
|
||||||
|
#[arg(long)]
|
||||||
|
amend: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Remote repository operations
|
||||||
|
Remote {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: RemoteCommands,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum RemoteCommands {
|
||||||
|
/// Add a new remote
|
||||||
|
Add {
|
||||||
|
/// Remote name
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// Remote URL
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Remove a remote
|
||||||
|
Remove {
|
||||||
|
/// Remote name
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// List all remotes
|
||||||
|
List {
|
||||||
|
/// Show URLs
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match &cli.command {
|
||||||
|
Commands::Init { path, bare } => {
|
||||||
|
if cli.verbose {
|
||||||
|
println!("Initializing repository at {:?}", path);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"Initialized {} repository in {}",
|
||||||
|
if *bare { "bare" } else { "normal" },
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Add { files, all } => {
|
||||||
|
if *all {
|
||||||
|
println!("Adding all files");
|
||||||
|
} else {
|
||||||
|
println!("Adding {} file(s)", files.len());
|
||||||
|
if cli.verbose {
|
||||||
|
for file in files {
|
||||||
|
println!(" - {}", file.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Commit { message, amend } => {
|
||||||
|
if *amend {
|
||||||
|
println!("Amending previous commit");
|
||||||
|
}
|
||||||
|
println!("Committing with message: {}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Remote { command } => match command {
|
||||||
|
RemoteCommands::Add { name, url } => {
|
||||||
|
println!("Adding remote '{}' -> {}", name, url);
|
||||||
|
}
|
||||||
|
RemoteCommands::Remove { name } => {
|
||||||
|
println!("Removing remote '{}'", name);
|
||||||
|
}
|
||||||
|
RemoteCommands::List { verbose } => {
|
||||||
|
println!("Listing remotes{}", if *verbose { " (verbose)" } else { "" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
143
skills/clap-patterns/templates/value-enum.rs
Normal file
143
skills/clap-patterns/templates/value-enum.rs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/// ValueEnum Template for Type-Safe Options
|
||||||
|
///
|
||||||
|
/// This template demonstrates:
|
||||||
|
/// - ValueEnum trait for constrained choices
|
||||||
|
/// - Type-safe option selection
|
||||||
|
/// - Automatic validation and help text
|
||||||
|
/// - Pattern matching on enums
|
||||||
|
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
|
/// Output format options
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum Format {
|
||||||
|
/// JavaScript Object Notation
|
||||||
|
Json,
|
||||||
|
/// YAML Ain't Markup Language
|
||||||
|
Yaml,
|
||||||
|
/// Tom's Obvious, Minimal Language
|
||||||
|
Toml,
|
||||||
|
/// Comma-Separated Values
|
||||||
|
Csv,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log level options
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum LogLevel {
|
||||||
|
/// Detailed debug information
|
||||||
|
Debug,
|
||||||
|
/// General information
|
||||||
|
Info,
|
||||||
|
/// Warning messages
|
||||||
|
Warn,
|
||||||
|
/// Error messages only
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color output mode
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
enum ColorMode {
|
||||||
|
/// Always use colors
|
||||||
|
Always,
|
||||||
|
/// Never use colors
|
||||||
|
Never,
|
||||||
|
/// Automatically detect (default)
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "converter")]
|
||||||
|
#[command(about = "Convert data between formats with type-safe options")]
|
||||||
|
struct Cli {
|
||||||
|
/// Input file
|
||||||
|
input: std::path::PathBuf,
|
||||||
|
|
||||||
|
/// Output format
|
||||||
|
#[arg(short, long, value_enum, default_value_t = Format::Json)]
|
||||||
|
format: Format,
|
||||||
|
|
||||||
|
/// Log level
|
||||||
|
#[arg(short, long, value_enum, default_value_t = LogLevel::Info)]
|
||||||
|
log_level: LogLevel,
|
||||||
|
|
||||||
|
/// Color mode for output
|
||||||
|
#[arg(long, value_enum, default_value_t = ColorMode::Auto)]
|
||||||
|
color: ColorMode,
|
||||||
|
|
||||||
|
/// Pretty print output (for supported formats)
|
||||||
|
#[arg(short, long)]
|
||||||
|
pretty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
// Configure logging based on log level
|
||||||
|
match cli.log_level {
|
||||||
|
LogLevel::Debug => println!("🔍 Debug logging enabled"),
|
||||||
|
LogLevel::Info => println!("ℹ️ Info logging enabled"),
|
||||||
|
LogLevel::Warn => println!("⚠️ Warning logging enabled"),
|
||||||
|
LogLevel::Error => println!("❌ Error logging only"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check color mode
|
||||||
|
let use_colors = match cli.color {
|
||||||
|
ColorMode::Always => true,
|
||||||
|
ColorMode::Never => false,
|
||||||
|
ColorMode::Auto => atty::is(atty::Stream::Stdout),
|
||||||
|
};
|
||||||
|
|
||||||
|
if use_colors {
|
||||||
|
println!("🎨 Color output enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process based on format
|
||||||
|
println!("Converting {} to {:?}", cli.input.display(), cli.format);
|
||||||
|
|
||||||
|
match cli.format {
|
||||||
|
Format::Json => {
|
||||||
|
println!("Converting to JSON{}", if cli.pretty { " (pretty)" } else { "" });
|
||||||
|
// JSON conversion logic here
|
||||||
|
}
|
||||||
|
Format::Yaml => {
|
||||||
|
println!("Converting to YAML");
|
||||||
|
// YAML conversion logic here
|
||||||
|
}
|
||||||
|
Format::Toml => {
|
||||||
|
println!("Converting to TOML");
|
||||||
|
// TOML conversion logic here
|
||||||
|
}
|
||||||
|
Format::Csv => {
|
||||||
|
println!("Converting to CSV");
|
||||||
|
// CSV conversion logic here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✓ Conversion complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if stdout is a terminal (for color auto-detection)
|
||||||
|
mod atty {
|
||||||
|
pub enum Stream {
|
||||||
|
Stdout,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is(_stream: Stream) -> bool {
|
||||||
|
// Simple implementation - checks if stdout is a TTY
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// cargo run -- input.txt --format json --log-level debug
|
||||||
|
// cargo run -- data.yml --format toml --color always --pretty
|
||||||
|
// cargo run -- config.json --format yaml --log-level warn
|
||||||
109
skills/clap-patterns/templates/value-parser.rs
Normal file
109
skills/clap-patterns/templates/value-parser.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/// Value Parser Template with Custom Validation
|
||||||
|
///
|
||||||
|
/// This template demonstrates:
|
||||||
|
/// - Custom value parsers
|
||||||
|
/// - Range validation
|
||||||
|
/// - Format validation (regex)
|
||||||
|
/// - Error handling with helpful messages
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
const PORT_RANGE: RangeInclusive<usize> = 1..=65535;
|
||||||
|
|
||||||
|
/// Parse and validate port number
|
||||||
|
fn port_in_range(s: &str) -> Result<u16, String> {
|
||||||
|
let port: usize = s
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("`{}` isn't a valid port number", s))?;
|
||||||
|
|
||||||
|
if PORT_RANGE.contains(&port) {
|
||||||
|
Ok(port as u16)
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"port not in range {}-{}",
|
||||||
|
PORT_RANGE.start(),
|
||||||
|
PORT_RANGE.end()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate email format (basic validation)
|
||||||
|
fn validate_email(s: &str) -> Result<String, String> {
|
||||||
|
if s.contains('@') && s.contains('.') && s.len() > 5 {
|
||||||
|
Ok(s.to_string())
|
||||||
|
} else {
|
||||||
|
Err(format!("`{}` is not a valid email address", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse percentage (0-100)
|
||||||
|
fn parse_percentage(s: &str) -> Result<u8, String> {
|
||||||
|
let value: u8 = s
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("`{}` isn't a valid number", s))?;
|
||||||
|
|
||||||
|
if value <= 100 {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err("percentage must be between 0 and 100".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate directory exists
|
||||||
|
fn validate_directory(s: &str) -> Result<std::path::PathBuf, String> {
|
||||||
|
let path = std::path::PathBuf::from(s);
|
||||||
|
|
||||||
|
if path.exists() && path.is_dir() {
|
||||||
|
Ok(path)
|
||||||
|
} else {
|
||||||
|
Err(format!("directory does not exist: {}", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "validator")]
|
||||||
|
#[command(about = "CLI with custom value parsers and validation")]
|
||||||
|
struct Cli {
|
||||||
|
/// Port number (1-65535)
|
||||||
|
#[arg(short, long, value_parser = port_in_range)]
|
||||||
|
port: u16,
|
||||||
|
|
||||||
|
/// Email address
|
||||||
|
#[arg(short, long, value_parser = validate_email)]
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
/// Success threshold percentage (0-100)
|
||||||
|
#[arg(short, long, value_parser = parse_percentage, default_value = "80")]
|
||||||
|
threshold: u8,
|
||||||
|
|
||||||
|
/// Working directory (must exist)
|
||||||
|
#[arg(short, long, value_parser = validate_directory)]
|
||||||
|
workdir: Option<std::path::PathBuf>,
|
||||||
|
|
||||||
|
/// Number of retries (1-10)
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value = "3",
|
||||||
|
value_parser = clap::value_parser!(u8).range(1..=10)
|
||||||
|
)]
|
||||||
|
retries: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
println!("Configuration:");
|
||||||
|
println!(" Port: {}", cli.port);
|
||||||
|
println!(" Email: {}", cli.email);
|
||||||
|
println!(" Threshold: {}%", cli.threshold);
|
||||||
|
println!(" Retries: {}", cli.retries);
|
||||||
|
|
||||||
|
if let Some(workdir) = cli.workdir {
|
||||||
|
println!(" Working directory: {}", workdir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your application logic here
|
||||||
|
println!("\nValidation passed! All inputs are valid.");
|
||||||
|
}
|
||||||
334
skills/cli-patterns/SKILL.md
Normal file
334
skills/cli-patterns/SKILL.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
---
|
||||||
|
name: cli-patterns
|
||||||
|
description: Lightweight Go CLI patterns using urfave/cli. Use when building CLI tools, creating commands with flags, implementing subcommands, adding before/after hooks, organizing command categories, or when user mentions Go CLI, urfave/cli, cobra alternatives, CLI flags, CLI categories.
|
||||||
|
allowed-tools: Bash, Read, Write, Edit
|
||||||
|
---
|
||||||
|
|
||||||
|
# CLI Patterns Skill
|
||||||
|
|
||||||
|
Lightweight Go CLI patterns using urfave/cli for fast, simple command-line applications.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Provides battle-tested patterns for building production-ready CLI tools in Go using urfave/cli v2. Focus on simplicity, speed, and maintainability over complex frameworks like Cobra.
|
||||||
|
|
||||||
|
## Why urfave/cli?
|
||||||
|
|
||||||
|
- **Lightweight**: Minimal dependencies, small binary size
|
||||||
|
- **Fast**: Quick compilation, fast execution
|
||||||
|
- **Simple API**: Easy to learn, less boilerplate than Cobra
|
||||||
|
- **Production-ready**: Used by Docker, Nomad, and many other tools
|
||||||
|
- **Native Go**: Feels like standard library code
|
||||||
|
|
||||||
|
## Core Patterns
|
||||||
|
|
||||||
|
### 1. Basic CLI Structure
|
||||||
|
|
||||||
|
Use `templates/basic-cli.go` for simple single-command CLIs:
|
||||||
|
- Main command with flags
|
||||||
|
- Help text generation
|
||||||
|
- Error handling
|
||||||
|
- Exit codes
|
||||||
|
|
||||||
|
### 2. Subcommands
|
||||||
|
|
||||||
|
Use `templates/subcommands-cli.go` for multi-command CLIs:
|
||||||
|
- Command hierarchy (app → command → subcommand)
|
||||||
|
- Shared flags across commands
|
||||||
|
- Command aliases
|
||||||
|
- Command categories
|
||||||
|
|
||||||
|
### 3. Flags and Options
|
||||||
|
|
||||||
|
Use `templates/flags-demo.go` for comprehensive flag examples:
|
||||||
|
- String, int, bool, duration flags
|
||||||
|
- Required vs optional flags
|
||||||
|
- Default values
|
||||||
|
- Environment variable fallbacks
|
||||||
|
- Flag aliases (short and long forms)
|
||||||
|
- Custom flag types
|
||||||
|
|
||||||
|
### 4. Command Categories
|
||||||
|
|
||||||
|
Use `templates/categories-cli.go` for organized command groups:
|
||||||
|
- Group related commands
|
||||||
|
- Better help text organization
|
||||||
|
- Professional CLI UX
|
||||||
|
- Examples: database commands, deploy commands, etc.
|
||||||
|
|
||||||
|
### 5. Before/After Hooks
|
||||||
|
|
||||||
|
Use `templates/hooks-cli.go` for lifecycle management:
|
||||||
|
- Global setup (before all commands)
|
||||||
|
- Global cleanup (after all commands)
|
||||||
|
- Per-command setup/teardown
|
||||||
|
- Initialization and validation
|
||||||
|
- Resource management
|
||||||
|
|
||||||
|
### 6. Context and State
|
||||||
|
|
||||||
|
Use `templates/context-cli.go` for shared state:
|
||||||
|
- Pass configuration between commands
|
||||||
|
- Share database connections
|
||||||
|
- Manage API clients
|
||||||
|
- Context values
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
### Generation Scripts
|
||||||
|
|
||||||
|
**`scripts/generate-basic.sh <app-name>`**
|
||||||
|
- Generates basic CLI structure
|
||||||
|
- Creates main.go with single command
|
||||||
|
- Adds common flags (verbose, config)
|
||||||
|
- Includes help text template
|
||||||
|
|
||||||
|
**`scripts/generate-subcommands.sh <app-name>`**
|
||||||
|
- Generates multi-command CLI
|
||||||
|
- Creates command structure
|
||||||
|
- Adds subcommand examples
|
||||||
|
- Includes command categories
|
||||||
|
|
||||||
|
**`scripts/generate-full.sh <app-name>`**
|
||||||
|
- Generates complete CLI with all patterns
|
||||||
|
- Includes before/after hooks
|
||||||
|
- Adds comprehensive flag examples
|
||||||
|
- Sets up command categories
|
||||||
|
- Includes context management
|
||||||
|
|
||||||
|
### Utility Scripts
|
||||||
|
|
||||||
|
**`scripts/add-command.sh <app-name> <command-name>`**
|
||||||
|
- Adds new command to existing CLI
|
||||||
|
- Updates command registration
|
||||||
|
- Creates command file
|
||||||
|
- Adds to appropriate category
|
||||||
|
|
||||||
|
**`scripts/add-flag.sh <file> <flag-name> <flag-type>`**
|
||||||
|
- Adds flag to command
|
||||||
|
- Supports all flag types
|
||||||
|
- Includes environment variable fallback
|
||||||
|
- Adds help text
|
||||||
|
|
||||||
|
**`scripts/validate-cli.sh <project-path>`**
|
||||||
|
- Validates CLI structure
|
||||||
|
- Checks for common mistakes
|
||||||
|
- Verifies flag definitions
|
||||||
|
- Ensures help text exists
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
### Core Templates
|
||||||
|
|
||||||
|
**`templates/basic-cli.go`**
|
||||||
|
- Single-command CLI
|
||||||
|
- Standard flags (verbose, version)
|
||||||
|
- Error handling patterns
|
||||||
|
- Exit code management
|
||||||
|
|
||||||
|
**`templates/subcommands-cli.go`**
|
||||||
|
- Multi-command structure
|
||||||
|
- Command registration
|
||||||
|
- Shared flags
|
||||||
|
- Help text organization
|
||||||
|
|
||||||
|
**`templates/flags-demo.go`**
|
||||||
|
- All flag types demonstrated
|
||||||
|
- Environment variable fallbacks
|
||||||
|
- Required flag validation
|
||||||
|
- Custom flag types
|
||||||
|
|
||||||
|
**`templates/categories-cli.go`**
|
||||||
|
- Command categorization
|
||||||
|
- Professional help output
|
||||||
|
- Grouped commands
|
||||||
|
- Category-based organization
|
||||||
|
|
||||||
|
**`templates/hooks-cli.go`**
|
||||||
|
- Before/After hooks
|
||||||
|
- Global setup/teardown
|
||||||
|
- Per-command hooks
|
||||||
|
- Resource initialization
|
||||||
|
|
||||||
|
**`templates/context-cli.go`**
|
||||||
|
- Context management
|
||||||
|
- Shared state
|
||||||
|
- Configuration passing
|
||||||
|
- API client sharing
|
||||||
|
|
||||||
|
### TypeScript Equivalent (Node.js)
|
||||||
|
|
||||||
|
**`templates/commander-basic.ts`**
|
||||||
|
- commander.js equivalent patterns
|
||||||
|
- TypeScript type safety
|
||||||
|
- Similar API to urfave/cli
|
||||||
|
|
||||||
|
**`templates/oclif-basic.ts`**
|
||||||
|
- oclif framework patterns (Heroku/Salesforce style)
|
||||||
|
- Class-based commands
|
||||||
|
- Plugin system
|
||||||
|
|
||||||
|
### Python Equivalent
|
||||||
|
|
||||||
|
**`templates/click-basic.py`**
|
||||||
|
- click framework patterns
|
||||||
|
- Decorator-based commands
|
||||||
|
- Python CLI best practices
|
||||||
|
|
||||||
|
**`templates/typer-basic.py`**
|
||||||
|
- typer framework (FastAPI CLI)
|
||||||
|
- Type hints for validation
|
||||||
|
- Modern Python patterns
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Database CLI Tool
|
||||||
|
|
||||||
|
**`examples/db-cli/`**
|
||||||
|
- Complete database management CLI
|
||||||
|
- Commands: connect, migrate, seed, backup
|
||||||
|
- Categories: schema, data, admin
|
||||||
|
- Before hook: validate connection
|
||||||
|
- After hook: close connections
|
||||||
|
|
||||||
|
### Example 2: Deployment Tool
|
||||||
|
|
||||||
|
**`examples/deploy-cli/`**
|
||||||
|
- Deployment automation CLI
|
||||||
|
- Commands: build, test, deploy, rollback
|
||||||
|
- Categories: build, deploy, monitor
|
||||||
|
- Context: share deployment config
|
||||||
|
- Hooks: setup AWS credentials
|
||||||
|
|
||||||
|
### Example 3: API Client
|
||||||
|
|
||||||
|
**`examples/api-cli/`**
|
||||||
|
- REST API client CLI
|
||||||
|
- Commands: get, post, put, delete
|
||||||
|
- Global flags: auth token, base URL
|
||||||
|
- Before hook: authenticate
|
||||||
|
- Context: share HTTP client
|
||||||
|
|
||||||
|
### Example 4: File Processor
|
||||||
|
|
||||||
|
**`examples/file-cli/`**
|
||||||
|
- File processing tool
|
||||||
|
- Commands: convert, validate, optimize
|
||||||
|
- Categories: input, output, processing
|
||||||
|
- Flags: input format, output format
|
||||||
|
- Progress indicators
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### CLI Design
|
||||||
|
|
||||||
|
1. **Keep it simple**: Start with basic structure, add complexity as needed
|
||||||
|
2. **Consistent naming**: Use kebab-case for commands (deploy-app, not deployApp)
|
||||||
|
3. **Clear help text**: Every command and flag needs description
|
||||||
|
4. **Exit codes**: Use standard codes (0=success, 1=error, 2=usage error)
|
||||||
|
|
||||||
|
### Flag Patterns
|
||||||
|
|
||||||
|
1. **Environment variables**: Always provide env var fallback for important flags
|
||||||
|
2. **Sensible defaults**: Required flags should be rare
|
||||||
|
3. **Short and long forms**: -v/--verbose, -c/--config
|
||||||
|
4. **Validation**: Validate flags in Before hook, not in action
|
||||||
|
|
||||||
|
### Command Organization
|
||||||
|
|
||||||
|
1. **Categories**: Group related commands (>5 commands = use categories)
|
||||||
|
2. **Aliases**: Provide shortcuts for common commands
|
||||||
|
3. **Subcommands**: Use for hierarchical operations (db migrate up/down)
|
||||||
|
4. **Help text**: Keep concise, provide examples
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
1. **Fast compilation**: urfave/cli compiles faster than Cobra
|
||||||
|
2. **Small binaries**: Minimal dependencies = smaller output
|
||||||
|
3. **Startup time**: Use Before hooks for expensive initialization
|
||||||
|
4. **Lazy loading**: Don't initialize resources unless command needs them
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Configuration File Loading
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Before = func(c *cli.Context) error {
|
||||||
|
configPath := c.String("config")
|
||||||
|
if configPath != "" {
|
||||||
|
return loadConfig(configPath)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variable Fallbacks
|
||||||
|
|
||||||
|
```go
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "token",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Usage: "API token",
|
||||||
|
EnvVars: []string{"API_TOKEN"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Flags
|
||||||
|
|
||||||
|
```go
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "host",
|
||||||
|
Required: true,
|
||||||
|
Usage: "Database host",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global State Management
|
||||||
|
|
||||||
|
```go
|
||||||
|
type AppContext struct {
|
||||||
|
Config *Config
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Before = func(c *cli.Context) error {
|
||||||
|
ctx := &AppContext{
|
||||||
|
Config: loadConfig(),
|
||||||
|
}
|
||||||
|
c.App.Metadata["ctx"] = ctx
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run `scripts/validate-cli.sh` to check:
|
||||||
|
- All commands have descriptions
|
||||||
|
- All flags have usage text
|
||||||
|
- Before/After hooks are properly defined
|
||||||
|
- Help text is clear and concise
|
||||||
|
- No unused imports
|
||||||
|
- Proper error handling
|
||||||
|
|
||||||
|
## Migration Guides
|
||||||
|
|
||||||
|
### From Cobra to urfave/cli
|
||||||
|
|
||||||
|
See `examples/cobra-migration/` for:
|
||||||
|
- Command mapping (cobra.Command → cli.Command)
|
||||||
|
- Flag conversion (cobra flags → cli flags)
|
||||||
|
- Hook equivalents (PreRun → Before)
|
||||||
|
- Context differences
|
||||||
|
|
||||||
|
### From Click (Python) to urfave/cli
|
||||||
|
|
||||||
|
See `examples/click-migration/` for:
|
||||||
|
- Decorator to struct conversion
|
||||||
|
- Option to flag mapping
|
||||||
|
- Context passing patterns
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [urfave/cli v2 Documentation](https://cli.urfave.org/v2/)
|
||||||
|
- [Docker CLI Source](https://github.com/docker/cli) - Real-world example
|
||||||
|
- [Go CLI Best Practices](https://github.com/cli-dev/guide)
|
||||||
212
skills/cli-patterns/examples/EXAMPLES-INDEX.md
Normal file
212
skills/cli-patterns/examples/EXAMPLES-INDEX.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# CLI Patterns Examples Index
|
||||||
|
|
||||||
|
Comprehensive examples demonstrating urfave/cli patterns in production-ready applications.
|
||||||
|
|
||||||
|
## Example Applications
|
||||||
|
|
||||||
|
### 1. Database CLI Tool (`db-cli/`)
|
||||||
|
|
||||||
|
**Purpose**: Complete database management CLI with categories, hooks, and connection handling.
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Command categories (Schema, Data, Admin)
|
||||||
|
- Before hook for connection validation
|
||||||
|
- After hook for cleanup
|
||||||
|
- Required and optional flags
|
||||||
|
- Environment variable fallbacks
|
||||||
|
|
||||||
|
**Commands**:
|
||||||
|
- `migrate` - Run migrations with direction and steps
|
||||||
|
- `rollback` - Rollback last migration
|
||||||
|
- `seed` - Seed database with test data
|
||||||
|
- `backup` - Create database backup
|
||||||
|
- `restore` - Restore from backup
|
||||||
|
- `status` - Check database status
|
||||||
|
- `vacuum` - Optimize database
|
||||||
|
|
||||||
|
**Key Patterns**:
|
||||||
|
```go
|
||||||
|
// Connection validation in Before hook
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
conn := c.String("connection")
|
||||||
|
// Validate connection
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup in After hook
|
||||||
|
After: func(c *cli.Context) error {
|
||||||
|
// Close connections
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Deployment CLI Tool (`deploy-cli/`)
|
||||||
|
|
||||||
|
**Purpose**: Deployment automation with context management and environment validation.
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Context management with shared state
|
||||||
|
- Environment validation
|
||||||
|
- Confirmation prompts for destructive actions
|
||||||
|
- AWS region configuration
|
||||||
|
- Build, deploy, and monitor workflows
|
||||||
|
|
||||||
|
**Commands**:
|
||||||
|
- `build` - Build application with tags
|
||||||
|
- `test` - Run test suite
|
||||||
|
- `deploy` - Deploy to environment (with confirmation)
|
||||||
|
- `rollback` - Rollback to previous version
|
||||||
|
- `logs` - View deployment logs
|
||||||
|
- `status` - Check deployment status
|
||||||
|
|
||||||
|
**Key Patterns**:
|
||||||
|
```go
|
||||||
|
// Shared context across commands
|
||||||
|
type DeployContext struct {
|
||||||
|
Environment string
|
||||||
|
AWSRegion string
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store context in Before hook
|
||||||
|
ctx := &DeployContext{...}
|
||||||
|
c.App.Metadata["ctx"] = ctx
|
||||||
|
|
||||||
|
// Retrieve in command
|
||||||
|
ctx := c.App.Metadata["ctx"].(*DeployContext)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. API Client CLI Tool (`api-cli/`)
|
||||||
|
|
||||||
|
**Purpose**: REST API client with HTTP client sharing and authentication.
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- HTTP client sharing via context
|
||||||
|
- Authentication in Before hook
|
||||||
|
- Multiple HTTP methods (GET, POST, PUT, DELETE)
|
||||||
|
- Request timeout configuration
|
||||||
|
- Token masking for security
|
||||||
|
|
||||||
|
**Commands**:
|
||||||
|
- `get` - GET request with headers
|
||||||
|
- `post` - POST request with data
|
||||||
|
- `put` - PUT request with data
|
||||||
|
- `delete` - DELETE request
|
||||||
|
- `auth-test` - Test authentication
|
||||||
|
|
||||||
|
**Key Patterns**:
|
||||||
|
```go
|
||||||
|
// HTTP client in context
|
||||||
|
type APIContext struct {
|
||||||
|
BaseURL string
|
||||||
|
Token string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize in Before hook
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
ctx := &APIContext{
|
||||||
|
HTTPClient: client,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use in commands
|
||||||
|
ctx := c.App.Metadata["ctx"].(*APIContext)
|
||||||
|
resp, err := ctx.HTTPClient.Get(url)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pattern Summary
|
||||||
|
|
||||||
|
### Context Management
|
||||||
|
All three examples demonstrate different context patterns:
|
||||||
|
- **db-cli**: Connection validation and cleanup
|
||||||
|
- **deploy-cli**: Shared deployment configuration
|
||||||
|
- **api-cli**: HTTP client and authentication sharing
|
||||||
|
|
||||||
|
### Before/After Hooks
|
||||||
|
- **Before**: Setup, validation, authentication, connection establishment
|
||||||
|
- **After**: Cleanup, resource release, connection closing
|
||||||
|
|
||||||
|
### Command Categories
|
||||||
|
Organized command groups for better UX:
|
||||||
|
- **db-cli**: Schema, Data, Admin
|
||||||
|
- **deploy-cli**: Build, Deploy, Monitor
|
||||||
|
- **api-cli**: No categories (simple HTTP verbs)
|
||||||
|
|
||||||
|
### Flag Patterns
|
||||||
|
- Required flags: `--connection`, `--env`, `--token`
|
||||||
|
- Environment variables: All support env var fallbacks
|
||||||
|
- Aliases: Short forms (-v, -e, -t)
|
||||||
|
- Multiple values: StringSlice for headers
|
||||||
|
- Custom types: Duration for timeouts
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
All examples demonstrate:
|
||||||
|
- Validation in Before hooks
|
||||||
|
- Proper error returns
|
||||||
|
- User-friendly error messages
|
||||||
|
- Exit code handling
|
||||||
|
|
||||||
|
## Running the Examples
|
||||||
|
|
||||||
|
### Database CLI
|
||||||
|
```bash
|
||||||
|
export DATABASE_URL="postgres://user:pass@localhost/mydb"
|
||||||
|
cd examples/db-cli
|
||||||
|
go build -o dbctl .
|
||||||
|
./dbctl migrate
|
||||||
|
./dbctl backup --output backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment CLI
|
||||||
|
```bash
|
||||||
|
export DEPLOY_ENV=staging
|
||||||
|
export AWS_REGION=us-east-1
|
||||||
|
cd examples/deploy-cli
|
||||||
|
go build -o deploy .
|
||||||
|
./deploy build --tag v1.0.0
|
||||||
|
./deploy deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Client CLI
|
||||||
|
```bash
|
||||||
|
export API_URL=https://api.example.com
|
||||||
|
export API_TOKEN=your_token_here
|
||||||
|
cd examples/api-cli
|
||||||
|
go build -o api .
|
||||||
|
./api get /users
|
||||||
|
./api post /users '{"name":"John"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Learning Path
|
||||||
|
|
||||||
|
**Beginner**:
|
||||||
|
1. Start with `db-cli` - demonstrates basic categories and hooks
|
||||||
|
2. Study Before/After hook patterns
|
||||||
|
3. Learn flag types and validation
|
||||||
|
|
||||||
|
**Intermediate**:
|
||||||
|
4. Study `deploy-cli` - context management and shared state
|
||||||
|
5. Learn environment validation
|
||||||
|
6. Understand confirmation prompts
|
||||||
|
|
||||||
|
**Advanced**:
|
||||||
|
7. Study `api-cli` - HTTP client sharing and authentication
|
||||||
|
8. Learn complex context patterns
|
||||||
|
9. Understand resource lifecycle management
|
||||||
|
|
||||||
|
## Cross-Language Comparison
|
||||||
|
|
||||||
|
Each example can be implemented in other languages:
|
||||||
|
- **TypeScript**: Use commander.js (see templates/)
|
||||||
|
- **Python**: Use click or typer (see templates/)
|
||||||
|
- **Ruby**: Use thor
|
||||||
|
- **Rust**: Use clap
|
||||||
|
|
||||||
|
The patterns translate directly across languages with similar CLI frameworks.
|
||||||
69
skills/cli-patterns/examples/api-cli/README.md
Normal file
69
skills/cli-patterns/examples/api-cli/README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# API Client CLI Tool Example
|
||||||
|
|
||||||
|
Complete REST API client CLI demonstrating:
|
||||||
|
- HTTP client sharing via context
|
||||||
|
- Authentication in Before hook
|
||||||
|
- Multiple HTTP methods (GET, POST, PUT, DELETE)
|
||||||
|
- Headers and request configuration
|
||||||
|
- Arguments handling
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set environment variables
|
||||||
|
export API_URL=https://api.example.com
|
||||||
|
export API_TOKEN=your_token_here
|
||||||
|
|
||||||
|
# GET request
|
||||||
|
api get /users
|
||||||
|
api get /users/123
|
||||||
|
|
||||||
|
# POST request
|
||||||
|
api post /users '{"name": "John", "email": "john@example.com"}'
|
||||||
|
api post /posts '{"title": "Hello", "body": "World"}' --content-type application/json
|
||||||
|
|
||||||
|
# PUT request
|
||||||
|
api put /users/123 '{"name": "Jane"}'
|
||||||
|
|
||||||
|
# DELETE request
|
||||||
|
api delete /users/123
|
||||||
|
|
||||||
|
# Test authentication
|
||||||
|
api auth-test
|
||||||
|
|
||||||
|
# Custom timeout
|
||||||
|
api --timeout 60s get /slow-endpoint
|
||||||
|
|
||||||
|
# Additional headers
|
||||||
|
api get /users -H "Accept:application/json" -H "X-Custom:value"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features Demonstrated
|
||||||
|
|
||||||
|
1. **Context Management**: Shared HTTPClient and auth across requests
|
||||||
|
2. **Before Hook**: Authenticates and sets up HTTP client
|
||||||
|
3. **Arguments**: Commands accept endpoint and data as arguments
|
||||||
|
4. **Required Flags**: --url and --token are required
|
||||||
|
5. **Environment Variables**: API_URL, API_TOKEN, API_TIMEOUT fallbacks
|
||||||
|
6. **Duration Flags**: --timeout uses time.Duration type
|
||||||
|
7. **Multiple Values**: --header can be specified multiple times
|
||||||
|
8. **Helper Functions**: maskToken() for secure token display
|
||||||
|
|
||||||
|
## HTTP Client Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
type APIContext struct {
|
||||||
|
BaseURL string
|
||||||
|
Token string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize in Before hook
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
ctx := &APIContext{...}
|
||||||
|
c.App.Metadata["ctx"] = ctx
|
||||||
|
|
||||||
|
// Use in commands
|
||||||
|
ctx := c.App.Metadata["ctx"].(*APIContext)
|
||||||
|
resp, err := ctx.HTTPClient.Get(url)
|
||||||
|
```
|
||||||
205
skills/cli-patterns/examples/api-cli/main.go
Normal file
205
skills/cli-patterns/examples/api-cli/main.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIContext struct {
|
||||||
|
BaseURL string
|
||||||
|
Token string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "api",
|
||||||
|
Usage: "REST API client CLI",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "url",
|
||||||
|
Usage: "API base URL",
|
||||||
|
EnvVars: []string{"API_URL"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "token",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Usage: "Authentication token",
|
||||||
|
EnvVars: []string{"API_TOKEN"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "timeout",
|
||||||
|
Usage: "Request timeout",
|
||||||
|
Value: 30 * time.Second,
|
||||||
|
EnvVars: []string{"API_TIMEOUT"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
baseURL := c.String("url")
|
||||||
|
token := c.String("token")
|
||||||
|
timeout := c.Duration("timeout")
|
||||||
|
|
||||||
|
fmt.Println("🔐 Authenticating with API...")
|
||||||
|
|
||||||
|
// Create HTTP client
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store context
|
||||||
|
ctx := &APIContext{
|
||||||
|
BaseURL: baseURL,
|
||||||
|
Token: token,
|
||||||
|
HTTPClient: client,
|
||||||
|
}
|
||||||
|
c.App.Metadata["ctx"] = ctx
|
||||||
|
|
||||||
|
fmt.Println("✅ Authentication successful")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "get",
|
||||||
|
Usage: "GET request",
|
||||||
|
ArgsUsage: "<endpoint>",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "header",
|
||||||
|
Aliases: []string{"H"},
|
||||||
|
Usage: "Additional headers (key:value)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*APIContext)
|
||||||
|
|
||||||
|
if c.NArg() < 1 {
|
||||||
|
return fmt.Errorf("endpoint required")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := c.Args().Get(0)
|
||||||
|
url := fmt.Sprintf("%s%s", ctx.BaseURL, endpoint)
|
||||||
|
|
||||||
|
fmt.Printf("GET %s\n", url)
|
||||||
|
fmt.Printf("Authorization: Bearer %s\n", maskToken(ctx.Token))
|
||||||
|
|
||||||
|
// In real app: make HTTP request
|
||||||
|
fmt.Println("Response: 200 OK")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "post",
|
||||||
|
Usage: "POST request",
|
||||||
|
ArgsUsage: "<endpoint> <data>",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "content-type",
|
||||||
|
Aliases: []string{"ct"},
|
||||||
|
Usage: "Content-Type header",
|
||||||
|
Value: "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*APIContext)
|
||||||
|
|
||||||
|
if c.NArg() < 2 {
|
||||||
|
return fmt.Errorf("usage: post <endpoint> <data>")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := c.Args().Get(0)
|
||||||
|
data := c.Args().Get(1)
|
||||||
|
url := fmt.Sprintf("%s%s", ctx.BaseURL, endpoint)
|
||||||
|
contentType := c.String("content-type")
|
||||||
|
|
||||||
|
fmt.Printf("POST %s\n", url)
|
||||||
|
fmt.Printf("Content-Type: %s\n", contentType)
|
||||||
|
fmt.Printf("Data: %s\n", data)
|
||||||
|
|
||||||
|
// In real app: make HTTP POST request
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "put",
|
||||||
|
Usage: "PUT request",
|
||||||
|
ArgsUsage: "<endpoint> <data>",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*APIContext)
|
||||||
|
|
||||||
|
if c.NArg() < 2 {
|
||||||
|
return fmt.Errorf("usage: put <endpoint> <data>")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := c.Args().Get(0)
|
||||||
|
data := c.Args().Get(1)
|
||||||
|
url := fmt.Sprintf("%s%s", ctx.BaseURL, endpoint)
|
||||||
|
|
||||||
|
fmt.Printf("PUT %s\n", url)
|
||||||
|
fmt.Printf("Data: %s\n", data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "delete",
|
||||||
|
Usage: "DELETE request",
|
||||||
|
ArgsUsage: "<endpoint>",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*APIContext)
|
||||||
|
|
||||||
|
if c.NArg() < 1 {
|
||||||
|
return fmt.Errorf("endpoint required")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := c.Args().Get(0)
|
||||||
|
url := fmt.Sprintf("%s%s", ctx.BaseURL, endpoint)
|
||||||
|
|
||||||
|
fmt.Printf("DELETE %s\n", url)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "auth-test",
|
||||||
|
Usage: "Test authentication",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*APIContext)
|
||||||
|
|
||||||
|
fmt.Println("Testing authentication...")
|
||||||
|
fmt.Printf("API URL: %s\n", ctx.BaseURL)
|
||||||
|
fmt.Printf("Token: %s\n", maskToken(ctx.Token))
|
||||||
|
fmt.Println("Status: Authenticated ✅")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskToken(token string) string {
|
||||||
|
if len(token) < 8 {
|
||||||
|
return "****"
|
||||||
|
}
|
||||||
|
return token[:4] + "****" + token[len(token)-4:]
|
||||||
|
}
|
||||||
46
skills/cli-patterns/examples/db-cli/README.md
Normal file
46
skills/cli-patterns/examples/db-cli/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Database CLI Tool Example
|
||||||
|
|
||||||
|
Complete database management CLI demonstrating:
|
||||||
|
- Command categories (Schema, Data, Admin)
|
||||||
|
- Before hook for connection validation
|
||||||
|
- After hook for cleanup
|
||||||
|
- Required and optional flags
|
||||||
|
- Environment variable fallbacks
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set connection string
|
||||||
|
export DATABASE_URL="postgres://user:pass@localhost/mydb"
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
dbctl migrate
|
||||||
|
dbctl migrate --direction down --steps 2
|
||||||
|
|
||||||
|
# Rollback
|
||||||
|
dbctl rollback
|
||||||
|
|
||||||
|
# Seed database
|
||||||
|
dbctl seed --file seeds/test-data.sql
|
||||||
|
|
||||||
|
# Backup and restore
|
||||||
|
dbctl backup --output backups/db-$(date +%Y%m%d).sql
|
||||||
|
dbctl restore --input backups/db-20240101.sql
|
||||||
|
|
||||||
|
# Admin tasks
|
||||||
|
dbctl status
|
||||||
|
dbctl vacuum
|
||||||
|
|
||||||
|
# Verbose output
|
||||||
|
dbctl -v migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features Demonstrated
|
||||||
|
|
||||||
|
1. **Command Categories**: Schema, Data, Admin
|
||||||
|
2. **Global Flags**: --connection, --verbose
|
||||||
|
3. **Before Hook**: Validates connection before any command
|
||||||
|
4. **After Hook**: Closes connections after command completes
|
||||||
|
5. **Required Flags**: backup/restore require file paths
|
||||||
|
6. **Environment Variables**: DATABASE_URL fallback
|
||||||
|
7. **Flag Aliases**: -v for --verbose, -d for --direction
|
||||||
183
skills/cli-patterns/examples/db-cli/main.go
Normal file
183
skills/cli-patterns/examples/db-cli/main.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "dbctl",
|
||||||
|
Usage: "Database management CLI tool",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "connection",
|
||||||
|
Aliases: []string{"conn"},
|
||||||
|
Usage: "Database connection string",
|
||||||
|
EnvVars: []string{"DATABASE_URL"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "Enable verbose output",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
conn := c.String("connection")
|
||||||
|
verbose := c.Bool("verbose")
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Println("🔗 Validating database connection...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate connection string
|
||||||
|
if conn == "" {
|
||||||
|
return fmt.Errorf("database connection string required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Println("✅ Connection string validated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
After: func(c *cli.Context) error {
|
||||||
|
if c.Bool("verbose") {
|
||||||
|
fmt.Println("🔚 Closing database connections...")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
// Schema category
|
||||||
|
{
|
||||||
|
Name: "migrate",
|
||||||
|
Category: "Schema",
|
||||||
|
Usage: "Run database migrations",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "direction",
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
Usage: "Migration direction (up/down)",
|
||||||
|
Value: "up",
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "steps",
|
||||||
|
Usage: "Number of steps to migrate",
|
||||||
|
Value: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
direction := c.String("direction")
|
||||||
|
steps := c.Int("steps")
|
||||||
|
|
||||||
|
fmt.Printf("Running migrations %s", direction)
|
||||||
|
if steps > 0 {
|
||||||
|
fmt.Printf(" (%d steps)", steps)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "rollback",
|
||||||
|
Category: "Schema",
|
||||||
|
Usage: "Rollback last migration",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Rolling back last migration...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Data category
|
||||||
|
{
|
||||||
|
Name: "seed",
|
||||||
|
Category: "Data",
|
||||||
|
Usage: "Seed database with test data",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "file",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "Seed file path",
|
||||||
|
Value: "seeds/default.sql",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
file := c.String("file")
|
||||||
|
fmt.Printf("Seeding database from: %s\n", file)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "backup",
|
||||||
|
Category: "Data",
|
||||||
|
Usage: "Backup database",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "Backup output path",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
output := c.String("output")
|
||||||
|
fmt.Printf("Backing up database to: %s\n", output)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "restore",
|
||||||
|
Category: "Data",
|
||||||
|
Usage: "Restore database from backup",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "input",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "Backup file path",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
input := c.String("input")
|
||||||
|
fmt.Printf("Restoring database from: %s\n", input)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Admin category
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Category: "Admin",
|
||||||
|
Usage: "Check database status",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Database Status:")
|
||||||
|
fmt.Println(" Connection: Active")
|
||||||
|
fmt.Println(" Tables: 15")
|
||||||
|
fmt.Println(" Size: 245 MB")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "vacuum",
|
||||||
|
Category: "Admin",
|
||||||
|
Usage: "Optimize database",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Optimizing database...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
skills/cli-patterns/examples/deploy-cli/README.md
Normal file
60
skills/cli-patterns/examples/deploy-cli/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Deployment CLI Tool Example
|
||||||
|
|
||||||
|
Complete deployment automation CLI demonstrating:
|
||||||
|
- Context management with shared state
|
||||||
|
- Environment validation in Before hook
|
||||||
|
- Command categories (Build, Deploy, Monitor)
|
||||||
|
- Confirmation prompts for destructive actions
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set environment variables
|
||||||
|
export DEPLOY_ENV=staging
|
||||||
|
export AWS_REGION=us-west-2
|
||||||
|
|
||||||
|
# Build application
|
||||||
|
deploy --env staging build
|
||||||
|
deploy -e production build --tag v1.2.3
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
deploy --env staging test
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
deploy --env staging deploy
|
||||||
|
deploy -e production deploy --auto-approve
|
||||||
|
|
||||||
|
# Rollback
|
||||||
|
deploy --env production rollback
|
||||||
|
|
||||||
|
# Monitor
|
||||||
|
deploy --env production logs --follow
|
||||||
|
deploy -e staging status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features Demonstrated
|
||||||
|
|
||||||
|
1. **Context Management**: Shared DeployContext across commands
|
||||||
|
2. **Environment Validation**: Before hook validates target environment
|
||||||
|
3. **Required Flags**: --env is required for all operations
|
||||||
|
4. **Confirmation Prompts**: Deploy asks for confirmation (unless --auto-approve)
|
||||||
|
5. **Command Categories**: Build, Deploy, Monitor
|
||||||
|
6. **Environment Variables**: DEPLOY_ENV, AWS_REGION fallbacks
|
||||||
|
7. **Shared State**: Context passed to all commands via metadata
|
||||||
|
|
||||||
|
## Context Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
type DeployContext struct {
|
||||||
|
Environment string
|
||||||
|
AWSRegion string
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in Before hook
|
||||||
|
ctx := &DeployContext{...}
|
||||||
|
c.App.Metadata["ctx"] = ctx
|
||||||
|
|
||||||
|
// Retrieve in command
|
||||||
|
ctx := c.App.Metadata["ctx"].(*DeployContext)
|
||||||
|
```
|
||||||
192
skills/cli-patterns/examples/deploy-cli/main.go
Normal file
192
skills/cli-patterns/examples/deploy-cli/main.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeployContext struct {
|
||||||
|
Environment string
|
||||||
|
AWSRegion string
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "deploy",
|
||||||
|
Usage: "Deployment automation CLI",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "env",
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
Usage: "Target environment",
|
||||||
|
EnvVars: []string{"DEPLOY_ENV"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "region",
|
||||||
|
Usage: "AWS region",
|
||||||
|
EnvVars: []string{"AWS_REGION"},
|
||||||
|
Value: "us-east-1",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "Enable verbose output",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
env := c.String("env")
|
||||||
|
region := c.String("region")
|
||||||
|
verbose := c.Bool("verbose")
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Println("🔧 Setting up deployment context...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate environment
|
||||||
|
validEnvs := []string{"dev", "staging", "production"}
|
||||||
|
valid := false
|
||||||
|
for _, e := range validEnvs {
|
||||||
|
if env == e {
|
||||||
|
valid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("invalid environment: %s (must be dev, staging, or production)", env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store context
|
||||||
|
ctx := &DeployContext{
|
||||||
|
Environment: env,
|
||||||
|
AWSRegion: region,
|
||||||
|
Verbose: verbose,
|
||||||
|
}
|
||||||
|
c.App.Metadata["ctx"] = ctx
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("Environment: %s\n", env)
|
||||||
|
fmt.Printf("Region: %s\n", region)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
// Build category
|
||||||
|
{
|
||||||
|
Name: "build",
|
||||||
|
Category: "Build",
|
||||||
|
Usage: "Build application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "tag",
|
||||||
|
Usage: "Docker image tag",
|
||||||
|
Value: "latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*DeployContext)
|
||||||
|
tag := c.String("tag")
|
||||||
|
|
||||||
|
fmt.Printf("Building for environment: %s\n", ctx.Environment)
|
||||||
|
fmt.Printf("Image tag: %s\n", tag)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test",
|
||||||
|
Category: "Build",
|
||||||
|
Usage: "Run tests",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Running test suite...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Deploy category
|
||||||
|
{
|
||||||
|
Name: "deploy",
|
||||||
|
Category: "Deploy",
|
||||||
|
Usage: "Deploy application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "auto-approve",
|
||||||
|
Usage: "Skip confirmation prompt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*DeployContext)
|
||||||
|
autoApprove := c.Bool("auto-approve")
|
||||||
|
|
||||||
|
fmt.Printf("Deploying to %s in %s...\n", ctx.Environment, ctx.AWSRegion)
|
||||||
|
|
||||||
|
if !autoApprove {
|
||||||
|
fmt.Print("Continue? (y/n): ")
|
||||||
|
// In real app: read user input
|
||||||
|
fmt.Println("y")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Deployment started...")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "rollback",
|
||||||
|
Category: "Deploy",
|
||||||
|
Usage: "Rollback to previous version",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*DeployContext)
|
||||||
|
fmt.Printf("Rolling back %s deployment...\n", ctx.Environment)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Monitor category
|
||||||
|
{
|
||||||
|
Name: "logs",
|
||||||
|
Category: "Monitor",
|
||||||
|
Usage: "View deployment logs",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "follow",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "Follow log output",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
follow := c.Bool("follow")
|
||||||
|
fmt.Println("Fetching logs...")
|
||||||
|
if follow {
|
||||||
|
fmt.Println("Following logs (Ctrl+C to stop)...")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Category: "Monitor",
|
||||||
|
Usage: "Check deployment status",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
ctx := c.App.Metadata["ctx"].(*DeployContext)
|
||||||
|
fmt.Printf("Deployment Status (%s):\n", ctx.Environment)
|
||||||
|
fmt.Println(" Status: Running")
|
||||||
|
fmt.Println(" Instances: 3/3")
|
||||||
|
fmt.Println(" Health: Healthy")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
52
skills/cli-patterns/scripts/add-command.sh
Executable file
52
skills/cli-patterns/scripts/add-command.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Add a new command to existing CLI
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ $# -lt 2 ]; then
|
||||||
|
echo "Usage: $0 <app-name> <command-name> [category]"
|
||||||
|
echo "Example: $0 myapp backup Deploy"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
APP_NAME="$1"
|
||||||
|
COMMAND_NAME="$2"
|
||||||
|
CATEGORY="${3:-General}"
|
||||||
|
|
||||||
|
if [ ! -d "$APP_NAME" ]; then
|
||||||
|
echo "Error: Directory $APP_NAME not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$APP_NAME"
|
||||||
|
|
||||||
|
# Create command implementation
|
||||||
|
FUNC_NAME="${COMMAND_NAME}Command"
|
||||||
|
|
||||||
|
cat >> commands.go <<EOF
|
||||||
|
|
||||||
|
func ${FUNC_NAME}(c *cli.Context) error {
|
||||||
|
fmt.Println("Executing ${COMMAND_NAME} command...")
|
||||||
|
// TODO: Implement ${COMMAND_NAME} logic
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Generate command definition
|
||||||
|
cat > /tmp/new_command.txt <<EOF
|
||||||
|
{
|
||||||
|
Name: "${COMMAND_NAME}",
|
||||||
|
Category: "${CATEGORY}",
|
||||||
|
Usage: "TODO: Add usage description",
|
||||||
|
Action: ${FUNC_NAME},
|
||||||
|
},
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ Command stub created!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Add the following to your Commands slice in main.go:"
|
||||||
|
cat /tmp/new_command.txt
|
||||||
|
echo ""
|
||||||
|
echo "2. Implement the logic in commands.go:${FUNC_NAME}"
|
||||||
|
echo "3. Add flags if needed"
|
||||||
109
skills/cli-patterns/scripts/generate-basic.sh
Executable file
109
skills/cli-patterns/scripts/generate-basic.sh
Executable file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Generate basic CLI structure with urfave/cli
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_NAME="${1:-myapp}"
|
||||||
|
|
||||||
|
echo "Generating basic CLI: $APP_NAME"
|
||||||
|
|
||||||
|
# Create project structure
|
||||||
|
mkdir -p "$APP_NAME"
|
||||||
|
cd "$APP_NAME"
|
||||||
|
|
||||||
|
# Initialize Go module
|
||||||
|
go mod init "$APP_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Install urfave/cli
|
||||||
|
echo "Installing urfave/cli v2..."
|
||||||
|
go get github.com/urfave/cli/v2@latest
|
||||||
|
|
||||||
|
# Create main.go
|
||||||
|
cat > main.go <<'EOF'
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "APP_NAME_PLACEHOLDER",
|
||||||
|
Usage: "A simple CLI tool",
|
||||||
|
Version: "0.1.0",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "Enable verbose output",
|
||||||
|
EnvVars: []string{"VERBOSE"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Path to config file",
|
||||||
|
EnvVars: []string{"CONFIG_PATH"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
verbose := c.Bool("verbose")
|
||||||
|
config := c.String("config")
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Println("Verbose mode enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config != "" {
|
||||||
|
fmt.Printf("Using config: %s\n", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Hello from APP_NAME_PLACEHOLDER!")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Replace placeholder
|
||||||
|
sed -i "s/APP_NAME_PLACEHOLDER/$APP_NAME/g" main.go
|
||||||
|
|
||||||
|
# Create README
|
||||||
|
cat > README.md <<EOF
|
||||||
|
# $APP_NAME
|
||||||
|
|
||||||
|
A CLI tool built with urfave/cli.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
go install
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
$APP_NAME --help
|
||||||
|
$APP_NAME --verbose
|
||||||
|
$APP_NAME --config config.yaml
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- \`VERBOSE\`: Enable verbose output
|
||||||
|
- \`CONFIG_PATH\`: Path to config file
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Build
|
||||||
|
echo "Building..."
|
||||||
|
go build -o "$APP_NAME" .
|
||||||
|
|
||||||
|
echo "✅ Basic CLI generated successfully!"
|
||||||
|
echo "Run: ./$APP_NAME --help"
|
||||||
313
skills/cli-patterns/scripts/generate-full.sh
Executable file
313
skills/cli-patterns/scripts/generate-full.sh
Executable file
@@ -0,0 +1,313 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Generate complete CLI with all patterns
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_NAME="${1:-myapp}"
|
||||||
|
|
||||||
|
echo "Generating full-featured CLI: $APP_NAME"
|
||||||
|
|
||||||
|
# Create project structure
|
||||||
|
mkdir -p "$APP_NAME"
|
||||||
|
cd "$APP_NAME"
|
||||||
|
|
||||||
|
# Initialize Go module
|
||||||
|
go mod init "$APP_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
go get github.com/urfave/cli/v2@latest
|
||||||
|
|
||||||
|
# Create main.go with all patterns
|
||||||
|
cat > main.go <<'EOF'
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppContext holds shared state
|
||||||
|
type AppContext struct {
|
||||||
|
Verbose bool
|
||||||
|
Config string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "APP_NAME_PLACEHOLDER",
|
||||||
|
Usage: "A full-featured CLI tool with all patterns",
|
||||||
|
Version: "0.1.0",
|
||||||
|
|
||||||
|
// Global flags available to all commands
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "Enable verbose output",
|
||||||
|
EnvVars: []string{"VERBOSE"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Path to config file",
|
||||||
|
EnvVars: []string{"CONFIG_PATH"},
|
||||||
|
Value: "config.yaml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Before hook - runs before any command
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
verbose := c.Bool("verbose")
|
||||||
|
config := c.String("config")
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Println("🚀 Initializing application...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store context for use in commands
|
||||||
|
ctx := &AppContext{
|
||||||
|
Verbose: verbose,
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
c.App.Metadata["ctx"] = ctx
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// After hook - runs after any command
|
||||||
|
After: func(c *cli.Context) error {
|
||||||
|
if ctx, ok := c.App.Metadata["ctx"].(*AppContext); ok {
|
||||||
|
if ctx.Verbose {
|
||||||
|
fmt.Println("✅ Application finished successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// Commands organized by category
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "build",
|
||||||
|
Category: "Build",
|
||||||
|
Usage: "Build the project",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "Output file path",
|
||||||
|
Value: "dist/app",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "optimize",
|
||||||
|
Usage: "Enable optimizations",
|
||||||
|
Value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Preparing build...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
output := c.String("output")
|
||||||
|
optimize := c.Bool("optimize")
|
||||||
|
|
||||||
|
fmt.Printf("Building to: %s\n", output)
|
||||||
|
if optimize {
|
||||||
|
fmt.Println("Optimizations: enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
After: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Build complete!")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test",
|
||||||
|
Category: "Build",
|
||||||
|
Usage: "Run tests",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "coverage",
|
||||||
|
Aliases: []string{"cov"},
|
||||||
|
Usage: "Generate coverage report",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
coverage := c.Bool("coverage")
|
||||||
|
|
||||||
|
fmt.Println("Running tests...")
|
||||||
|
if coverage {
|
||||||
|
fmt.Println("Generating coverage report...")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "deploy",
|
||||||
|
Category: "Deploy",
|
||||||
|
Usage: "Deploy the application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "env",
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
Usage: "Target environment",
|
||||||
|
Required: true,
|
||||||
|
Value: "staging",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
env := c.String("env")
|
||||||
|
fmt.Printf("Deploying to %s...\n", env)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "rollback",
|
||||||
|
Category: "Deploy",
|
||||||
|
Usage: "Rollback deployment",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Rolling back deployment...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "logs",
|
||||||
|
Category: "Monitor",
|
||||||
|
Usage: "View application logs",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "tail",
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
Usage: "Number of lines to show",
|
||||||
|
Value: 100,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "follow",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "Follow log output",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
tail := c.Int("tail")
|
||||||
|
follow := c.Bool("follow")
|
||||||
|
|
||||||
|
fmt.Printf("Showing last %d lines...\n", tail)
|
||||||
|
if follow {
|
||||||
|
fmt.Println("Following logs...")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Category: "Monitor",
|
||||||
|
Usage: "Check application status",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Application status: healthy")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Replace placeholder
|
||||||
|
sed -i "s/APP_NAME_PLACEHOLDER/$APP_NAME/g" main.go
|
||||||
|
|
||||||
|
# Create comprehensive README
|
||||||
|
cat > README.md <<EOF
|
||||||
|
# $APP_NAME
|
||||||
|
|
||||||
|
A full-featured CLI tool demonstrating all urfave/cli patterns.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Global flags with environment variable fallbacks
|
||||||
|
- ✅ Command categories for organization
|
||||||
|
- ✅ Before/After hooks for lifecycle management
|
||||||
|
- ✅ Context management for shared state
|
||||||
|
- ✅ Comprehensive flag types
|
||||||
|
- ✅ Subcommands and aliases
|
||||||
|
- ✅ Help text and documentation
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
go install
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
$APP_NAME build
|
||||||
|
$APP_NAME build --output dist/myapp --optimize
|
||||||
|
$APP_NAME test --coverage
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Deploy Commands
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
$APP_NAME deploy --env staging
|
||||||
|
$APP_NAME deploy -e production
|
||||||
|
$APP_NAME rollback
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Monitor Commands
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
$APP_NAME logs
|
||||||
|
$APP_NAME logs --tail 50 --follow
|
||||||
|
$APP_NAME status
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Global Flags
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
$APP_NAME --verbose build
|
||||||
|
$APP_NAME --config custom.yaml deploy --env prod
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- \`VERBOSE\`: Enable verbose output
|
||||||
|
- \`CONFIG_PATH\`: Path to config file
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Build with optimizations
|
||||||
|
$APP_NAME -v build -o dist/app --optimize
|
||||||
|
|
||||||
|
# Deploy to production
|
||||||
|
$APP_NAME --config prod.yaml deploy -e production
|
||||||
|
|
||||||
|
# Follow logs
|
||||||
|
$APP_NAME logs -f -n 200
|
||||||
|
\`\`\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Build
|
||||||
|
echo "Building..."
|
||||||
|
go build -o "$APP_NAME" .
|
||||||
|
|
||||||
|
echo "✅ Full-featured CLI generated successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Try these commands:"
|
||||||
|
echo " ./$APP_NAME --help"
|
||||||
|
echo " ./$APP_NAME build --help"
|
||||||
|
echo " ./$APP_NAME -v build"
|
||||||
174
skills/cli-patterns/scripts/generate-subcommands.sh
Executable file
174
skills/cli-patterns/scripts/generate-subcommands.sh
Executable file
@@ -0,0 +1,174 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Generate CLI with subcommands structure
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_NAME="${1:-myapp}"
|
||||||
|
|
||||||
|
echo "Generating CLI with subcommands: $APP_NAME"
|
||||||
|
|
||||||
|
# Create project structure
|
||||||
|
mkdir -p "$APP_NAME/commands"
|
||||||
|
cd "$APP_NAME"
|
||||||
|
|
||||||
|
# Initialize Go module
|
||||||
|
go mod init "$APP_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Install urfave/cli
|
||||||
|
echo "Installing urfave/cli v2..."
|
||||||
|
go get github.com/urfave/cli/v2@latest
|
||||||
|
|
||||||
|
# Create main.go
|
||||||
|
cat > main.go <<'EOF'
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "APP_NAME_PLACEHOLDER",
|
||||||
|
Usage: "A multi-command CLI tool",
|
||||||
|
Version: "0.1.0",
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "start",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "Start the service",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "port",
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
Value: 8080,
|
||||||
|
Usage: "Port to listen on",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
return startCommand(c)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "stop",
|
||||||
|
Usage: "Stop the service",
|
||||||
|
Action: stopCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Usage: "Check service status",
|
||||||
|
Action: statusCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "config",
|
||||||
|
Usage: "Configuration management",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "show",
|
||||||
|
Usage: "Show current configuration",
|
||||||
|
Action: configShowCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "set",
|
||||||
|
Usage: "Set configuration value",
|
||||||
|
Action: configSetCommand,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create commands.go
|
||||||
|
cat > commands.go <<'EOF'
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startCommand(c *cli.Context) error {
|
||||||
|
port := c.Int("port")
|
||||||
|
fmt.Printf("Starting service on port %d...\n", port)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCommand(c *cli.Context) error {
|
||||||
|
fmt.Println("Stopping service...")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusCommand(c *cli.Context) error {
|
||||||
|
fmt.Println("Service status: running")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configShowCommand(c *cli.Context) error {
|
||||||
|
fmt.Println("Current configuration:")
|
||||||
|
fmt.Println(" port: 8080")
|
||||||
|
fmt.Println(" host: localhost")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configSetCommand(c *cli.Context) error {
|
||||||
|
key := c.Args().Get(0)
|
||||||
|
value := c.Args().Get(1)
|
||||||
|
|
||||||
|
if key == "" || value == "" {
|
||||||
|
return fmt.Errorf("usage: config set <key> <value>")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Setting %s = %s\n", key, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Replace placeholder
|
||||||
|
sed -i "s/APP_NAME_PLACEHOLDER/$APP_NAME/g" main.go
|
||||||
|
|
||||||
|
# Create README
|
||||||
|
cat > README.md <<EOF
|
||||||
|
# $APP_NAME
|
||||||
|
|
||||||
|
A CLI tool with subcommands built with urfave/cli.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
go install
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Start service
|
||||||
|
$APP_NAME start --port 8080
|
||||||
|
$APP_NAME s -p 3000
|
||||||
|
|
||||||
|
# Stop service
|
||||||
|
$APP_NAME stop
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
$APP_NAME status
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
$APP_NAME config show
|
||||||
|
$APP_NAME config set host 0.0.0.0
|
||||||
|
\`\`\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Build
|
||||||
|
echo "Building..."
|
||||||
|
go build -o "$APP_NAME" .
|
||||||
|
|
||||||
|
echo "✅ CLI with subcommands generated successfully!"
|
||||||
|
echo "Run: ./$APP_NAME --help"
|
||||||
103
skills/cli-patterns/scripts/validate-cli.sh
Executable file
103
skills/cli-patterns/scripts/validate-cli.sh
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Validate CLI structure and best practices
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_PATH="${1:-.}"
|
||||||
|
|
||||||
|
echo "🔍 Validating CLI project: $PROJECT_PATH"
|
||||||
|
|
||||||
|
cd "$PROJECT_PATH"
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Check if main.go exists
|
||||||
|
if [ ! -f "main.go" ]; then
|
||||||
|
echo "❌ main.go not found"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ main.go exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if go.mod exists
|
||||||
|
if [ ! -f "go.mod" ]; then
|
||||||
|
echo "❌ go.mod not found (run 'go mod init')"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ go.mod exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for urfave/cli dependency
|
||||||
|
if grep -q "github.com/urfave/cli/v2" go.mod 2>/dev/null; then
|
||||||
|
echo "✅ urfave/cli dependency found"
|
||||||
|
else
|
||||||
|
echo "⚠️ urfave/cli dependency not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for App definition
|
||||||
|
if grep -q "cli.App" main.go 2>/dev/null; then
|
||||||
|
echo "✅ cli.App definition found"
|
||||||
|
else
|
||||||
|
echo "❌ cli.App definition not found"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for Usage field
|
||||||
|
if grep -q "Usage:" main.go 2>/dev/null; then
|
||||||
|
echo "✅ Usage field defined"
|
||||||
|
else
|
||||||
|
echo "⚠️ Usage field not found (recommended)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for Version field
|
||||||
|
if grep -q "Version:" main.go 2>/dev/null; then
|
||||||
|
echo "✅ Version field defined"
|
||||||
|
else
|
||||||
|
echo "⚠️ Version field not found (recommended)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if commands have descriptions
|
||||||
|
if grep -A 5 "Commands:" main.go 2>/dev/null | grep -q "Usage:"; then
|
||||||
|
echo "✅ Commands have usage descriptions"
|
||||||
|
else
|
||||||
|
echo "⚠️ Some commands might be missing usage descriptions"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for proper error handling
|
||||||
|
if grep -q "if err := app.Run" main.go 2>/dev/null; then
|
||||||
|
echo "✅ Proper error handling in main"
|
||||||
|
else
|
||||||
|
echo "❌ Missing error handling for app.Run"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to build
|
||||||
|
echo ""
|
||||||
|
echo "🔨 Attempting build..."
|
||||||
|
if go build -o /tmp/test_build . 2>&1; then
|
||||||
|
echo "✅ Build successful"
|
||||||
|
rm -f /tmp/test_build
|
||||||
|
else
|
||||||
|
echo "❌ Build failed"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run go vet
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Running go vet..."
|
||||||
|
if go vet ./... 2>&1; then
|
||||||
|
echo "✅ go vet passed"
|
||||||
|
else
|
||||||
|
echo "⚠️ go vet found issues"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
|
if [ $ERRORS -eq 0 ]; then
|
||||||
|
echo "✅ Validation passed! No critical errors found."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "❌ Validation failed with $ERRORS critical error(s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
52
skills/cli-patterns/templates/basic-cli.go
Normal file
52
skills/cli-patterns/templates/basic-cli.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "myapp",
|
||||||
|
Usage: "A simple CLI application",
|
||||||
|
Version: "0.1.0",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "Enable verbose output",
|
||||||
|
EnvVars: []string{"VERBOSE"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Path to config file",
|
||||||
|
EnvVars: []string{"CONFIG_PATH"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
verbose := c.Bool("verbose")
|
||||||
|
config := c.String("config")
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Println("Verbose mode enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config != "" {
|
||||||
|
fmt.Printf("Using config: %s\n", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your application logic here
|
||||||
|
fmt.Println("Hello, World!")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
141
skills/cli-patterns/templates/categories-cli.go
Normal file
141
skills/cli-patterns/templates/categories-cli.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "myapp",
|
||||||
|
Usage: "CLI tool with categorized commands",
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
// Database category
|
||||||
|
{
|
||||||
|
Name: "create-db",
|
||||||
|
Category: "Database",
|
||||||
|
Usage: "Create a new database",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Creating database...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "migrate",
|
||||||
|
Category: "Database",
|
||||||
|
Usage: "Run database migrations",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Running migrations...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "seed",
|
||||||
|
Category: "Database",
|
||||||
|
Usage: "Seed database with test data",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Seeding database...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Deploy category
|
||||||
|
{
|
||||||
|
Name: "deploy",
|
||||||
|
Category: "Deploy",
|
||||||
|
Usage: "Deploy application",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "env",
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
Usage: "Target environment",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
env := c.String("env")
|
||||||
|
fmt.Printf("Deploying to %s...\n", env)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "rollback",
|
||||||
|
Category: "Deploy",
|
||||||
|
Usage: "Rollback deployment",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Rolling back...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Monitor category
|
||||||
|
{
|
||||||
|
Name: "logs",
|
||||||
|
Category: "Monitor",
|
||||||
|
Usage: "View application logs",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "follow",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "Follow log output",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
follow := c.Bool("follow")
|
||||||
|
fmt.Println("Fetching logs...")
|
||||||
|
if follow {
|
||||||
|
fmt.Println("Following logs (Ctrl+C to stop)...")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Category: "Monitor",
|
||||||
|
Usage: "Check application status",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Status: Running")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "metrics",
|
||||||
|
Category: "Monitor",
|
||||||
|
Usage: "View application metrics",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Fetching metrics...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Config category
|
||||||
|
{
|
||||||
|
Name: "show-config",
|
||||||
|
Category: "Config",
|
||||||
|
Usage: "Show current configuration",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Current configuration:")
|
||||||
|
fmt.Println(" env: production")
|
||||||
|
fmt.Println(" port: 8080")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "set-config",
|
||||||
|
Category: "Config",
|
||||||
|
Usage: "Set configuration value",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Setting configuration...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
52
skills/cli-patterns/templates/click-basic.py
Normal file
52
skills/cli-patterns/templates/click-basic.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Python equivalent using click (similar API to urfave/cli)
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.version_option('0.1.0')
|
||||||
|
@click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
|
||||||
|
@click.option('--config', '-c', envvar='CONFIG_PATH', help='Path to config file')
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, verbose, config):
|
||||||
|
"""A simple CLI application"""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj['verbose'] = verbose
|
||||||
|
ctx.obj['config'] = config
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
click.echo('Verbose mode enabled')
|
||||||
|
|
||||||
|
if config:
|
||||||
|
click.echo(f'Using config: {config}')
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option('--port', '-p', default=8080, help='Port to listen on')
|
||||||
|
@click.pass_context
|
||||||
|
def start(ctx, port):
|
||||||
|
"""Start the service"""
|
||||||
|
if ctx.obj['verbose']:
|
||||||
|
click.echo(f'Starting service on port {port}')
|
||||||
|
else:
|
||||||
|
click.echo(f'Starting on port {port}')
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
def stop(ctx):
|
||||||
|
"""Stop the service"""
|
||||||
|
click.echo('Stopping service...')
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def status():
|
||||||
|
"""Check service status"""
|
||||||
|
click.echo('Service is running')
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@click.argument('value')
|
||||||
|
def config(key, value):
|
||||||
|
"""Set configuration value"""
|
||||||
|
click.echo(f'Setting {key} = {value}')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cli(obj={})
|
||||||
51
skills/cli-patterns/templates/commander-basic.ts
Normal file
51
skills/cli-patterns/templates/commander-basic.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// TypeScript equivalent using commander.js (similar API to urfave/cli)
|
||||||
|
|
||||||
|
import { Command } from 'commander';
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('myapp')
|
||||||
|
.description('A simple CLI application')
|
||||||
|
.version('0.1.0');
|
||||||
|
|
||||||
|
program
|
||||||
|
.option('-v, --verbose', 'Enable verbose output')
|
||||||
|
.option('-c, --config <path>', 'Path to config file', process.env.CONFIG_PATH)
|
||||||
|
.action((options) => {
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log('Verbose mode enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.config) {
|
||||||
|
console.log(`Using config: ${options.config}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Hello, World!');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subcommands
|
||||||
|
program
|
||||||
|
.command('start')
|
||||||
|
.description('Start the service')
|
||||||
|
.option('-p, --port <number>', 'Port to listen on', '8080')
|
||||||
|
.action((options) => {
|
||||||
|
console.log(`Starting service on port ${options.port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('stop')
|
||||||
|
.description('Stop the service')
|
||||||
|
.action(() => {
|
||||||
|
console.log('Stopping service...');
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('status')
|
||||||
|
.description('Check service status')
|
||||||
|
.action(() => {
|
||||||
|
console.log('Service is running');
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse();
|
||||||
152
skills/cli-patterns/templates/context-cli.go
Normal file
152
skills/cli-patterns/templates/context-cli.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppContext holds shared state across commands
|
||||||
|
type AppContext struct {
|
||||||
|
Config *Config
|
||||||
|
DB *sql.DB
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents application configuration
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Database string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "context-demo",
|
||||||
|
Usage: "Demonstration of context and state management",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "Enable verbose output",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "Path to config file",
|
||||||
|
Value: "config.yaml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Initialize shared context
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
verbose := c.Bool("verbose")
|
||||||
|
configPath := c.String("config")
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("Loading config from: %s\n", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create application context
|
||||||
|
appCtx := &AppContext{
|
||||||
|
Config: &Config{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
Database: "mydb",
|
||||||
|
},
|
||||||
|
Verbose: verbose,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate database connection
|
||||||
|
// In real app: appCtx.DB, err = sql.Open("postgres", connStr)
|
||||||
|
if verbose {
|
||||||
|
fmt.Println("Connected to database")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store context in app metadata
|
||||||
|
c.App.Metadata["ctx"] = appCtx
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cleanup shared resources
|
||||||
|
After: func(c *cli.Context) error {
|
||||||
|
if ctx, ok := c.App.Metadata["ctx"].(*AppContext); ok {
|
||||||
|
if ctx.DB != nil {
|
||||||
|
// ctx.DB.Close()
|
||||||
|
if ctx.Verbose {
|
||||||
|
fmt.Println("Database connection closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "query",
|
||||||
|
Usage: "Execute a database query",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
// Retrieve context
|
||||||
|
ctx := c.App.Metadata["ctx"].(*AppContext)
|
||||||
|
|
||||||
|
if ctx.Verbose {
|
||||||
|
fmt.Printf("Connecting to %s:%d/%s\n",
|
||||||
|
ctx.Config.Host,
|
||||||
|
ctx.Config.Port,
|
||||||
|
ctx.Config.Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Executing query...")
|
||||||
|
// Use ctx.DB for actual query
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "migrate",
|
||||||
|
Usage: "Run database migrations",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
// Retrieve context
|
||||||
|
ctx := c.App.Metadata["ctx"].(*AppContext)
|
||||||
|
|
||||||
|
if ctx.Verbose {
|
||||||
|
fmt.Println("Running migrations with context...")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Migrating database: %s\n", ctx.Config.Database)
|
||||||
|
// Use ctx.DB for migrations
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Usage: "Check database status",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
// Retrieve context
|
||||||
|
ctx := c.App.Metadata["ctx"].(*AppContext)
|
||||||
|
|
||||||
|
fmt.Printf("Database: %s\n", ctx.Config.Database)
|
||||||
|
fmt.Printf("Host: %s:%d\n", ctx.Config.Host, ctx.Config.Port)
|
||||||
|
fmt.Println("Status: Connected")
|
||||||
|
|
||||||
|
if ctx.Verbose {
|
||||||
|
fmt.Println("Verbose mode: enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
172
skills/cli-patterns/templates/flags-demo.go
Normal file
172
skills/cli-patterns/templates/flags-demo.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "flags-demo",
|
||||||
|
Usage: "Demonstration of all flag types in urfave/cli",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
// String flag
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
Value: "World",
|
||||||
|
Usage: "Name to greet",
|
||||||
|
EnvVars: []string{"GREETING_NAME"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Int flag
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "count",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Value: 1,
|
||||||
|
Usage: "Number of times to repeat",
|
||||||
|
EnvVars: []string{"REPEAT_COUNT"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bool flag
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "Enable verbose output",
|
||||||
|
EnvVars: []string{"VERBOSE"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Int64 flag
|
||||||
|
&cli.Int64Flag{
|
||||||
|
Name: "size",
|
||||||
|
Value: 1024,
|
||||||
|
Usage: "Size in bytes",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Uint flag
|
||||||
|
&cli.UintFlag{
|
||||||
|
Name: "port",
|
||||||
|
Value: 8080,
|
||||||
|
Usage: "Port number",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Float64 flag
|
||||||
|
&cli.Float64Flag{
|
||||||
|
Name: "timeout",
|
||||||
|
Value: 30.0,
|
||||||
|
Usage: "Timeout in seconds",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Duration flag
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "wait",
|
||||||
|
Value: 10 * time.Second,
|
||||||
|
Usage: "Wait duration",
|
||||||
|
},
|
||||||
|
|
||||||
|
// StringSlice flag (multiple values)
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "tag",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Usage: "Tags (can be specified multiple times)",
|
||||||
|
},
|
||||||
|
|
||||||
|
// IntSlice flag (multiple int values)
|
||||||
|
&cli.IntSliceFlag{
|
||||||
|
Name: "priority",
|
||||||
|
Usage: "Priority values",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Required flag
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "token",
|
||||||
|
Usage: "API token (required)",
|
||||||
|
Required: true,
|
||||||
|
EnvVars: []string{"API_TOKEN"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Flag with default from env
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "env",
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
Value: "development",
|
||||||
|
Usage: "Environment name",
|
||||||
|
EnvVars: []string{"ENV", "ENVIRONMENT"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hidden flag (not shown in help)
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "secret",
|
||||||
|
Usage: "Secret value",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
// String flag
|
||||||
|
name := c.String("name")
|
||||||
|
fmt.Printf("Name: %s\n", name)
|
||||||
|
|
||||||
|
// Int flag
|
||||||
|
count := c.Int("count")
|
||||||
|
fmt.Printf("Count: %d\n", count)
|
||||||
|
|
||||||
|
// Bool flag
|
||||||
|
verbose := c.Bool("verbose")
|
||||||
|
if verbose {
|
||||||
|
fmt.Println("Verbose mode: enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 flag
|
||||||
|
size := c.Int64("size")
|
||||||
|
fmt.Printf("Size: %d bytes\n", size)
|
||||||
|
|
||||||
|
// Uint flag
|
||||||
|
port := c.Uint("port")
|
||||||
|
fmt.Printf("Port: %d\n", port)
|
||||||
|
|
||||||
|
// Float64 flag
|
||||||
|
timeout := c.Float64("timeout")
|
||||||
|
fmt.Printf("Timeout: %.2f seconds\n", timeout)
|
||||||
|
|
||||||
|
// Duration flag
|
||||||
|
wait := c.Duration("wait")
|
||||||
|
fmt.Printf("Wait: %s\n", wait)
|
||||||
|
|
||||||
|
// StringSlice flag
|
||||||
|
tags := c.StringSlice("tag")
|
||||||
|
if len(tags) > 0 {
|
||||||
|
fmt.Printf("Tags: %v\n", tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntSlice flag
|
||||||
|
priorities := c.IntSlice("priority")
|
||||||
|
if len(priorities) > 0 {
|
||||||
|
fmt.Printf("Priorities: %v\n", priorities)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required flag
|
||||||
|
token := c.String("token")
|
||||||
|
fmt.Printf("Token: %s\n", token)
|
||||||
|
|
||||||
|
// Environment flag
|
||||||
|
env := c.String("env")
|
||||||
|
fmt.Printf("Environment: %s\n", env)
|
||||||
|
|
||||||
|
// Greeting logic
|
||||||
|
fmt.Println("\n---")
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
fmt.Printf("Hello, %s!\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
94
skills/cli-patterns/templates/hooks-cli.go
Normal file
94
skills/cli-patterns/templates/hooks-cli.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "hooks-demo",
|
||||||
|
Usage: "Demonstration of Before/After hooks",
|
||||||
|
|
||||||
|
// Global Before hook - runs before any command
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
fmt.Println("🚀 [GLOBAL BEFORE] Initializing application...")
|
||||||
|
fmt.Println(" - Loading configuration")
|
||||||
|
fmt.Println(" - Setting up connections")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// Global After hook - runs after any command
|
||||||
|
After: func(c *cli.Context) error {
|
||||||
|
fmt.Println("✅ [GLOBAL AFTER] Cleaning up...")
|
||||||
|
fmt.Println(" - Closing connections")
|
||||||
|
fmt.Println(" - Saving state")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "process",
|
||||||
|
Usage: "Process data with hooks",
|
||||||
|
|
||||||
|
// Command-specific Before hook
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
fmt.Println(" [COMMAND BEFORE] Preparing to process...")
|
||||||
|
fmt.Println(" - Validating input")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// Command action
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println(" [ACTION] Processing data...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// Command-specific After hook
|
||||||
|
After: func(c *cli.Context) error {
|
||||||
|
fmt.Println(" [COMMAND AFTER] Processing complete!")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "validate",
|
||||||
|
Usage: "Validate configuration",
|
||||||
|
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
fmt.Println(" [COMMAND BEFORE] Starting validation...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println(" [ACTION] Validating...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
After: func(c *cli.Context) error {
|
||||||
|
fmt.Println(" [COMMAND AFTER] Validation complete!")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example output when running "hooks-demo process":
|
||||||
|
// 🚀 [GLOBAL BEFORE] Initializing application...
|
||||||
|
// - Loading configuration
|
||||||
|
// - Setting up connections
|
||||||
|
// [COMMAND BEFORE] Preparing to process...
|
||||||
|
// - Validating input
|
||||||
|
// [ACTION] Processing data...
|
||||||
|
// [COMMAND AFTER] Processing complete!
|
||||||
|
// ✅ [GLOBAL AFTER] Cleaning up...
|
||||||
|
// - Closing connections
|
||||||
|
// - Saving state
|
||||||
116
skills/cli-patterns/templates/subcommands-cli.go
Normal file
116
skills/cli-patterns/templates/subcommands-cli.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "myapp",
|
||||||
|
Usage: "A CLI tool with subcommands",
|
||||||
|
Version: "0.1.0",
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "start",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "Start the service",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "port",
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
Value: 8080,
|
||||||
|
Usage: "Port to listen on",
|
||||||
|
EnvVars: []string{"PORT"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "host",
|
||||||
|
Value: "localhost",
|
||||||
|
Usage: "Host to bind to",
|
||||||
|
EnvVars: []string{"HOST"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
port := c.Int("port")
|
||||||
|
host := c.String("host")
|
||||||
|
fmt.Printf("Starting service on %s:%d\n", host, port)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "stop",
|
||||||
|
Usage: "Stop the service",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Stopping service...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "restart",
|
||||||
|
Usage: "Restart the service",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Restarting service...")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Usage: "Check service status",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Service is running")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "config",
|
||||||
|
Usage: "Configuration management",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "show",
|
||||||
|
Usage: "Show current configuration",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
fmt.Println("Current configuration:")
|
||||||
|
fmt.Println(" port: 8080")
|
||||||
|
fmt.Println(" host: localhost")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "set",
|
||||||
|
Usage: "Set configuration value",
|
||||||
|
ArgsUsage: "<key> <value>",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
if c.NArg() < 2 {
|
||||||
|
return fmt.Errorf("usage: config set <key> <value>")
|
||||||
|
}
|
||||||
|
key := c.Args().Get(0)
|
||||||
|
value := c.Args().Get(1)
|
||||||
|
fmt.Printf("Setting %s = %s\n", key, value)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "get",
|
||||||
|
Usage: "Get configuration value",
|
||||||
|
ArgsUsage: "<key>",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
if c.NArg() < 1 {
|
||||||
|
return fmt.Errorf("usage: config get <key>")
|
||||||
|
}
|
||||||
|
key := c.Args().Get(0)
|
||||||
|
fmt.Printf("%s = <value>\n", key)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
skills/cli-patterns/templates/typer-basic.py
Normal file
58
skills/cli-patterns/templates/typer-basic.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Modern Python CLI using typer (FastAPI style)
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from typing import Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
class Environment(str, Enum):
|
||||||
|
development = "development"
|
||||||
|
staging = "staging"
|
||||||
|
production = "production"
|
||||||
|
|
||||||
|
@app.callback()
|
||||||
|
def main(
|
||||||
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
|
||||||
|
config: Optional[str] = typer.Option(None, "--config", "-c", envvar="CONFIG_PATH", help="Path to config file")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
A simple CLI application built with Typer
|
||||||
|
"""
|
||||||
|
if verbose:
|
||||||
|
typer.echo("Verbose mode enabled")
|
||||||
|
|
||||||
|
if config:
|
||||||
|
typer.echo(f"Using config: {config}")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def start(
|
||||||
|
port: int = typer.Option(8080, "--port", "-p", help="Port to listen on"),
|
||||||
|
host: str = typer.Option("localhost", help="Host to bind to"),
|
||||||
|
):
|
||||||
|
"""Start the service"""
|
||||||
|
typer.echo(f"Starting service on {host}:{port}")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def stop():
|
||||||
|
"""Stop the service"""
|
||||||
|
typer.echo("Stopping service...")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def status():
|
||||||
|
"""Check service status"""
|
||||||
|
typer.echo("Service is running")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def deploy(
|
||||||
|
env: Environment = typer.Option(..., "--env", "-e", help="Target environment"),
|
||||||
|
force: bool = typer.Option(False, "--force", help="Force deployment")
|
||||||
|
):
|
||||||
|
"""Deploy to environment"""
|
||||||
|
typer.echo(f"Deploying to {env.value}...")
|
||||||
|
if force:
|
||||||
|
typer.echo("Force flag enabled")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
155
skills/cli-testing-patterns/SKILL.md
Normal file
155
skills/cli-testing-patterns/SKILL.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
name: cli-testing-patterns
|
||||||
|
description: CLI testing strategies and patterns for Node.js (Jest) and Python (pytest, Click.testing.CliRunner). Use when writing tests for CLI tools, testing command execution, validating exit codes, testing output, implementing CLI test suites, or when user mentions CLI testing, Jest CLI tests, pytest CLI, Click.testing.CliRunner, command testing, or exit code validation.
|
||||||
|
allowed-tools: Read, Write, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
# CLI Testing Patterns
|
||||||
|
|
||||||
|
Comprehensive testing strategies for CLI applications using industry-standard testing frameworks. Covers command execution testing, exit code validation, output verification, interactive prompt testing, and integration testing patterns.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### When Testing Node.js CLI Tools
|
||||||
|
|
||||||
|
1. **Use Jest for testing CLI commands**
|
||||||
|
- Import `child_process.execSync` for command execution
|
||||||
|
- Create helper function to run CLI and capture output
|
||||||
|
- Test exit codes, stdout, stderr separately
|
||||||
|
- Handle both success and error cases
|
||||||
|
|
||||||
|
2. **Test Structure**
|
||||||
|
- Set up CLI path relative to test location
|
||||||
|
- Create `runCLI()` helper that returns `{stdout, stderr, code}`
|
||||||
|
- Use try-catch to handle non-zero exit codes
|
||||||
|
- Test common scenarios: version, help, unknown commands
|
||||||
|
|
||||||
|
3. **What to Test**
|
||||||
|
- Command execution with various argument combinations
|
||||||
|
- Exit code validation (0 for success, non-zero for errors)
|
||||||
|
- Output content (stdout) validation
|
||||||
|
- Error messages (stderr) validation
|
||||||
|
- Configuration file handling
|
||||||
|
- Interactive prompts (with mocked input)
|
||||||
|
|
||||||
|
### When Testing Python CLI Tools
|
||||||
|
|
||||||
|
1. **Use pytest with Click.testing.CliRunner**
|
||||||
|
- Import `CliRunner` from `click.testing`
|
||||||
|
- Create runner fixture for reusable test setup
|
||||||
|
- Invoke commands with `runner.invoke(cli, ['args'])`
|
||||||
|
- Check `result.exit_code` and `result.output`
|
||||||
|
|
||||||
|
2. **Test Structure**
|
||||||
|
- Create pytest fixture for CliRunner instance
|
||||||
|
- Use `runner.invoke()` to execute CLI commands
|
||||||
|
- Access results through `result` object
|
||||||
|
- Simulate interactive input with `input='responses\n'`
|
||||||
|
|
||||||
|
3. **What to Test**
|
||||||
|
- Command invocation with various arguments
|
||||||
|
- Exit code validation
|
||||||
|
- Output content verification
|
||||||
|
- Error handling and messages
|
||||||
|
- Interactive prompt responses
|
||||||
|
- Configuration handling
|
||||||
|
|
||||||
|
### Exit Code Testing Patterns
|
||||||
|
|
||||||
|
**Standard Exit Codes:**
|
||||||
|
- `0` - Success
|
||||||
|
- `1` - General error
|
||||||
|
- `2` - Misuse of command (invalid arguments)
|
||||||
|
- `126` - Command cannot execute
|
||||||
|
- `127` - Command not found
|
||||||
|
- `128+N` - Fatal error signal N
|
||||||
|
|
||||||
|
**Testing Strategy:**
|
||||||
|
- Always test both success (0) and failure (non-zero) cases
|
||||||
|
- Verify specific exit codes for different error conditions
|
||||||
|
- Test argument validation returns appropriate codes
|
||||||
|
- Ensure help/version return 0 (success)
|
||||||
|
|
||||||
|
### Output Validation Patterns
|
||||||
|
|
||||||
|
**Content Testing:**
|
||||||
|
- Check for presence of key text in output
|
||||||
|
- Validate format (JSON, YAML, tables)
|
||||||
|
- Test color/formatting codes (if applicable)
|
||||||
|
- Verify error messages are user-friendly
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
- Use `.toContain()` for flexible matching (Jest)
|
||||||
|
- Use `in result.output` for Python tests
|
||||||
|
- Test both positive and negative cases
|
||||||
|
- Validate complete workflows (multi-command)
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
Use these templates for CLI testing:
|
||||||
|
|
||||||
|
### Node.js/Jest Templates
|
||||||
|
- `templates/jest-cli-test.ts` - Complete Jest test suite with execSync
|
||||||
|
- `templates/jest-config-test.ts` - Configuration file testing
|
||||||
|
- `templates/jest-integration-test.ts` - Multi-command integration tests
|
||||||
|
|
||||||
|
### Python/Pytest Templates
|
||||||
|
- `templates/pytest-click-test.py` - Click.testing.CliRunner tests
|
||||||
|
- `templates/pytest-fixtures.py` - Reusable pytest fixtures
|
||||||
|
- `templates/pytest-integration-test.py` - Integration test patterns
|
||||||
|
|
||||||
|
### Test Utilities
|
||||||
|
- `templates/test-helpers.ts` - Node.js test helper functions
|
||||||
|
- `templates/test-helpers.py` - Python test helper functions
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
Use these scripts for test setup and execution:
|
||||||
|
|
||||||
|
- `scripts/setup-jest-testing.sh` - Install Jest and configure for CLI testing
|
||||||
|
- `scripts/setup-pytest-testing.sh` - Install pytest and Click testing dependencies
|
||||||
|
- `scripts/run-cli-tests.sh` - Execute all CLI tests with coverage
|
||||||
|
- `scripts/validate-test-coverage.sh` - Check test coverage thresholds
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See complete examples in the `examples/` directory:
|
||||||
|
|
||||||
|
- `examples/jest-basic/` - Basic Jest CLI testing setup
|
||||||
|
- `examples/jest-advanced/` - Advanced Jest patterns with mocking
|
||||||
|
- `examples/pytest-click/` - Click.testing.CliRunner examples
|
||||||
|
- `examples/integration-testing/` - Full integration test suites
|
||||||
|
- `examples/exit-code-testing/` - Exit code validation patterns
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
**Node.js Testing:**
|
||||||
|
- Jest 29.x or later
|
||||||
|
- TypeScript support (ts-jest)
|
||||||
|
- Node.js 16+
|
||||||
|
|
||||||
|
**Python Testing:**
|
||||||
|
- pytest 7.x or later
|
||||||
|
- Click 8.x or later
|
||||||
|
- Python 3.8+
|
||||||
|
|
||||||
|
**Both:**
|
||||||
|
- Test coverage reporting tools
|
||||||
|
- CI/CD integration support
|
||||||
|
- Mock/stub capabilities for external dependencies
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Test in Isolation** - Each test should be independent
|
||||||
|
2. **Mock External Dependencies** - Don't make real API calls or file system changes
|
||||||
|
3. **Test Error Paths** - Test failures as thoroughly as successes
|
||||||
|
4. **Use Fixtures** - Share setup code across tests
|
||||||
|
5. **Clear Test Names** - Name tests to describe what they validate
|
||||||
|
6. **Fast Execution** - Keep tests fast for rapid feedback
|
||||||
|
7. **Coverage Goals** - Aim for 80%+ code coverage
|
||||||
|
8. **Integration Tests** - Test complete workflows, not just units
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Purpose**: Standardize CLI testing across Node.js and Python projects
|
||||||
|
**Load when**: Writing tests for CLI tools, validating command execution, testing exit codes
|
||||||
406
skills/cli-testing-patterns/examples/exit-code-testing/README.md
Normal file
406
skills/cli-testing-patterns/examples/exit-code-testing/README.md
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
# Exit Code Testing Patterns
|
||||||
|
|
||||||
|
Comprehensive guide to testing CLI exit codes correctly.
|
||||||
|
|
||||||
|
## Standard Exit Codes
|
||||||
|
|
||||||
|
### POSIX Standard Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning | When to Use |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| 0 | Success | Command completed successfully |
|
||||||
|
| 1 | General Error | Catchall for general errors |
|
||||||
|
| 2 | Misuse of Command | Invalid arguments or options |
|
||||||
|
| 126 | Command Cannot Execute | Permission problem or not executable |
|
||||||
|
| 127 | Command Not Found | Command not found in PATH |
|
||||||
|
| 128+N | Fatal Error Signal N | Process terminated by signal N |
|
||||||
|
| 130 | Ctrl+C Termination | Process terminated by SIGINT |
|
||||||
|
|
||||||
|
### Custom Application Exit Codes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define custom exit codes
|
||||||
|
enum ExitCode {
|
||||||
|
SUCCESS = 0,
|
||||||
|
GENERAL_ERROR = 1,
|
||||||
|
INVALID_ARGUMENT = 2,
|
||||||
|
CONFIG_ERROR = 3,
|
||||||
|
NETWORK_ERROR = 4,
|
||||||
|
AUTH_ERROR = 5,
|
||||||
|
NOT_FOUND = 6,
|
||||||
|
ALREADY_EXISTS = 7,
|
||||||
|
PERMISSION_DENIED = 8,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node.js Exit Code Testing
|
||||||
|
|
||||||
|
### Basic Exit Code Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Exit Code Tests', () => {
|
||||||
|
test('success returns 0', () => {
|
||||||
|
const { code } = runCLI('status');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('general error returns 1', () => {
|
||||||
|
const { code } = runCLI('fail-command');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid argument returns 2', () => {
|
||||||
|
const { code } = runCLI('deploy --invalid-env unknown');
|
||||||
|
expect(code).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('command not found returns 127', () => {
|
||||||
|
const { code } = runCLI('nonexistent-command');
|
||||||
|
expect(code).toBe(127);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Error Conditions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Specific Exit Codes', () => {
|
||||||
|
test('configuration error', () => {
|
||||||
|
const { code, stderr } = runCLI('deploy production');
|
||||||
|
expect(code).toBe(3); // CONFIG_ERROR
|
||||||
|
expect(stderr).toContain('configuration');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('network error', () => {
|
||||||
|
// Mock network failure
|
||||||
|
const { code, stderr } = runCLI('fetch --url https://unreachable.example.com');
|
||||||
|
expect(code).toBe(4); // NETWORK_ERROR
|
||||||
|
expect(stderr).toContain('network');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('authentication error', () => {
|
||||||
|
const { code, stderr } = runCLI('login --token invalid');
|
||||||
|
expect(code).toBe(5); // AUTH_ERROR
|
||||||
|
expect(stderr).toContain('authentication');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resource not found', () => {
|
||||||
|
const { code, stderr } = runCLI('get resource-123');
|
||||||
|
expect(code).toBe(6); // NOT_FOUND
|
||||||
|
expect(stderr).toContain('not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resource already exists', () => {
|
||||||
|
runCLI('create my-resource');
|
||||||
|
const { code, stderr } = runCLI('create my-resource');
|
||||||
|
expect(code).toBe(7); // ALREADY_EXISTS
|
||||||
|
expect(stderr).toContain('already exists');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Exit Code Consistency
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Exit Code Consistency', () => {
|
||||||
|
const errorScenarios = [
|
||||||
|
{ args: 'deploy', expectedCode: 2, reason: 'missing required argument' },
|
||||||
|
{ args: 'deploy --env invalid', expectedCode: 2, reason: 'invalid environment' },
|
||||||
|
{ args: 'config get missing', expectedCode: 6, reason: 'config key not found' },
|
||||||
|
{ args: 'unknown-cmd', expectedCode: 127, reason: 'command not found' },
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(errorScenarios)(
|
||||||
|
'should return exit code $expectedCode for $reason',
|
||||||
|
({ args, expectedCode }) => {
|
||||||
|
const { code } = runCLI(args);
|
||||||
|
expect(code).toBe(expectedCode);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python Exit Code Testing
|
||||||
|
|
||||||
|
### Basic Exit Code Testing
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestExitCodes:
|
||||||
|
"""Test CLI exit codes"""
|
||||||
|
|
||||||
|
def test_success_exit_code(self, runner):
|
||||||
|
"""Success should return 0"""
|
||||||
|
result = runner.invoke(cli, ['status'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_general_error_exit_code(self, runner):
|
||||||
|
"""General error should return 1"""
|
||||||
|
result = runner.invoke(cli, ['fail-command'])
|
||||||
|
assert result.exit_code == 1
|
||||||
|
|
||||||
|
def test_usage_error_exit_code(self, runner):
|
||||||
|
"""Usage error should return 2"""
|
||||||
|
result = runner.invoke(cli, ['deploy']) # Missing required arg
|
||||||
|
assert result.exit_code == 2
|
||||||
|
|
||||||
|
def test_unknown_command_exit_code(self, runner):
|
||||||
|
"""Unknown command handling"""
|
||||||
|
result = runner.invoke(cli, ['nonexistent'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Exit Codes with Click
|
||||||
|
|
||||||
|
```python
|
||||||
|
import click
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Define custom exit codes
|
||||||
|
class ExitCode:
|
||||||
|
SUCCESS = 0
|
||||||
|
GENERAL_ERROR = 1
|
||||||
|
INVALID_ARGUMENT = 2
|
||||||
|
CONFIG_ERROR = 3
|
||||||
|
NETWORK_ERROR = 4
|
||||||
|
AUTH_ERROR = 5
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def deploy():
|
||||||
|
"""Deploy command with custom exit codes"""
|
||||||
|
try:
|
||||||
|
# Check configuration
|
||||||
|
if not has_valid_config():
|
||||||
|
click.echo("Configuration error", err=True)
|
||||||
|
sys.exit(ExitCode.CONFIG_ERROR)
|
||||||
|
|
||||||
|
# Check authentication
|
||||||
|
if not is_authenticated():
|
||||||
|
click.echo("Authentication failed", err=True)
|
||||||
|
sys.exit(ExitCode.AUTH_ERROR)
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
deploy_application()
|
||||||
|
click.echo("Deployment successful")
|
||||||
|
sys.exit(ExitCode.SUCCESS)
|
||||||
|
|
||||||
|
except NetworkError:
|
||||||
|
click.echo("Network error", err=True)
|
||||||
|
sys.exit(ExitCode.NETWORK_ERROR)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"Error: {e}", err=True)
|
||||||
|
sys.exit(ExitCode.GENERAL_ERROR)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Custom Exit Codes
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestCustomExitCodes:
|
||||||
|
"""Test custom exit codes"""
|
||||||
|
|
||||||
|
def test_config_error_exit_code(self, runner, tmp_path):
|
||||||
|
"""Configuration error should return 3"""
|
||||||
|
# Remove config file
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code == 3
|
||||||
|
assert 'configuration' in result.output.lower()
|
||||||
|
|
||||||
|
def test_network_error_exit_code(self, runner, monkeypatch):
|
||||||
|
"""Network error should return 4"""
|
||||||
|
def mock_request(*args, **kwargs):
|
||||||
|
raise NetworkError("Connection failed")
|
||||||
|
|
||||||
|
monkeypatch.setattr('requests.post', mock_request)
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code == 4
|
||||||
|
assert 'network' in result.output.lower()
|
||||||
|
|
||||||
|
def test_auth_error_exit_code(self, runner):
|
||||||
|
"""Authentication error should return 5"""
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production', '--token', 'invalid'])
|
||||||
|
assert result.exit_code == 5
|
||||||
|
assert 'authentication' in result.output.lower()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Exit Codes in Scripts
|
||||||
|
|
||||||
|
### Bash Script Exit Code Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Script Exit Codes', () => {
|
||||||
|
test('should respect shell exit codes', () => {
|
||||||
|
// Test that CLI properly exits with script error codes
|
||||||
|
const script = `
|
||||||
|
#!/bin/bash
|
||||||
|
${CLI_PATH} deploy staging
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Deployment failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Deployment succeeded"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { code, stdout } = execSync(script, { encoding: 'utf8' });
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Deployment succeeded');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should propagate errors in pipelines', () => {
|
||||||
|
const { code } = execSync(`${CLI_PATH} invalid | tee output.log`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
expect(code).not.toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Code Best Practices
|
||||||
|
|
||||||
|
### 1. Document Exit Codes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* CLI Exit Codes
|
||||||
|
*
|
||||||
|
* 0 - Success
|
||||||
|
* 1 - General error
|
||||||
|
* 2 - Invalid arguments
|
||||||
|
* 3 - Configuration error
|
||||||
|
* 4 - Network error
|
||||||
|
* 5 - Authentication error
|
||||||
|
* 6 - Resource not found
|
||||||
|
* 7 - Resource already exists
|
||||||
|
* 8 - Permission denied
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Consistent Error Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handle_error(error: Exception) -> int:
|
||||||
|
"""
|
||||||
|
Handle errors and return appropriate exit code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Appropriate exit code for the error type
|
||||||
|
"""
|
||||||
|
if isinstance(error, ConfigurationError):
|
||||||
|
click.echo(f"Configuration error: {error}", err=True)
|
||||||
|
return ExitCode.CONFIG_ERROR
|
||||||
|
elif isinstance(error, NetworkError):
|
||||||
|
click.echo(f"Network error: {error}", err=True)
|
||||||
|
return ExitCode.NETWORK_ERROR
|
||||||
|
elif isinstance(error, AuthenticationError):
|
||||||
|
click.echo(f"Authentication failed: {error}", err=True)
|
||||||
|
return ExitCode.AUTH_ERROR
|
||||||
|
else:
|
||||||
|
click.echo(f"Error: {error}", err=True)
|
||||||
|
return ExitCode.GENERAL_ERROR
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Exit Codes with Error Messages
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test('exit code matches error type', () => {
|
||||||
|
const errorCases = [
|
||||||
|
{ args: 'deploy', expectedCode: 2, expectedMsg: 'missing required argument' },
|
||||||
|
{ args: 'login --token bad', expectedCode: 5, expectedMsg: 'authentication failed' },
|
||||||
|
{ args: 'get missing-id', expectedCode: 6, expectedMsg: 'not found' },
|
||||||
|
];
|
||||||
|
|
||||||
|
errorCases.forEach(({ args, expectedCode, expectedMsg }) => {
|
||||||
|
const { code, stderr } = runCLI(args);
|
||||||
|
expect(code).toBe(expectedCode);
|
||||||
|
expect(stderr.toLowerCase()).toContain(expectedMsg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Help and Version Return 0
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_help_returns_success(runner):
|
||||||
|
"""Help should return 0"""
|
||||||
|
result = runner.invoke(cli, ['--help'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_version_returns_success(runner):
|
||||||
|
"""Version should return 0"""
|
||||||
|
result = runner.invoke(cli, ['--version'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### 1. Don't Use Exit Code 0 for Errors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Wrong - using 0 for errors
|
||||||
|
if (error) {
|
||||||
|
console.error('Error occurred');
|
||||||
|
process.exit(0); // Should be non-zero!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Correct - using non-zero for errors
|
||||||
|
if (error) {
|
||||||
|
console.error('Error occurred');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Don't Ignore Exit Codes in Tests
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ Wrong - not checking exit code
|
||||||
|
def test_deploy(runner):
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert 'deployed' in result.output # What if it failed?
|
||||||
|
|
||||||
|
# ✅ Correct - always check exit code
|
||||||
|
def test_deploy(runner):
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'deployed' in result.output
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Specific Exit Codes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Wrong - using 1 for everything
|
||||||
|
if (configError) process.exit(1);
|
||||||
|
if (networkError) process.exit(1);
|
||||||
|
if (authError) process.exit(1);
|
||||||
|
|
||||||
|
// ✅ Correct - using specific codes
|
||||||
|
if (configError) process.exit(ExitCode.CONFIG_ERROR);
|
||||||
|
if (networkError) process.exit(ExitCode.NETWORK_ERROR);
|
||||||
|
if (authError) process.exit(ExitCode.AUTH_ERROR);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Exit Codes in CI/CD
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions example
|
||||||
|
- name: Test CLI Exit Codes
|
||||||
|
run: |
|
||||||
|
# Should succeed
|
||||||
|
./cli status && echo "Status check passed" || exit 1
|
||||||
|
|
||||||
|
# Should fail
|
||||||
|
./cli invalid-command && exit 1 || echo "Error handling works"
|
||||||
|
|
||||||
|
# Check specific exit code
|
||||||
|
./cli deploy --missing-arg
|
||||||
|
if [ $? -eq 2 ]; then
|
||||||
|
echo "Correct exit code for invalid argument"
|
||||||
|
else
|
||||||
|
echo "Wrong exit code"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Exit Codes on Linux](https://tldp.org/LDP/abs/html/exitcodes.html)
|
||||||
|
- [POSIX Exit Codes](https://pubs.opengroup.org/onlinepubs/9699919799/)
|
||||||
|
- [GNU Exit Codes](https://www.gnu.org/software/libc/manual/html_node/Exit-Status.html)
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
# Integration Testing for CLI Applications
|
||||||
|
|
||||||
|
Complete workflows and integration testing patterns for CLI applications.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Integration tests verify that multiple CLI commands work together correctly, testing complete user workflows rather than individual commands in isolation.
|
||||||
|
|
||||||
|
## Key Differences from Unit Tests
|
||||||
|
|
||||||
|
| Unit Tests | Integration Tests |
|
||||||
|
|------------|-------------------|
|
||||||
|
| Test individual commands | Test command sequences |
|
||||||
|
| Mock external dependencies | May use real dependencies |
|
||||||
|
| Fast execution | Slower execution |
|
||||||
|
| Isolated state | Shared state across commands |
|
||||||
|
|
||||||
|
## Node.js Integration Testing
|
||||||
|
|
||||||
|
### Multi-Command Workflow
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Complete Deployment Workflow', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-integration-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('full deployment workflow', () => {
|
||||||
|
// Step 1: Initialize project
|
||||||
|
let result = runCLI(`init my-project --cwd ${tempDir}`);
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(tempDir, 'my-project'))).toBe(true);
|
||||||
|
|
||||||
|
// Step 2: Configure
|
||||||
|
const projectDir = path.join(tempDir, 'my-project');
|
||||||
|
result = runCLI(`config set api_key test_key --cwd ${projectDir}`);
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
|
||||||
|
// Step 3: Build
|
||||||
|
result = runCLI(`build --production --cwd ${projectDir}`);
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(projectDir, 'dist'))).toBe(true);
|
||||||
|
|
||||||
|
// Step 4: Deploy
|
||||||
|
result = runCLI(`deploy staging --cwd ${projectDir}`);
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
expect(result.stdout).toContain('Deployed successfully');
|
||||||
|
|
||||||
|
// Step 5: Verify
|
||||||
|
result = runCLI(`status --cwd ${projectDir}`);
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
expect(result.stdout).toContain('staging');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Persistence Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('State Persistence', () => {
|
||||||
|
test('state persists across commands', () => {
|
||||||
|
const workspace = createTempWorkspace();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create initial state
|
||||||
|
runCLI(`init --cwd ${workspace}`);
|
||||||
|
runCLI(`config set key1 value1 --cwd ${workspace}`);
|
||||||
|
runCLI(`config set key2 value2 --cwd ${workspace}`);
|
||||||
|
|
||||||
|
// Verify state persists
|
||||||
|
let result = runCLI(`config get key1 --cwd ${workspace}`);
|
||||||
|
expect(result.stdout).toContain('value1');
|
||||||
|
|
||||||
|
// Modify state
|
||||||
|
runCLI(`config set key1 updated --cwd ${workspace}`);
|
||||||
|
|
||||||
|
// Verify modification
|
||||||
|
result = runCLI(`config get key1 --cwd ${workspace}`);
|
||||||
|
expect(result.stdout).toContain('updated');
|
||||||
|
|
||||||
|
// Verify other keys unchanged
|
||||||
|
result = runCLI(`config get key2 --cwd ${workspace}`);
|
||||||
|
expect(result.stdout).toContain('value2');
|
||||||
|
} finally {
|
||||||
|
cleanupWorkspace(workspace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python Integration Testing
|
||||||
|
|
||||||
|
### Complete Workflow Testing
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestCompleteWorkflow:
|
||||||
|
"""Test complete CLI workflows"""
|
||||||
|
|
||||||
|
def test_project_lifecycle(self, runner):
|
||||||
|
"""Test complete project lifecycle"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# Initialize
|
||||||
|
result = runner.invoke(cli, ['create', 'test-project'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Enter project directory
|
||||||
|
os.chdir('test-project')
|
||||||
|
|
||||||
|
# Configure
|
||||||
|
result = runner.invoke(cli, ['config', 'set', 'api_key', 'test_key'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Add dependencies
|
||||||
|
result = runner.invoke(cli, ['add', 'dependency', 'requests'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Build
|
||||||
|
result = runner.invoke(cli, ['build'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert os.path.exists('dist')
|
||||||
|
|
||||||
|
# Test
|
||||||
|
result = runner.invoke(cli, ['test'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
result = runner.invoke(cli, ['deploy', 'staging'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
result = runner.invoke(cli, ['status'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'staging' in result.output
|
||||||
|
|
||||||
|
def test_multi_environment_workflow(self, runner):
|
||||||
|
"""Test workflow across multiple environments"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# Setup
|
||||||
|
runner.invoke(cli, ['init', 'multi-env-app'])
|
||||||
|
os.chdir('multi-env-app')
|
||||||
|
|
||||||
|
# Configure environments
|
||||||
|
environments = ['development', 'staging', 'production']
|
||||||
|
|
||||||
|
for env in environments:
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['config', 'set', 'api_key', f'{env}_key', '--env', env]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Deploy to each environment
|
||||||
|
for env in environments:
|
||||||
|
result = runner.invoke(cli, ['deploy', env])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert env in result.output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Recovery Testing
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestErrorRecovery:
|
||||||
|
"""Test error recovery workflows"""
|
||||||
|
|
||||||
|
def test_rollback_on_failure(self, runner):
|
||||||
|
"""Test rollback after failed deployment"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# Setup
|
||||||
|
runner.invoke(cli, ['init', 'rollback-test'])
|
||||||
|
os.chdir('rollback-test')
|
||||||
|
runner.invoke(cli, ['config', 'set', 'api_key', 'test_key'])
|
||||||
|
|
||||||
|
# Successful deployment
|
||||||
|
result = runner.invoke(cli, ['deploy', 'staging'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Failed deployment (simulate)
|
||||||
|
result = runner.invoke(cli, ['deploy', 'staging', '--force-fail'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
|
||||||
|
# Rollback
|
||||||
|
result = runner.invoke(cli, ['rollback'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'rollback successful' in result.output.lower()
|
||||||
|
|
||||||
|
def test_recovery_from_corruption(self, runner):
|
||||||
|
"""Test recovery from corrupted state"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# Create valid state
|
||||||
|
runner.invoke(cli, ['init', 'corrupt-test'])
|
||||||
|
os.chdir('corrupt-test')
|
||||||
|
runner.invoke(cli, ['config', 'set', 'key', 'value'])
|
||||||
|
|
||||||
|
# Corrupt state file
|
||||||
|
with open('.cli-state', 'w') as f:
|
||||||
|
f.write('invalid json {[}')
|
||||||
|
|
||||||
|
# Should detect and recover
|
||||||
|
result = runner.invoke(cli, ['config', 'get', 'key'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'corrupt' in result.output.lower()
|
||||||
|
|
||||||
|
# Reset state
|
||||||
|
result = runner.invoke(cli, ['reset', '--force'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Should work after reset
|
||||||
|
result = runner.invoke(cli, ['config', 'set', 'key', 'new_value'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Test Patterns
|
||||||
|
|
||||||
|
### 1. Sequential Command Testing
|
||||||
|
|
||||||
|
Test commands that must run in a specific order:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_sequential_workflow(runner):
|
||||||
|
"""Test commands that depend on each other"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# Each command depends on the previous
|
||||||
|
commands = [
|
||||||
|
['init', 'project'],
|
||||||
|
['config', 'set', 'key', 'value'],
|
||||||
|
['build'],
|
||||||
|
['test'],
|
||||||
|
['deploy', 'staging']
|
||||||
|
]
|
||||||
|
|
||||||
|
for cmd in commands:
|
||||||
|
result = runner.invoke(cli, cmd)
|
||||||
|
assert result.exit_code == 0, \
|
||||||
|
f"Command {' '.join(cmd)} failed: {result.output}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Concurrent Operation Testing
|
||||||
|
|
||||||
|
Test that concurrent operations are handled correctly:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_concurrent_operations(runner):
|
||||||
|
"""Test handling of concurrent operations"""
|
||||||
|
import threading
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def run_command():
|
||||||
|
result = runner.invoke(cli, ['deploy', 'staging'])
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
# Start multiple deployments
|
||||||
|
threads = [threading.Thread(target=run_command) for _ in range(3)]
|
||||||
|
for thread in threads:
|
||||||
|
thread.start()
|
||||||
|
for thread in threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
# Only one should succeed, others should detect lock
|
||||||
|
successful = sum(1 for r in results if r.exit_code == 0)
|
||||||
|
assert successful == 1
|
||||||
|
assert any('locked' in r.output.lower() for r in results if r.exit_code != 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Data Migration Testing
|
||||||
|
|
||||||
|
Test data migration between versions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_data_migration(runner):
|
||||||
|
"""Test data migration workflow"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# Create old version data
|
||||||
|
old_data = {'version': 1, 'data': {'key': 'value'}}
|
||||||
|
with open('data.json', 'w') as f:
|
||||||
|
json.dump(old_data, f)
|
||||||
|
|
||||||
|
# Run migration
|
||||||
|
result = runner.invoke(cli, ['migrate', '--to', '2.0'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Verify new format
|
||||||
|
with open('data.json', 'r') as f:
|
||||||
|
new_data = json.load(f)
|
||||||
|
assert new_data['version'] == 2
|
||||||
|
assert new_data['data']['key'] == 'value'
|
||||||
|
|
||||||
|
# Verify backup created
|
||||||
|
assert os.path.exists('data.json.backup')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use Isolated Environments**: Each test should run in a clean environment
|
||||||
|
2. **Test Real Workflows**: Test actual user scenarios, not artificial sequences
|
||||||
|
3. **Include Error Paths**: Test recovery from failures
|
||||||
|
4. **Test State Persistence**: Verify data persists correctly across commands
|
||||||
|
5. **Use Realistic Data**: Test with data similar to production use cases
|
||||||
|
6. **Clean Up Resources**: Always cleanup temp files and resources
|
||||||
|
7. **Document Workflows**: Clearly document what workflow each test verifies
|
||||||
|
8. **Set Appropriate Timeouts**: Integration tests may take longer
|
||||||
|
9. **Mark Slow Tests**: Use test markers for slow-running integration tests
|
||||||
|
10. **Test Concurrency**: Verify handling of simultaneous operations
|
||||||
|
|
||||||
|
## Running Integration Tests
|
||||||
|
|
||||||
|
### Node.js/Jest
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all integration tests
|
||||||
|
npm test -- --testPathPattern=integration
|
||||||
|
|
||||||
|
# Run specific integration test
|
||||||
|
npm test -- integration/deployment.test.ts
|
||||||
|
|
||||||
|
# Run with extended timeout
|
||||||
|
npm test -- --testTimeout=30000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python/pytest
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all integration tests
|
||||||
|
pytest tests/integration
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
pytest tests/integration/test_workflow.py
|
||||||
|
|
||||||
|
# Run marked integration tests
|
||||||
|
pytest -m integration
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
pytest tests/integration -v
|
||||||
|
|
||||||
|
# Skip slow tests
|
||||||
|
pytest -m "not slow"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Integration Testing Best Practices](https://martinfowler.com/bliki/IntegrationTest.html)
|
||||||
|
- [Testing Strategies](https://testing.googleblog.com/)
|
||||||
|
- [CLI Testing Patterns](https://clig.dev/#testing)
|
||||||
277
skills/cli-testing-patterns/examples/jest-advanced/README.md
Normal file
277
skills/cli-testing-patterns/examples/jest-advanced/README.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# Jest Advanced CLI Testing Example
|
||||||
|
|
||||||
|
Advanced testing patterns for CLI applications including mocking, fixtures, and integration tests.
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### 1. Async Command Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
|
async function runCLIAsync(args: string[]): Promise<CLIResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn(CLI_PATH, args, { stdio: 'pipe' });
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
resolve({ stdout, stderr, code: code || 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should handle long-running command', async () => {
|
||||||
|
const result = await runCLIAsync(['deploy', 'production']);
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
}, 30000); // 30 second timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Environment Variable Mocking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('environment configuration', () => {
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use API key from environment', () => {
|
||||||
|
process.env.API_KEY = 'test_key_123';
|
||||||
|
const { stdout, code } = runCLI('status');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Authenticated');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail without API key', () => {
|
||||||
|
delete process.env.API_KEY;
|
||||||
|
const { stderr, code } = runCLI('status');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('API key not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. File System Fixtures
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
describe('config file handling', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-test-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create config file', () => {
|
||||||
|
const configFile = path.join(tempDir, '.config');
|
||||||
|
const result = runCLI(`init --config ${configFile}`);
|
||||||
|
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
expect(fs.existsSync(configFile)).toBe(true);
|
||||||
|
|
||||||
|
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||||||
|
expect(config).toHaveProperty('api_key');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Mocking External APIs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import nock from 'nock';
|
||||||
|
|
||||||
|
describe('API interaction', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
nock.cleanAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fetch deployment status', () => {
|
||||||
|
nock('https://api.example.com')
|
||||||
|
.get('/deployments/123')
|
||||||
|
.reply(200, { status: 'success', environment: 'production' });
|
||||||
|
|
||||||
|
const { stdout, code } = runCLI('status --deployment 123');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('success');
|
||||||
|
expect(stdout).toContain('production');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle API errors', () => {
|
||||||
|
nock('https://api.example.com')
|
||||||
|
.get('/deployments/123')
|
||||||
|
.reply(500, { error: 'Internal Server Error' });
|
||||||
|
|
||||||
|
const { stderr, code } = runCLI('status --deployment 123');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('API error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Test Fixtures
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// test-fixtures.ts
|
||||||
|
export const createTestFixtures = () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-test-'));
|
||||||
|
|
||||||
|
// Create sample project structure
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'src'));
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'package.json'),
|
||||||
|
JSON.stringify({ name: 'test-project', version: '1.0.0' })
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tempDir,
|
||||||
|
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage in tests
|
||||||
|
test('should build project', () => {
|
||||||
|
const fixtures = createTestFixtures();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = runCLI(`build --cwd ${fixtures.tempDir}`);
|
||||||
|
expect(result.code).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(fixtures.tempDir, 'dist'))).toBe(true);
|
||||||
|
} finally {
|
||||||
|
fixtures.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Snapshot Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test('help output matches snapshot', () => {
|
||||||
|
const { stdout } = runCLI('--help');
|
||||||
|
expect(stdout).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('version format matches snapshot', () => {
|
||||||
|
const { stdout } = runCLI('--version');
|
||||||
|
expect(stdout).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Parameterized Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe.each([
|
||||||
|
['development', 'dev.example.com'],
|
||||||
|
['staging', 'staging.example.com'],
|
||||||
|
['production', 'api.example.com'],
|
||||||
|
])('deploy to %s', (environment, expectedUrl) => {
|
||||||
|
test(`should deploy to ${environment}`, () => {
|
||||||
|
const { stdout, code } = runCLI(`deploy ${environment}`);
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain(expectedUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Interactive Command Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Readable, Writable } from 'stream';
|
||||||
|
|
||||||
|
test('should handle interactive prompts', (done) => {
|
||||||
|
const child = spawn(CLI_PATH, ['init'], { stdio: 'pipe' });
|
||||||
|
|
||||||
|
const inputs = ['my-project', 'John Doe', 'john@example.com'];
|
||||||
|
let inputIndex = 0;
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
if (output.includes('?') && inputIndex < inputs.length) {
|
||||||
|
child.stdin?.write(inputs[inputIndex] + '\n');
|
||||||
|
inputIndex++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
expect(code).toBe(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Coverage-Driven Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Ensure all CLI commands are tested
|
||||||
|
describe('CLI command coverage', () => {
|
||||||
|
const commands = ['init', 'build', 'deploy', 'status', 'config'];
|
||||||
|
|
||||||
|
commands.forEach((command) => {
|
||||||
|
test(`${command} command exists`, () => {
|
||||||
|
const { stdout } = runCLI('--help');
|
||||||
|
expect(stdout).toContain(command);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${command} has help text`, () => {
|
||||||
|
const { stdout, code } = runCLI(`${command} --help`);
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Usage:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Performance Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test('command executes within time limit', () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const { code } = runCLI('status');
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(duration).toBeLessThan(2000); // Should complete within 2 seconds
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use Test Fixtures**: Create reusable test data and cleanup functions
|
||||||
|
2. **Mock External Dependencies**: Never make real API calls or database connections
|
||||||
|
3. **Test Edge Cases**: Test boundary conditions, empty inputs, special characters
|
||||||
|
4. **Async Handling**: Use proper async/await or promises for async operations
|
||||||
|
5. **Cleanup**: Always cleanup temp files, reset mocks, restore environment
|
||||||
|
6. **Isolation**: Tests should not depend on execution order
|
||||||
|
7. **Clear Error Messages**: Write assertions with helpful failure messages
|
||||||
|
|
||||||
|
## Common Advanced Patterns
|
||||||
|
|
||||||
|
- Concurrent execution testing
|
||||||
|
- File locking and race conditions
|
||||||
|
- Signal handling (SIGTERM, SIGINT)
|
||||||
|
- Large file processing
|
||||||
|
- Streaming output
|
||||||
|
- Progress indicators
|
||||||
|
- Error recovery and retry logic
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Jest Advanced Features](https://jestjs.io/docs/advanced)
|
||||||
|
- [Mocking with Jest](https://jestjs.io/docs/mock-functions)
|
||||||
|
- [Snapshot Testing](https://jestjs.io/docs/snapshot-testing)
|
||||||
145
skills/cli-testing-patterns/examples/jest-basic/README.md
Normal file
145
skills/cli-testing-patterns/examples/jest-basic/README.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Jest Basic CLI Testing Example
|
||||||
|
|
||||||
|
This example demonstrates basic CLI testing patterns using Jest for Node.js/TypeScript projects.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev jest @types/jest ts-jest @types/node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
describe('CLI Tool Tests', () => {
|
||||||
|
const CLI_PATH = path.join(__dirname, '../bin/mycli');
|
||||||
|
|
||||||
|
function runCLI(args: string) {
|
||||||
|
try {
|
||||||
|
const stdout = execSync(`${CLI_PATH} ${args}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
return { stdout, stderr: '', code: 0 };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
stdout: error.stdout || '',
|
||||||
|
stderr: error.stderr || '',
|
||||||
|
code: error.status || 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should display version', () => {
|
||||||
|
const { stdout, code } = runCLI('--version');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('1.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display help', () => {
|
||||||
|
const { stdout, code } = runCLI('--help');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Usage:');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle unknown command', () => {
|
||||||
|
const { stderr, code } = runCLI('unknown-command');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('unknown command');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# Run in watch mode
|
||||||
|
npm run test:watch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### 1. Command Execution Helper
|
||||||
|
|
||||||
|
Create a reusable `runCLI()` function that:
|
||||||
|
- Executes CLI commands using `execSync`
|
||||||
|
- Captures stdout, stderr, and exit codes
|
||||||
|
- Handles both success and failure cases
|
||||||
|
|
||||||
|
### 2. Exit Code Testing
|
||||||
|
|
||||||
|
Always test exit codes:
|
||||||
|
- `0` for success
|
||||||
|
- Non-zero for errors
|
||||||
|
- Specific codes for different error types
|
||||||
|
|
||||||
|
### 3. Output Validation
|
||||||
|
|
||||||
|
Test output content using Jest matchers:
|
||||||
|
- `.toContain()` for substring matching
|
||||||
|
- `.toMatch()` for regex patterns
|
||||||
|
- `.toBe()` for exact matches
|
||||||
|
|
||||||
|
### 4. Error Handling
|
||||||
|
|
||||||
|
Test error scenarios:
|
||||||
|
- Unknown commands
|
||||||
|
- Invalid options
|
||||||
|
- Missing required arguments
|
||||||
|
- Invalid argument types
|
||||||
|
|
||||||
|
## Example Test Cases
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('deploy command', () => {
|
||||||
|
test('should deploy with valid arguments', () => {
|
||||||
|
const { stdout, code } = runCLI('deploy production --force');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Deploying to production');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail without required arguments', () => {
|
||||||
|
const { stderr, code } = runCLI('deploy');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('missing required argument');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate environment names', () => {
|
||||||
|
const { stderr, code } = runCLI('deploy invalid-env');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('invalid environment');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Isolate Tests**: Each test should be independent
|
||||||
|
2. **Use Descriptive Names**: Test names should describe what they validate
|
||||||
|
3. **Test Both Success and Failure**: Cover happy path and error cases
|
||||||
|
4. **Mock External Dependencies**: Don't make real API calls or file system changes
|
||||||
|
5. **Use Type Safety**: Leverage TypeScript for better test reliability
|
||||||
|
6. **Keep Tests Fast**: Fast tests encourage frequent running
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- ❌ Not testing exit codes
|
||||||
|
- ❌ Only testing success cases
|
||||||
|
- ❌ Hardcoding paths instead of using `path.join()`
|
||||||
|
- ❌ Not handling async operations properly
|
||||||
|
- ❌ Testing implementation details instead of behavior
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Jest Documentation](https://jestjs.io/docs/getting-started)
|
||||||
|
- [Testing CLI Applications](https://jestjs.io/docs/cli)
|
||||||
|
- [TypeScript with Jest](https://jestjs.io/docs/getting-started#using-typescript)
|
||||||
353
skills/cli-testing-patterns/examples/pytest-click/README.md
Normal file
353
skills/cli-testing-patterns/examples/pytest-click/README.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
# Pytest Click Testing Example
|
||||||
|
|
||||||
|
Comprehensive examples for testing Click-based CLI applications using pytest and CliRunner.
|
||||||
|
|
||||||
|
## Basic Setup
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from mycli.cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runner():
|
||||||
|
return CliRunner()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Command Testing
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestBasicCommands:
|
||||||
|
"""Test basic CLI commands"""
|
||||||
|
|
||||||
|
def test_version(self, runner):
|
||||||
|
"""Test version command"""
|
||||||
|
result = runner.invoke(cli, ['--version'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert '1.0.0' in result.output
|
||||||
|
|
||||||
|
def test_help(self, runner):
|
||||||
|
"""Test help command"""
|
||||||
|
result = runner.invoke(cli, ['--help'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Usage:' in result.output
|
||||||
|
|
||||||
|
def test_unknown_command(self, runner):
|
||||||
|
"""Test unknown command handling"""
|
||||||
|
result = runner.invoke(cli, ['unknown'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'no such command' in result.output.lower()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Arguments
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestArgumentParsing:
|
||||||
|
"""Test argument parsing"""
|
||||||
|
|
||||||
|
def test_required_argument(self, runner):
|
||||||
|
"""Test command with required argument"""
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'production' in result.output
|
||||||
|
|
||||||
|
def test_missing_required_argument(self, runner):
|
||||||
|
"""Test missing required argument"""
|
||||||
|
result = runner.invoke(cli, ['deploy'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'missing argument' in result.output.lower()
|
||||||
|
|
||||||
|
def test_optional_argument(self, runner):
|
||||||
|
"""Test optional argument"""
|
||||||
|
result = runner.invoke(cli, ['build', '--output', 'dist'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'dist' in result.output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Options
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestOptionParsing:
|
||||||
|
"""Test option parsing"""
|
||||||
|
|
||||||
|
def test_boolean_flag(self, runner):
|
||||||
|
"""Test boolean flag option"""
|
||||||
|
result = runner.invoke(cli, ['deploy', 'staging', '--force'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'force' in result.output.lower()
|
||||||
|
|
||||||
|
def test_option_with_value(self, runner):
|
||||||
|
"""Test option with value"""
|
||||||
|
result = runner.invoke(cli, ['config', 'set', '--key', 'api_key', '--value', 'test'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_multiple_options(self, runner):
|
||||||
|
"""Test multiple options"""
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['deploy', 'production', '--verbose', '--dry-run', '--timeout', '60']
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Interactive Prompts
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestInteractivePrompts:
|
||||||
|
"""Test interactive prompt handling"""
|
||||||
|
|
||||||
|
def test_simple_prompt(self, runner):
|
||||||
|
"""Test simple text prompt"""
|
||||||
|
result = runner.invoke(cli, ['init'], input='my-project\n')
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'my-project' in result.output
|
||||||
|
|
||||||
|
def test_confirmation_prompt(self, runner):
|
||||||
|
"""Test confirmation prompt (yes)"""
|
||||||
|
result = runner.invoke(cli, ['delete', 'resource-id'], input='y\n')
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'deleted' in result.output.lower()
|
||||||
|
|
||||||
|
def test_confirmation_prompt_no(self, runner):
|
||||||
|
"""Test confirmation prompt (no)"""
|
||||||
|
result = runner.invoke(cli, ['delete', 'resource-id'], input='n\n')
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert 'cancelled' in result.output.lower()
|
||||||
|
|
||||||
|
def test_multiple_prompts(self, runner):
|
||||||
|
"""Test multiple prompts in sequence"""
|
||||||
|
inputs = 'my-project\nJohn Doe\njohn@example.com\n'
|
||||||
|
result = runner.invoke(cli, ['init', '--interactive'], input=inputs)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'my-project' in result.output
|
||||||
|
assert 'John Doe' in result.output
|
||||||
|
|
||||||
|
def test_choice_prompt(self, runner):
|
||||||
|
"""Test choice prompt"""
|
||||||
|
result = runner.invoke(cli, ['deploy'], input='1\n') # Select option 1
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Isolated Filesystem
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestFileOperations:
|
||||||
|
"""Test file operations with isolated filesystem"""
|
||||||
|
|
||||||
|
def test_create_file(self, runner):
|
||||||
|
"""Test file creation"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(cli, ['init', 'test-project'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
import os
|
||||||
|
assert os.path.exists('test-project')
|
||||||
|
|
||||||
|
def test_read_file(self, runner):
|
||||||
|
"""Test reading from file"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# Create test file
|
||||||
|
with open('input.txt', 'w') as f:
|
||||||
|
f.write('test data')
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ['process', '--input', 'input.txt'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_write_file(self, runner):
|
||||||
|
"""Test writing to file"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(cli, ['export', '--output', 'output.txt'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
import os
|
||||||
|
assert os.path.exists('output.txt')
|
||||||
|
with open('output.txt', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
assert len(content) > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Environment Variables
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestEnvironmentVariables:
|
||||||
|
"""Test environment variable handling"""
|
||||||
|
|
||||||
|
def test_with_env_var(self, runner):
|
||||||
|
"""Test command with environment variable"""
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['status'],
|
||||||
|
env={'API_KEY': 'test_key_123'}
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_without_env_var(self, runner):
|
||||||
|
"""Test command without required environment variable"""
|
||||||
|
result = runner.invoke(cli, ['status'])
|
||||||
|
# Assuming API_KEY is required
|
||||||
|
if 'API_KEY' not in result.output:
|
||||||
|
assert result.exit_code != 0
|
||||||
|
|
||||||
|
def test_env_var_override(self, runner, monkeypatch):
|
||||||
|
"""Test environment variable override"""
|
||||||
|
monkeypatch.setenv('API_KEY', 'overridden_key')
|
||||||
|
result = runner.invoke(cli, ['status'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Output Formats
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestOutputFormats:
|
||||||
|
"""Test different output formats"""
|
||||||
|
|
||||||
|
def test_json_output(self, runner):
|
||||||
|
"""Test JSON output format"""
|
||||||
|
result = runner.invoke(cli, ['status', '--format', 'json'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
data = json.loads(result.output)
|
||||||
|
assert isinstance(data, dict)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pytest.fail("Output is not valid JSON")
|
||||||
|
|
||||||
|
def test_yaml_output(self, runner):
|
||||||
|
"""Test YAML output format"""
|
||||||
|
result = runner.invoke(cli, ['status', '--format', 'yaml'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert ':' in result.output
|
||||||
|
|
||||||
|
def test_table_output(self, runner):
|
||||||
|
"""Test table output format"""
|
||||||
|
result = runner.invoke(cli, ['list'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert '│' in result.output or '|' in result.output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Exit Codes
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestExitCodes:
|
||||||
|
"""Test exit codes"""
|
||||||
|
|
||||||
|
def test_success_exit_code(self, runner):
|
||||||
|
"""Test success returns 0"""
|
||||||
|
result = runner.invoke(cli, ['status'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_error_exit_code(self, runner):
|
||||||
|
"""Test error returns non-zero"""
|
||||||
|
result = runner.invoke(cli, ['invalid-command'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
|
||||||
|
def test_validation_error_exit_code(self, runner):
|
||||||
|
"""Test validation error returns 2"""
|
||||||
|
result = runner.invoke(cli, ['deploy', '--invalid-option'])
|
||||||
|
assert result.exit_code == 2 # Click uses 2 for usage errors
|
||||||
|
|
||||||
|
def test_exception_exit_code(self, runner):
|
||||||
|
"""Test uncaught exception returns 1"""
|
||||||
|
result = runner.invoke(cli, ['command-that-throws'])
|
||||||
|
assert result.exit_code == 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Fixtures
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_config(tmp_path):
|
||||||
|
"""Create sample config file"""
|
||||||
|
config_file = tmp_path / '.myclirc'
|
||||||
|
config_file.write_text('''
|
||||||
|
api_key: your_test_key_here
|
||||||
|
environment: development
|
||||||
|
verbose: false
|
||||||
|
''')
|
||||||
|
return config_file
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_api(monkeypatch):
|
||||||
|
"""Mock external API calls"""
|
||||||
|
class MockAPI:
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def get(self, endpoint):
|
||||||
|
self.calls.append(('GET', endpoint))
|
||||||
|
return {'status': 'success'}
|
||||||
|
|
||||||
|
mock = MockAPI()
|
||||||
|
monkeypatch.setattr('mycli.api.client', mock)
|
||||||
|
return mock
|
||||||
|
|
||||||
|
|
||||||
|
class TestWithFixtures:
|
||||||
|
"""Test using fixtures"""
|
||||||
|
|
||||||
|
def test_with_config_file(self, runner, sample_config):
|
||||||
|
"""Test with config file"""
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['status', '--config', str(sample_config)]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_with_mock_api(self, runner, mock_api):
|
||||||
|
"""Test with mocked API"""
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert len(mock_api.calls) > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Error Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TestErrorHandling:
|
||||||
|
"""Test error handling"""
|
||||||
|
|
||||||
|
def test_network_error(self, runner, monkeypatch):
|
||||||
|
"""Test network error handling"""
|
||||||
|
def mock_request(*args, **kwargs):
|
||||||
|
raise ConnectionError("Network unreachable")
|
||||||
|
|
||||||
|
monkeypatch.setattr('requests.get', mock_request)
|
||||||
|
result = runner.invoke(cli, ['status'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'network' in result.output.lower()
|
||||||
|
|
||||||
|
def test_file_not_found(self, runner):
|
||||||
|
"""Test file not found error"""
|
||||||
|
result = runner.invoke(cli, ['process', '--input', 'nonexistent.txt'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'not found' in result.output.lower()
|
||||||
|
|
||||||
|
def test_invalid_json(self, runner):
|
||||||
|
"""Test invalid JSON handling"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
with open('config.json', 'w') as f:
|
||||||
|
f.write('invalid json {[}')
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ['config', 'load', 'config.json'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'invalid' in result.output.lower()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use Fixtures**: Share common setup across tests
|
||||||
|
2. **Isolated Filesystem**: Use `runner.isolated_filesystem()` for file operations
|
||||||
|
3. **Test Exit Codes**: Always check exit codes
|
||||||
|
4. **Clear Test Names**: Use descriptive test method names
|
||||||
|
5. **Test Edge Cases**: Test boundary conditions and error cases
|
||||||
|
6. **Mock External Dependencies**: Don't make real API calls
|
||||||
|
7. **Use Markers**: Mark tests as unit, integration, slow, etc.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Click Testing Documentation](https://click.palletsprojects.com/en/8.1.x/testing/)
|
||||||
|
- [Pytest Documentation](https://docs.pytest.org/)
|
||||||
|
- [CliRunner API](https://click.palletsprojects.com/en/8.1.x/api/#click.testing.CliRunner)
|
||||||
82
skills/cli-testing-patterns/scripts/run-cli-tests.sh
Executable file
82
skills/cli-testing-patterns/scripts/run-cli-tests.sh
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Run CLI Tests
|
||||||
|
#
|
||||||
|
# Detects the project type and runs appropriate tests with coverage
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🧪 Running CLI tests..."
|
||||||
|
|
||||||
|
# Detect project type
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
PROJECT_TYPE="node"
|
||||||
|
elif [ -f "setup.py" ] || [ -f "pyproject.toml" ]; then
|
||||||
|
PROJECT_TYPE="python"
|
||||||
|
else
|
||||||
|
echo "❌ Error: Could not detect project type"
|
||||||
|
echo " Expected package.json (Node.js) or setup.py/pyproject.toml (Python)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run tests based on project type
|
||||||
|
if [ "$PROJECT_TYPE" == "node" ]; then
|
||||||
|
echo "📦 Node.js project detected"
|
||||||
|
|
||||||
|
# Check if npm test is configured
|
||||||
|
if ! grep -q '"test"' package.json 2>/dev/null; then
|
||||||
|
echo "❌ Error: No test script found in package.json"
|
||||||
|
echo " Run setup-jest-testing.sh first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install dependencies if needed
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
echo "📦 Installing dependencies..."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
echo "🧪 Running Jest tests..."
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# Display coverage summary
|
||||||
|
if [ -f "coverage/lcov-report/index.html" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✅ Tests complete!"
|
||||||
|
echo "📊 Coverage report: coverage/lcov-report/index.html"
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [ "$PROJECT_TYPE" == "python" ]; then
|
||||||
|
echo "🐍 Python project detected"
|
||||||
|
|
||||||
|
# Check if pytest is installed
|
||||||
|
if ! command -v pytest &> /dev/null; then
|
||||||
|
echo "❌ Error: pytest is not installed"
|
||||||
|
echo " Run setup-pytest-testing.sh first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create/activate virtual environment if it exists
|
||||||
|
if [ -d "venv" ]; then
|
||||||
|
echo "🔧 Activating virtual environment..."
|
||||||
|
source venv/bin/activate
|
||||||
|
elif [ -d ".venv" ]; then
|
||||||
|
echo "🔧 Activating virtual environment..."
|
||||||
|
source .venv/bin/activate
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
echo "🧪 Running pytest tests..."
|
||||||
|
pytest --cov --cov-report=term-missing --cov-report=html
|
||||||
|
|
||||||
|
# Display coverage summary
|
||||||
|
if [ -d "htmlcov" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✅ Tests complete!"
|
||||||
|
echo "📊 Coverage report: htmlcov/index.html"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 All tests passed!"
|
||||||
235
skills/cli-testing-patterns/scripts/setup-jest-testing.sh
Executable file
235
skills/cli-testing-patterns/scripts/setup-jest-testing.sh
Executable file
@@ -0,0 +1,235 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Setup Jest for CLI Testing (Node.js/TypeScript)
|
||||||
|
#
|
||||||
|
# This script installs and configures Jest for testing CLI applications
|
||||||
|
# Includes TypeScript support, coverage reporting, and CLI testing utilities
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Setting up Jest for CLI testing..."
|
||||||
|
|
||||||
|
# Check if npm is available
|
||||||
|
if ! command -v npm &> /dev/null; then
|
||||||
|
echo "❌ Error: npm is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Jest and related dependencies
|
||||||
|
echo "📦 Installing Jest and dependencies..."
|
||||||
|
npm install --save-dev \
|
||||||
|
jest \
|
||||||
|
@types/jest \
|
||||||
|
ts-jest \
|
||||||
|
@types/node
|
||||||
|
|
||||||
|
# Create Jest configuration
|
||||||
|
echo "⚙️ Creating Jest configuration..."
|
||||||
|
cat > jest.config.js << 'EOF'
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>/tests'],
|
||||||
|
testMatch: [
|
||||||
|
'**/__tests__/**/*.ts',
|
||||||
|
'**/?(*.)+(spec|test).ts'
|
||||||
|
],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.{ts,js}',
|
||||||
|
'!src/**/*.d.ts',
|
||||||
|
'!src/**/*.test.ts',
|
||||||
|
'!src/**/__tests__/**'
|
||||||
|
],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
coverageReporters: ['text', 'lcov', 'html'],
|
||||||
|
coverageThresholds: {
|
||||||
|
global: {
|
||||||
|
branches: 70,
|
||||||
|
functions: 70,
|
||||||
|
lines: 70,
|
||||||
|
statements: 70
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verbose: true,
|
||||||
|
testTimeout: 10000
|
||||||
|
};
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create tests directory structure
|
||||||
|
echo "📁 Creating test directory structure..."
|
||||||
|
mkdir -p tests/{unit,integration,helpers}
|
||||||
|
|
||||||
|
# Create test helper file
|
||||||
|
echo "📝 Creating test helpers..."
|
||||||
|
cat > tests/helpers/cli-helpers.ts << 'EOF'
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export interface CLIResult {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CLI_PATH = path.join(__dirname, '../../bin/cli');
|
||||||
|
|
||||||
|
export function runCLI(args: string): CLIResult {
|
||||||
|
try {
|
||||||
|
const stdout = execSync(`${CLI_PATH} ${args}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
return { stdout, stderr: '', code: 0 };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
stdout: error.stdout || '',
|
||||||
|
stderr: error.stderr || '',
|
||||||
|
code: error.status || 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create sample test file
|
||||||
|
echo "📝 Creating sample test file..."
|
||||||
|
cat > tests/unit/cli.test.ts << 'EOF'
|
||||||
|
import { runCLI } from '../helpers/cli-helpers';
|
||||||
|
|
||||||
|
describe('CLI Tests', () => {
|
||||||
|
test('should display version', () => {
|
||||||
|
const { stdout, code } = runCLI('--version');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toMatch(/\d+\.\d+\.\d+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display help', () => {
|
||||||
|
const { stdout, code } = runCLI('--help');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Usage:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create TypeScript configuration for tests
|
||||||
|
echo "⚙️ Creating TypeScript configuration..."
|
||||||
|
if [ ! -f tsconfig.json ]; then
|
||||||
|
cat > tsconfig.json << 'EOF'
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update package.json scripts
|
||||||
|
echo "⚙️ Updating package.json scripts..."
|
||||||
|
if [ -f package.json ]; then
|
||||||
|
# Check if jq is available for JSON manipulation
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
# Add test scripts using jq
|
||||||
|
tmp=$(mktemp)
|
||||||
|
jq '.scripts.test = "jest" |
|
||||||
|
.scripts["test:watch"] = "jest --watch" |
|
||||||
|
.scripts["test:coverage"] = "jest --coverage" |
|
||||||
|
.scripts["test:ci"] = "jest --ci --coverage --maxWorkers=2"' \
|
||||||
|
package.json > "$tmp"
|
||||||
|
mv "$tmp" package.json
|
||||||
|
else
|
||||||
|
echo "⚠️ jq not found. Please manually add test scripts to package.json:"
|
||||||
|
echo ' "test": "jest"'
|
||||||
|
echo ' "test:watch": "jest --watch"'
|
||||||
|
echo ' "test:coverage": "jest --coverage"'
|
||||||
|
echo ' "test:ci": "jest --ci --coverage --maxWorkers=2"'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create .gitignore entries
|
||||||
|
echo "📝 Updating .gitignore..."
|
||||||
|
if [ -f .gitignore ]; then
|
||||||
|
grep -qxF 'coverage/' .gitignore || echo 'coverage/' >> .gitignore
|
||||||
|
grep -qxF '*.log' .gitignore || echo '*.log' >> .gitignore
|
||||||
|
else
|
||||||
|
cat > .gitignore << 'EOF'
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create README for tests
|
||||||
|
echo "📝 Creating test documentation..."
|
||||||
|
cat > tests/README.md << 'EOF'
|
||||||
|
# CLI Tests
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run tests in watch mode
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# Run tests in CI mode
|
||||||
|
npm run test:ci
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
- `unit/` - Unit tests for individual functions
|
||||||
|
- `integration/` - Integration tests for complete workflows
|
||||||
|
- `helpers/` - Test helper functions and utilities
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
Use the `runCLI` helper to execute CLI commands:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { runCLI } from '../helpers/cli-helpers';
|
||||||
|
|
||||||
|
test('should execute command', () => {
|
||||||
|
const { stdout, stderr, code } = runCLI('command --flag');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('expected output');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
Coverage reports are generated in the `coverage/` directory.
|
||||||
|
Target: 70% coverage for branches, functions, lines, and statements.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ Jest setup complete!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Run 'npm test' to execute tests"
|
||||||
|
echo " 2. Add more tests in tests/unit/ and tests/integration/"
|
||||||
|
echo " 3. Run 'npm run test:coverage' to see coverage report"
|
||||||
|
echo ""
|
||||||
|
echo "📚 Test files created:"
|
||||||
|
echo " - jest.config.js"
|
||||||
|
echo " - tests/helpers/cli-helpers.ts"
|
||||||
|
echo " - tests/unit/cli.test.ts"
|
||||||
|
echo " - tests/README.md"
|
||||||
448
skills/cli-testing-patterns/scripts/setup-pytest-testing.sh
Executable file
448
skills/cli-testing-patterns/scripts/setup-pytest-testing.sh
Executable file
@@ -0,0 +1,448 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Setup pytest for CLI Testing (Python)
|
||||||
|
#
|
||||||
|
# This script installs and configures pytest for testing Click-based CLI applications
|
||||||
|
# Includes coverage reporting, fixtures, and CLI testing utilities
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Setting up pytest for CLI testing..."
|
||||||
|
|
||||||
|
# Check if Python is available
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
echo "❌ Error: python3 is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if pip is available
|
||||||
|
if ! command -v pip3 &> /dev/null; then
|
||||||
|
echo "❌ Error: pip3 is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install pytest and related dependencies
|
||||||
|
echo "📦 Installing pytest and dependencies..."
|
||||||
|
pip3 install --upgrade \
|
||||||
|
pytest \
|
||||||
|
pytest-cov \
|
||||||
|
pytest-mock \
|
||||||
|
click
|
||||||
|
|
||||||
|
# Create pytest configuration
|
||||||
|
echo "⚙️ Creating pytest configuration..."
|
||||||
|
cat > pytest.ini << 'EOF'
|
||||||
|
[pytest]
|
||||||
|
testpaths = tests
|
||||||
|
python_files = test_*.py
|
||||||
|
python_classes = Test*
|
||||||
|
python_functions = test_*
|
||||||
|
addopts =
|
||||||
|
-v
|
||||||
|
--strict-markers
|
||||||
|
--tb=short
|
||||||
|
--cov=src
|
||||||
|
--cov-report=term-missing
|
||||||
|
--cov-report=html
|
||||||
|
--cov-report=xml
|
||||||
|
markers =
|
||||||
|
unit: Unit tests
|
||||||
|
integration: Integration tests
|
||||||
|
slow: Slow running tests
|
||||||
|
cli: CLI command tests
|
||||||
|
filterwarnings =
|
||||||
|
ignore::DeprecationWarning
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create tests directory structure
|
||||||
|
echo "📁 Creating test directory structure..."
|
||||||
|
mkdir -p tests/{unit,integration,fixtures}
|
||||||
|
|
||||||
|
# Create conftest.py with common fixtures
|
||||||
|
echo "📝 Creating pytest fixtures..."
|
||||||
|
cat > tests/conftest.py << 'EOF'
|
||||||
|
"""
|
||||||
|
Pytest configuration and fixtures for CLI testing
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from src.cli import cli # Adjust import based on your CLI module
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runner():
|
||||||
|
"""Create a CliRunner instance for testing"""
|
||||||
|
return CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def isolated_runner():
|
||||||
|
"""Create a CliRunner with isolated filesystem"""
|
||||||
|
runner = CliRunner()
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
yield runner
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_workspace(tmp_path):
|
||||||
|
"""Create a temporary workspace directory"""
|
||||||
|
workspace = tmp_path / 'workspace'
|
||||||
|
workspace.mkdir()
|
||||||
|
yield workspace
|
||||||
|
# Cleanup handled by tmp_path fixture
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config(temp_workspace):
|
||||||
|
"""Create a mock configuration file"""
|
||||||
|
config_file = temp_workspace / '.clirc'
|
||||||
|
config_content = """
|
||||||
|
api_key: your_test_key_here
|
||||||
|
environment: development
|
||||||
|
verbose: false
|
||||||
|
"""
|
||||||
|
config_file.write_text(config_content)
|
||||||
|
return config_file
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cli_harness(runner):
|
||||||
|
"""Create CLI test harness with helper methods"""
|
||||||
|
class CLIHarness:
|
||||||
|
def __init__(self, runner):
|
||||||
|
self.runner = runner
|
||||||
|
|
||||||
|
def run(self, args, input_data=None):
|
||||||
|
"""Run CLI command and return result"""
|
||||||
|
return self.runner.invoke(cli, args, input=input_data)
|
||||||
|
|
||||||
|
def assert_success(self, args, expected_in_output=None):
|
||||||
|
"""Assert command succeeds"""
|
||||||
|
result = self.run(args)
|
||||||
|
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||||
|
if expected_in_output:
|
||||||
|
assert expected_in_output in result.output
|
||||||
|
return result
|
||||||
|
|
||||||
|
def assert_failure(self, args, expected_in_output=None):
|
||||||
|
"""Assert command fails"""
|
||||||
|
result = self.run(args)
|
||||||
|
assert result.exit_code != 0, f"Command should have failed: {result.output}"
|
||||||
|
if expected_in_output:
|
||||||
|
assert expected_in_output in result.output
|
||||||
|
return result
|
||||||
|
|
||||||
|
return CLIHarness(runner)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create __init__.py files
|
||||||
|
touch tests/__init__.py
|
||||||
|
touch tests/unit/__init__.py
|
||||||
|
touch tests/integration/__init__.py
|
||||||
|
touch tests/fixtures/__init__.py
|
||||||
|
|
||||||
|
# Create sample test file
|
||||||
|
echo "📝 Creating sample test file..."
|
||||||
|
cat > tests/unit/test_cli.py << 'EOF'
|
||||||
|
"""
|
||||||
|
Unit tests for CLI commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from src.cli import cli # Adjust import based on your CLI module
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersionCommand:
|
||||||
|
"""Test version command"""
|
||||||
|
|
||||||
|
def test_version_flag(self, runner):
|
||||||
|
"""Should display version with --version"""
|
||||||
|
result = runner.invoke(cli, ['--version'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
# Adjust assertion based on your version format
|
||||||
|
|
||||||
|
def test_version_output_format(self, runner):
|
||||||
|
"""Should display version in correct format"""
|
||||||
|
result = runner.invoke(cli, ['--version'])
|
||||||
|
assert result.output.count('.') >= 2 # X.Y.Z format
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelpCommand:
|
||||||
|
"""Test help command"""
|
||||||
|
|
||||||
|
def test_help_flag(self, runner):
|
||||||
|
"""Should display help with --help"""
|
||||||
|
result = runner.invoke(cli, ['--help'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Usage:' in result.output
|
||||||
|
|
||||||
|
def test_help_shows_commands(self, runner):
|
||||||
|
"""Should list available commands"""
|
||||||
|
result = runner.invoke(cli, ['--help'])
|
||||||
|
assert 'Commands:' in result.output
|
||||||
|
|
||||||
|
|
||||||
|
class TestErrorHandling:
|
||||||
|
"""Test error handling"""
|
||||||
|
|
||||||
|
def test_unknown_command(self, runner):
|
||||||
|
"""Should handle unknown commands gracefully"""
|
||||||
|
result = runner.invoke(cli, ['unknown-command'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'no such command' in result.output.lower()
|
||||||
|
|
||||||
|
def test_invalid_option(self, runner):
|
||||||
|
"""Should handle invalid options"""
|
||||||
|
result = runner.invoke(cli, ['--invalid-option'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create sample integration test
|
||||||
|
echo "📝 Creating sample integration test..."
|
||||||
|
cat > tests/integration/test_workflow.py << 'EOF'
|
||||||
|
"""
|
||||||
|
Integration tests for CLI workflows
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from src.cli import cli # Adjust import based on your CLI module
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
class TestCompleteWorkflow:
|
||||||
|
"""Test complete CLI workflows"""
|
||||||
|
|
||||||
|
def test_init_and_config_workflow(self, isolated_runner):
|
||||||
|
"""Should complete init -> config workflow"""
|
||||||
|
runner = isolated_runner
|
||||||
|
|
||||||
|
# Initialize project
|
||||||
|
result = runner.invoke(cli, ['init', 'test-project'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Configure project
|
||||||
|
result = runner.invoke(cli, ['config', 'set', 'key', 'value'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Verify configuration
|
||||||
|
result = runner.invoke(cli, ['config', 'get', 'key'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'value' in result.output
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create requirements file for testing
|
||||||
|
echo "📝 Creating requirements-test.txt..."
|
||||||
|
cat > requirements-test.txt << 'EOF'
|
||||||
|
pytest>=7.0.0
|
||||||
|
pytest-cov>=4.0.0
|
||||||
|
pytest-mock>=3.10.0
|
||||||
|
click>=8.0.0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create .coveragerc for coverage configuration
|
||||||
|
echo "⚙️ Creating coverage configuration..."
|
||||||
|
cat > .coveragerc << 'EOF'
|
||||||
|
[run]
|
||||||
|
source = src
|
||||||
|
omit =
|
||||||
|
tests/*
|
||||||
|
*/venv/*
|
||||||
|
*/virtualenv/*
|
||||||
|
*/__pycache__/*
|
||||||
|
|
||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
pragma: no cover
|
||||||
|
def __repr__
|
||||||
|
raise AssertionError
|
||||||
|
raise NotImplementedError
|
||||||
|
if __name__ == .__main__.:
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
@abstractmethod
|
||||||
|
|
||||||
|
precision = 2
|
||||||
|
show_missing = True
|
||||||
|
|
||||||
|
[html]
|
||||||
|
directory = htmlcov
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Update .gitignore
|
||||||
|
echo "📝 Updating .gitignore..."
|
||||||
|
if [ -f .gitignore ]; then
|
||||||
|
grep -qxF '__pycache__/' .gitignore || echo '__pycache__/' >> .gitignore
|
||||||
|
grep -qxF '*.pyc' .gitignore || echo '*.pyc' >> .gitignore
|
||||||
|
grep -qxF '.pytest_cache/' .gitignore || echo '.pytest_cache/' >> .gitignore
|
||||||
|
grep -qxF 'htmlcov/' .gitignore || echo 'htmlcov/' >> .gitignore
|
||||||
|
grep -qxF '.coverage' .gitignore || echo '.coverage' >> .gitignore
|
||||||
|
grep -qxF 'coverage.xml' .gitignore || echo 'coverage.xml' >> .gitignore
|
||||||
|
else
|
||||||
|
cat > .gitignore << 'EOF'
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.pytest_cache/
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
coverage.xml
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create Makefile for convenient test commands
|
||||||
|
echo "📝 Creating Makefile..."
|
||||||
|
cat > Makefile << 'EOF'
|
||||||
|
.PHONY: test test-unit test-integration test-cov clean
|
||||||
|
|
||||||
|
test:
|
||||||
|
pytest
|
||||||
|
|
||||||
|
test-unit:
|
||||||
|
pytest tests/unit -v
|
||||||
|
|
||||||
|
test-integration:
|
||||||
|
pytest tests/integration -v
|
||||||
|
|
||||||
|
test-cov:
|
||||||
|
pytest --cov --cov-report=html --cov-report=term
|
||||||
|
|
||||||
|
test-watch:
|
||||||
|
pytest --watch
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf .pytest_cache htmlcov .coverage coverage.xml
|
||||||
|
find . -type d -name __pycache__ -exec rm -rf {} +
|
||||||
|
find . -type f -name "*.pyc" -delete
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create README for tests
|
||||||
|
echo "📝 Creating test documentation..."
|
||||||
|
cat > tests/README.md << 'EOF'
|
||||||
|
# CLI Tests
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run unit tests only
|
||||||
|
pytest tests/unit
|
||||||
|
|
||||||
|
# Run integration tests only
|
||||||
|
pytest tests/integration
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
pytest --cov --cov-report=html
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/unit/test_cli.py
|
||||||
|
|
||||||
|
# Run specific test function
|
||||||
|
pytest tests/unit/test_cli.py::test_version_flag
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
pytest -v
|
||||||
|
|
||||||
|
# Run and show print statements
|
||||||
|
pytest -s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Makefile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Run unit tests
|
||||||
|
make test-unit
|
||||||
|
|
||||||
|
# Run integration tests
|
||||||
|
make test-integration
|
||||||
|
|
||||||
|
# Run with coverage report
|
||||||
|
make test-cov
|
||||||
|
|
||||||
|
# Clean test artifacts
|
||||||
|
make clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
- `unit/` - Unit tests for individual functions and commands
|
||||||
|
- `integration/` - Integration tests for complete workflows
|
||||||
|
- `fixtures/` - Shared test fixtures and utilities
|
||||||
|
- `conftest.py` - Pytest configuration and common fixtures
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
Use the fixtures from `conftest.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_example(runner):
|
||||||
|
"""Test using CliRunner fixture"""
|
||||||
|
result = runner.invoke(cli, ['command', '--flag'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'expected' in result.output
|
||||||
|
|
||||||
|
def test_with_harness(cli_harness):
|
||||||
|
"""Test using CLI harness"""
|
||||||
|
result = cli_harness.assert_success(['command'], 'expected output')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Markers
|
||||||
|
|
||||||
|
Use markers to categorize tests:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_unit_example():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_integration_example():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_slow_operation():
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Run specific markers:
|
||||||
|
```bash
|
||||||
|
pytest -m unit
|
||||||
|
pytest -m "not slow"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
Coverage reports are generated in `htmlcov/` directory.
|
||||||
|
Open `htmlcov/index.html` to view detailed coverage report.
|
||||||
|
|
||||||
|
Target: 80%+ coverage for all modules.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ pytest setup complete!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Run 'pytest' to execute tests"
|
||||||
|
echo " 2. Run 'make test-cov' to see coverage report"
|
||||||
|
echo " 3. Add more tests in tests/unit/ and tests/integration/"
|
||||||
|
echo ""
|
||||||
|
echo "📚 Test files created:"
|
||||||
|
echo " - pytest.ini"
|
||||||
|
echo " - .coveragerc"
|
||||||
|
echo " - tests/conftest.py"
|
||||||
|
echo " - tests/unit/test_cli.py"
|
||||||
|
echo " - tests/integration/test_workflow.py"
|
||||||
|
echo " - tests/README.md"
|
||||||
|
echo " - Makefile"
|
||||||
127
skills/cli-testing-patterns/scripts/validate-test-coverage.sh
Executable file
127
skills/cli-testing-patterns/scripts/validate-test-coverage.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Validate Test Coverage
|
||||||
|
#
|
||||||
|
# Checks that test coverage meets minimum thresholds
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Default thresholds
|
||||||
|
MIN_COVERAGE=${MIN_COVERAGE:-70}
|
||||||
|
|
||||||
|
echo "📊 Validating test coverage..."
|
||||||
|
|
||||||
|
# Detect project type
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
PROJECT_TYPE="node"
|
||||||
|
elif [ -f "setup.py" ] || [ -f "pyproject.toml" ]; then
|
||||||
|
PROJECT_TYPE="python"
|
||||||
|
else
|
||||||
|
echo "❌ Error: Could not detect project type"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check coverage for Node.js projects
|
||||||
|
if [ "$PROJECT_TYPE" == "node" ]; then
|
||||||
|
echo "📦 Node.js project detected"
|
||||||
|
|
||||||
|
# Check if coverage data exists
|
||||||
|
if [ ! -d "coverage" ]; then
|
||||||
|
echo "❌ Error: No coverage data found"
|
||||||
|
echo " Run 'npm run test:coverage' first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if coverage summary exists
|
||||||
|
if [ ! -f "coverage/coverage-summary.json" ]; then
|
||||||
|
echo "❌ Error: coverage-summary.json not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract coverage percentages using jq if available
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
LINES=$(jq '.total.lines.pct' coverage/coverage-summary.json)
|
||||||
|
STATEMENTS=$(jq '.total.statements.pct' coverage/coverage-summary.json)
|
||||||
|
FUNCTIONS=$(jq '.total.functions.pct' coverage/coverage-summary.json)
|
||||||
|
BRANCHES=$(jq '.total.branches.pct' coverage/coverage-summary.json)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Coverage Summary:"
|
||||||
|
echo " Lines: ${LINES}%"
|
||||||
|
echo " Statements: ${STATEMENTS}%"
|
||||||
|
echo " Functions: ${FUNCTIONS}%"
|
||||||
|
echo " Branches: ${BRANCHES}%"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check thresholds
|
||||||
|
FAILED=0
|
||||||
|
if (( $(echo "$LINES < $MIN_COVERAGE" | bc -l) )); then
|
||||||
|
echo "❌ Lines coverage (${LINES}%) below threshold (${MIN_COVERAGE}%)"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
if (( $(echo "$STATEMENTS < $MIN_COVERAGE" | bc -l) )); then
|
||||||
|
echo "❌ Statements coverage (${STATEMENTS}%) below threshold (${MIN_COVERAGE}%)"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
if (( $(echo "$FUNCTIONS < $MIN_COVERAGE" | bc -l) )); then
|
||||||
|
echo "❌ Functions coverage (${FUNCTIONS}%) below threshold (${MIN_COVERAGE}%)"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
if (( $(echo "$BRANCHES < $MIN_COVERAGE" | bc -l) )); then
|
||||||
|
echo "❌ Branches coverage (${BRANCHES}%) below threshold (${MIN_COVERAGE}%)"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $FAILED -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ Coverage validation failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Coverage thresholds met!"
|
||||||
|
else
|
||||||
|
echo "⚠️ jq not installed, skipping detailed validation"
|
||||||
|
echo " Install jq for detailed coverage validation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check coverage for Python projects
|
||||||
|
elif [ "$PROJECT_TYPE" == "python" ]; then
|
||||||
|
echo "🐍 Python project detected"
|
||||||
|
|
||||||
|
# Check if coverage data exists
|
||||||
|
if [ ! -f ".coverage" ]; then
|
||||||
|
echo "❌ Error: No coverage data found"
|
||||||
|
echo " Run 'pytest --cov' first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate coverage report
|
||||||
|
if command -v coverage &> /dev/null; then
|
||||||
|
echo ""
|
||||||
|
coverage report
|
||||||
|
|
||||||
|
# Get total coverage percentage
|
||||||
|
TOTAL_COVERAGE=$(coverage report | tail -1 | awk '{print $NF}' | sed 's/%//')
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Total Coverage: ${TOTAL_COVERAGE}%"
|
||||||
|
echo "Minimum Required: ${MIN_COVERAGE}%"
|
||||||
|
|
||||||
|
# Compare coverage
|
||||||
|
if (( $(echo "$TOTAL_COVERAGE < $MIN_COVERAGE" | bc -l) )); then
|
||||||
|
echo ""
|
||||||
|
echo "❌ Coverage (${TOTAL_COVERAGE}%) below threshold (${MIN_COVERAGE}%)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Coverage thresholds met!"
|
||||||
|
else
|
||||||
|
echo "❌ Error: coverage tool not installed"
|
||||||
|
echo " Install with: pip install coverage"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Coverage validation passed!"
|
||||||
175
skills/cli-testing-patterns/templates/jest-cli-test.ts
Normal file
175
skills/cli-testing-patterns/templates/jest-cli-test.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* Jest CLI Test Template
|
||||||
|
*
|
||||||
|
* Complete test suite for CLI tools using Jest and child_process.execSync
|
||||||
|
* Tests command execution, exit codes, stdout/stderr output
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
describe('CLI Tool Tests', () => {
|
||||||
|
const CLI_PATH = path.join(__dirname, '../bin/mycli');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to execute CLI commands and capture output
|
||||||
|
* @param args - Command line arguments as string
|
||||||
|
* @returns Object with stdout, stderr, and exit code
|
||||||
|
*/
|
||||||
|
function runCLI(args: string): {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
code: number;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
const stdout = execSync(`${CLI_PATH} ${args}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
return { stdout, stderr: '', code: 0 };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
stdout: error.stdout || '',
|
||||||
|
stderr: error.stderr || '',
|
||||||
|
code: error.status || 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version Testing
|
||||||
|
describe('version command', () => {
|
||||||
|
test('should display version with --version', () => {
|
||||||
|
const { stdout, code } = runCLI('--version');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('1.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display version with -v', () => {
|
||||||
|
const { stdout, code } = runCLI('-v');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toMatch(/\d+\.\d+\.\d+/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Help Testing
|
||||||
|
describe('help command', () => {
|
||||||
|
test('should display help with --help', () => {
|
||||||
|
const { stdout, code } = runCLI('--help');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Usage:');
|
||||||
|
expect(stdout).toContain('Commands:');
|
||||||
|
expect(stdout).toContain('Options:');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display help with -h', () => {
|
||||||
|
const { stdout, code } = runCLI('-h');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Usage:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error Handling
|
||||||
|
describe('error handling', () => {
|
||||||
|
test('should handle unknown command', () => {
|
||||||
|
const { stderr, code } = runCLI('unknown-command');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('unknown command');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle invalid options', () => {
|
||||||
|
const { stderr, code } = runCLI('--invalid-option');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('unknown option');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate required arguments', () => {
|
||||||
|
const { stderr, code } = runCLI('deploy');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('missing required argument');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Command Execution
|
||||||
|
describe('command execution', () => {
|
||||||
|
test('should execute deploy command', () => {
|
||||||
|
const { stdout, code } = runCLI('deploy production --force');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Deploying to production');
|
||||||
|
expect(stdout).toContain('Force mode enabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should execute with flags', () => {
|
||||||
|
const { stdout, code } = runCLI('build --verbose --output dist');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Building project');
|
||||||
|
expect(stdout).toContain('Output: dist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuration Testing
|
||||||
|
describe('configuration', () => {
|
||||||
|
test('should set configuration value', () => {
|
||||||
|
const { stdout, code } = runCLI('config set key value');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Configuration updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get configuration value', () => {
|
||||||
|
runCLI('config set api_key your_key_here');
|
||||||
|
const { stdout, code } = runCLI('config get api_key');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('your_key_here');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should list all configuration', () => {
|
||||||
|
const { stdout, code } = runCLI('config list');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('Configuration:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exit Code Validation
|
||||||
|
describe('exit codes', () => {
|
||||||
|
test('should return 0 on success', () => {
|
||||||
|
const { code } = runCLI('status');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 1 on general error', () => {
|
||||||
|
const { code } = runCLI('invalid-command');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 2 on invalid arguments', () => {
|
||||||
|
const { code } = runCLI('deploy --invalid-flag');
|
||||||
|
expect(code).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Output Format Testing
|
||||||
|
describe('output formatting', () => {
|
||||||
|
test('should output JSON when requested', () => {
|
||||||
|
const { stdout, code } = runCLI('status --format json');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(() => JSON.parse(stdout)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should output YAML when requested', () => {
|
||||||
|
const { stdout, code } = runCLI('status --format yaml');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain(':');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should output table by default', () => {
|
||||||
|
const { stdout, code } = runCLI('status');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toMatch(/[─┼│]/); // Table characters
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
afterAll(() => {
|
||||||
|
// Clean up any test artifacts
|
||||||
|
});
|
||||||
|
});
|
||||||
198
skills/cli-testing-patterns/templates/jest-config-test.ts
Normal file
198
skills/cli-testing-patterns/templates/jest-config-test.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* Jest Configuration Testing Template
|
||||||
|
*
|
||||||
|
* Test CLI configuration file handling, validation, and persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
describe('CLI Configuration Tests', () => {
|
||||||
|
const CLI_PATH = path.join(__dirname, '../bin/mycli');
|
||||||
|
const TEST_CONFIG_DIR = path.join(os.tmpdir(), 'cli-test-config');
|
||||||
|
const TEST_CONFIG_FILE = path.join(TEST_CONFIG_DIR, '.myclirc');
|
||||||
|
|
||||||
|
function runCLI(args: string, env: Record<string, string> = {}): {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
code: number;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
const stdout = execSync(`${CLI_PATH} ${args}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
HOME: TEST_CONFIG_DIR,
|
||||||
|
...env,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { stdout, stderr: '', code: 0 };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
stdout: error.stdout || '',
|
||||||
|
stderr: error.stderr || '',
|
||||||
|
code: error.status || 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create temporary config directory
|
||||||
|
if (!fs.existsSync(TEST_CONFIG_DIR)) {
|
||||||
|
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up test config directory
|
||||||
|
if (fs.existsSync(TEST_CONFIG_DIR)) {
|
||||||
|
fs.rmSync(TEST_CONFIG_DIR, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('config initialization', () => {
|
||||||
|
test('should create config file on first run', () => {
|
||||||
|
runCLI('config init');
|
||||||
|
expect(fs.existsSync(TEST_CONFIG_FILE)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not overwrite existing config', () => {
|
||||||
|
fs.writeFileSync(TEST_CONFIG_FILE, 'existing: data\n');
|
||||||
|
const { stderr, code } = runCLI('config init');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('Config file already exists');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create config with default values', () => {
|
||||||
|
runCLI('config init');
|
||||||
|
const config = fs.readFileSync(TEST_CONFIG_FILE, 'utf8');
|
||||||
|
expect(config).toContain('api_key: your_api_key_here');
|
||||||
|
expect(config).toContain('environment: development');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('config set operations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
runCLI('config init');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set string value', () => {
|
||||||
|
const { code } = runCLI('config set api_key test_key_123');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
|
||||||
|
const config = fs.readFileSync(TEST_CONFIG_FILE, 'utf8');
|
||||||
|
expect(config).toContain('api_key: test_key_123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set boolean value', () => {
|
||||||
|
const { code } = runCLI('config set verbose true');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
|
||||||
|
const config = fs.readFileSync(TEST_CONFIG_FILE, 'utf8');
|
||||||
|
expect(config).toContain('verbose: true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set nested value', () => {
|
||||||
|
const { code } = runCLI('config set logging.level debug');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
|
||||||
|
const config = fs.readFileSync(TEST_CONFIG_FILE, 'utf8');
|
||||||
|
expect(config).toContain('level: debug');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle invalid key names', () => {
|
||||||
|
const { stderr, code } = runCLI('config set invalid..key value');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('Invalid key name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('config get operations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
runCLI('config init');
|
||||||
|
runCLI('config set api_key test_key_123');
|
||||||
|
runCLI('config set environment production');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get existing value', () => {
|
||||||
|
const { stdout, code } = runCLI('config get api_key');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('test_key_123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle non-existent key', () => {
|
||||||
|
const { stderr, code } = runCLI('config get nonexistent');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('Key not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get nested value', () => {
|
||||||
|
runCLI('config set database.host localhost');
|
||||||
|
const { stdout, code } = runCLI('config get database.host');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('localhost');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('config list operations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
runCLI('config init');
|
||||||
|
runCLI('config set api_key test_key_123');
|
||||||
|
runCLI('config set verbose true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should list all configuration', () => {
|
||||||
|
const { stdout, code } = runCLI('config list');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
expect(stdout).toContain('api_key');
|
||||||
|
expect(stdout).toContain('verbose');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should format list output', () => {
|
||||||
|
const { stdout, code } = runCLI('config list --format json');
|
||||||
|
expect(code).toBe(0);
|
||||||
|
const config = JSON.parse(stdout);
|
||||||
|
expect(config.api_key).toBe('test_key_123');
|
||||||
|
expect(config.verbose).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('config validation', () => {
|
||||||
|
test('should validate config file on load', () => {
|
||||||
|
fs.writeFileSync(TEST_CONFIG_FILE, 'invalid yaml: [}');
|
||||||
|
const { stderr, code } = runCLI('config list');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('Invalid configuration file');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate required fields', () => {
|
||||||
|
runCLI('config init');
|
||||||
|
fs.writeFileSync(TEST_CONFIG_FILE, 'optional: value\n');
|
||||||
|
const { stderr, code } = runCLI('deploy production');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('api_key is required');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('environment variable overrides', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
runCLI('config init');
|
||||||
|
runCLI('config set api_key file_key_123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should override with environment variable', () => {
|
||||||
|
const { stdout } = runCLI('config get api_key', {
|
||||||
|
MYCLI_API_KEY: 'env_key_123',
|
||||||
|
});
|
||||||
|
expect(stdout).toContain('env_key_123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use file value when env var not set', () => {
|
||||||
|
const { stdout } = runCLI('config get api_key');
|
||||||
|
expect(stdout).toContain('file_key_123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
223
skills/cli-testing-patterns/templates/jest-integration-test.ts
Normal file
223
skills/cli-testing-patterns/templates/jest-integration-test.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* Jest Integration Test Template
|
||||||
|
*
|
||||||
|
* Test complete CLI workflows with multiple commands and state persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
describe('CLI Integration Tests', () => {
|
||||||
|
const CLI_PATH = path.join(__dirname, '../bin/mycli');
|
||||||
|
const TEST_WORKSPACE = path.join(os.tmpdir(), 'cli-integration-test');
|
||||||
|
|
||||||
|
function runCLI(args: string, cwd: string = TEST_WORKSPACE): {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
code: number;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
const stdout = execSync(`${CLI_PATH} ${args}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
cwd,
|
||||||
|
});
|
||||||
|
return { stdout, stderr: '', code: 0 };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
stdout: error.stdout || '',
|
||||||
|
stderr: error.stderr || '',
|
||||||
|
code: error.status || 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create clean test workspace
|
||||||
|
if (fs.existsSync(TEST_WORKSPACE)) {
|
||||||
|
fs.rmSync(TEST_WORKSPACE, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(TEST_WORKSPACE, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up test workspace
|
||||||
|
if (fs.existsSync(TEST_WORKSPACE)) {
|
||||||
|
fs.rmSync(TEST_WORKSPACE, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('complete deployment workflow', () => {
|
||||||
|
test('should initialize, configure, and deploy', () => {
|
||||||
|
// Step 1: Initialize project
|
||||||
|
const init = runCLI('init my-project');
|
||||||
|
expect(init.code).toBe(0);
|
||||||
|
expect(init.stdout).toContain('Project initialized');
|
||||||
|
|
||||||
|
// Step 2: Configure deployment
|
||||||
|
const config = runCLI('config set api_key test_key_123');
|
||||||
|
expect(config.code).toBe(0);
|
||||||
|
|
||||||
|
// Step 3: Build project
|
||||||
|
const build = runCLI('build --production');
|
||||||
|
expect(build.code).toBe(0);
|
||||||
|
expect(build.stdout).toContain('Build successful');
|
||||||
|
|
||||||
|
// Step 4: Deploy
|
||||||
|
const deploy = runCLI('deploy production');
|
||||||
|
expect(deploy.code).toBe(0);
|
||||||
|
expect(deploy.stdout).toContain('Deployed successfully');
|
||||||
|
|
||||||
|
// Verify deployment artifacts
|
||||||
|
const deployFile = path.join(TEST_WORKSPACE, '.deploy');
|
||||||
|
expect(fs.existsSync(deployFile)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail deployment without configuration', () => {
|
||||||
|
runCLI('init my-project');
|
||||||
|
|
||||||
|
// Try to deploy without configuring API key
|
||||||
|
const { stderr, code } = runCLI('deploy production');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('API key not configured');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multi-environment workflow', () => {
|
||||||
|
test('should manage multiple environments', () => {
|
||||||
|
// Initialize project
|
||||||
|
runCLI('init my-project');
|
||||||
|
|
||||||
|
// Configure development environment
|
||||||
|
runCLI('config set api_key dev_key_123 --env development');
|
||||||
|
runCLI('config set base_url https://dev.example.com --env development');
|
||||||
|
|
||||||
|
// Configure production environment
|
||||||
|
runCLI('config set api_key prod_key_123 --env production');
|
||||||
|
runCLI('config set base_url https://api.example.com --env production');
|
||||||
|
|
||||||
|
// Deploy to development
|
||||||
|
const devDeploy = runCLI('deploy development');
|
||||||
|
expect(devDeploy.code).toBe(0);
|
||||||
|
expect(devDeploy.stdout).toContain('dev.example.com');
|
||||||
|
|
||||||
|
// Deploy to production
|
||||||
|
const prodDeploy = runCLI('deploy production');
|
||||||
|
expect(prodDeploy.code).toBe(0);
|
||||||
|
expect(prodDeploy.stdout).toContain('api.example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('state persistence workflow', () => {
|
||||||
|
test('should persist and restore state', () => {
|
||||||
|
// Create initial state
|
||||||
|
runCLI('state set counter 0');
|
||||||
|
|
||||||
|
// Increment counter multiple times
|
||||||
|
runCLI('increment');
|
||||||
|
runCLI('increment');
|
||||||
|
runCLI('increment');
|
||||||
|
|
||||||
|
// Verify final state
|
||||||
|
const { stdout } = runCLI('state get counter');
|
||||||
|
expect(stdout).toContain('3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle state file corruption', () => {
|
||||||
|
runCLI('state set key value');
|
||||||
|
|
||||||
|
// Corrupt state file
|
||||||
|
const stateFile = path.join(TEST_WORKSPACE, '.state');
|
||||||
|
fs.writeFileSync(stateFile, 'invalid json {[}');
|
||||||
|
|
||||||
|
// Should recover gracefully
|
||||||
|
const { stderr, code } = runCLI('state get key');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('Corrupted state file');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('plugin workflow', () => {
|
||||||
|
test('should install and use plugins', () => {
|
||||||
|
// Initialize project
|
||||||
|
runCLI('init my-project');
|
||||||
|
|
||||||
|
// Install plugin
|
||||||
|
const install = runCLI('plugin install my-plugin');
|
||||||
|
expect(install.code).toBe(0);
|
||||||
|
|
||||||
|
// Verify plugin is listed
|
||||||
|
const list = runCLI('plugin list');
|
||||||
|
expect(list.stdout).toContain('my-plugin');
|
||||||
|
|
||||||
|
// Use plugin command
|
||||||
|
const usePlugin = runCLI('my-plugin:command');
|
||||||
|
expect(usePlugin.code).toBe(0);
|
||||||
|
|
||||||
|
// Uninstall plugin
|
||||||
|
const uninstall = runCLI('plugin uninstall my-plugin');
|
||||||
|
expect(uninstall.code).toBe(0);
|
||||||
|
|
||||||
|
// Verify plugin is removed
|
||||||
|
const listAfter = runCLI('plugin list');
|
||||||
|
expect(listAfter.stdout).not.toContain('my-plugin');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error recovery workflow', () => {
|
||||||
|
test('should recover from partial failure', () => {
|
||||||
|
runCLI('init my-project');
|
||||||
|
|
||||||
|
// Simulate partial deployment failure
|
||||||
|
runCLI('deploy staging --force');
|
||||||
|
|
||||||
|
// Should be able to rollback
|
||||||
|
const rollback = runCLI('rollback');
|
||||||
|
expect(rollback.code).toBe(0);
|
||||||
|
expect(rollback.stdout).toContain('Rollback successful');
|
||||||
|
|
||||||
|
// Should be able to retry
|
||||||
|
const retry = runCLI('deploy staging --retry');
|
||||||
|
expect(retry.code).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('concurrent operations', () => {
|
||||||
|
test('should handle file locking', async () => {
|
||||||
|
runCLI('init my-project');
|
||||||
|
|
||||||
|
// Start long-running operation
|
||||||
|
const longOp = execSync(`${CLI_PATH} long-running-task &`, {
|
||||||
|
cwd: TEST_WORKSPACE,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to run another operation that needs lock
|
||||||
|
const { stderr, code } = runCLI('another-task');
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain('Another operation in progress');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data migration workflow', () => {
|
||||||
|
test('should migrate data between versions', () => {
|
||||||
|
// Create old version data
|
||||||
|
const oldData = { version: 1, data: 'legacy format' };
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(TEST_WORKSPACE, 'data.json'),
|
||||||
|
JSON.stringify(oldData)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
const migrate = runCLI('migrate --to 2.0');
|
||||||
|
expect(migrate.code).toBe(0);
|
||||||
|
|
||||||
|
// Verify new format
|
||||||
|
const newData = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(TEST_WORKSPACE, 'data.json'), 'utf8')
|
||||||
|
);
|
||||||
|
expect(newData.version).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
270
skills/cli-testing-patterns/templates/pytest-click-test.py
Normal file
270
skills/cli-testing-patterns/templates/pytest-click-test.py
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
Pytest Click Testing Template
|
||||||
|
|
||||||
|
Complete test suite for Click-based CLI applications using CliRunner
|
||||||
|
Tests command execution, exit codes, output validation, and interactive prompts
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from mycli.cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runner():
|
||||||
|
"""Create a CliRunner instance for testing"""
|
||||||
|
return CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersionCommand:
|
||||||
|
"""Test version display"""
|
||||||
|
|
||||||
|
def test_version_flag(self, runner):
|
||||||
|
"""Should display version with --version"""
|
||||||
|
result = runner.invoke(cli, ['--version'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert '1.0.0' in result.output
|
||||||
|
|
||||||
|
def test_version_short_flag(self, runner):
|
||||||
|
"""Should display version with -v"""
|
||||||
|
result = runner.invoke(cli, ['-v'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.count('.') == 2 # Version format X.Y.Z
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelpCommand:
|
||||||
|
"""Test help display"""
|
||||||
|
|
||||||
|
def test_help_flag(self, runner):
|
||||||
|
"""Should display help with --help"""
|
||||||
|
result = runner.invoke(cli, ['--help'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Usage:' in result.output
|
||||||
|
assert 'Commands:' in result.output
|
||||||
|
assert 'Options:' in result.output
|
||||||
|
|
||||||
|
def test_help_short_flag(self, runner):
|
||||||
|
"""Should display help with -h"""
|
||||||
|
result = runner.invoke(cli, ['-h'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Usage:' in result.output
|
||||||
|
|
||||||
|
def test_command_help(self, runner):
|
||||||
|
"""Should display help for specific command"""
|
||||||
|
result = runner.invoke(cli, ['deploy', '--help'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'deploy' in result.output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class TestErrorHandling:
|
||||||
|
"""Test error handling and validation"""
|
||||||
|
|
||||||
|
def test_unknown_command(self, runner):
|
||||||
|
"""Should handle unknown commands"""
|
||||||
|
result = runner.invoke(cli, ['unknown-command'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'no such command' in result.output.lower()
|
||||||
|
|
||||||
|
def test_invalid_option(self, runner):
|
||||||
|
"""Should handle invalid options"""
|
||||||
|
result = runner.invoke(cli, ['--invalid-option'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'no such option' in result.output.lower()
|
||||||
|
|
||||||
|
def test_missing_required_argument(self, runner):
|
||||||
|
"""Should validate required arguments"""
|
||||||
|
result = runner.invoke(cli, ['deploy'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'missing argument' in result.output.lower()
|
||||||
|
|
||||||
|
def test_invalid_argument_type(self, runner):
|
||||||
|
"""Should validate argument types"""
|
||||||
|
result = runner.invoke(cli, ['retry', '--count', 'invalid'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'invalid' in result.output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommandExecution:
|
||||||
|
"""Test command execution with various arguments"""
|
||||||
|
|
||||||
|
def test_deploy_command(self, runner):
|
||||||
|
"""Should execute deploy command"""
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production', '--force'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Deploying to production' in result.output
|
||||||
|
assert 'Force mode enabled' in result.output
|
||||||
|
|
||||||
|
def test_deploy_with_flags(self, runner):
|
||||||
|
"""Should handle multiple flags"""
|
||||||
|
result = runner.invoke(cli, ['deploy', 'staging', '--verbose', '--dry-run'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'staging' in result.output
|
||||||
|
assert 'dry run' in result.output.lower()
|
||||||
|
|
||||||
|
def test_build_command(self, runner):
|
||||||
|
"""Should execute build command"""
|
||||||
|
result = runner.invoke(cli, ['build', '--output', 'dist'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Building project' in result.output
|
||||||
|
assert 'dist' in result.output
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfiguration:
|
||||||
|
"""Test configuration management"""
|
||||||
|
|
||||||
|
def test_config_set(self, runner):
|
||||||
|
"""Should set configuration value"""
|
||||||
|
result = runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Configuration updated' in result.output
|
||||||
|
|
||||||
|
def test_config_get(self, runner):
|
||||||
|
"""Should get configuration value"""
|
||||||
|
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
||||||
|
result = runner.invoke(cli, ['config', 'get', 'api_key'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'your_key_here' in result.output
|
||||||
|
|
||||||
|
def test_config_list(self, runner):
|
||||||
|
"""Should list all configuration"""
|
||||||
|
result = runner.invoke(cli, ['config', 'list'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Configuration:' in result.output
|
||||||
|
|
||||||
|
def test_config_delete(self, runner):
|
||||||
|
"""Should delete configuration value"""
|
||||||
|
runner.invoke(cli, ['config', 'set', 'temp_key', 'temp_value'])
|
||||||
|
result = runner.invoke(cli, ['config', 'delete', 'temp_key'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'deleted' in result.output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class TestExitCodes:
|
||||||
|
"""Test exit code validation"""
|
||||||
|
|
||||||
|
def test_success_exit_code(self, runner):
|
||||||
|
"""Should return 0 on success"""
|
||||||
|
result = runner.invoke(cli, ['status'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_error_exit_code(self, runner):
|
||||||
|
"""Should return non-zero on error"""
|
||||||
|
result = runner.invoke(cli, ['invalid-command'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
|
||||||
|
def test_validation_error_exit_code(self, runner):
|
||||||
|
"""Should return specific code for validation errors"""
|
||||||
|
result = runner.invoke(cli, ['deploy', '--invalid-flag'])
|
||||||
|
assert result.exit_code == 2 # Click uses 2 for usage errors
|
||||||
|
|
||||||
|
|
||||||
|
class TestInteractivePrompts:
|
||||||
|
"""Test interactive prompt handling"""
|
||||||
|
|
||||||
|
def test_interactive_deploy_wizard(self, runner):
|
||||||
|
"""Should handle interactive prompts"""
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['deploy-wizard'],
|
||||||
|
input='my-app\n1\nyes\n'
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'my-app' in result.output
|
||||||
|
|
||||||
|
def test_confirmation_prompt(self, runner):
|
||||||
|
"""Should handle confirmation prompts"""
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['delete', 'resource-id'],
|
||||||
|
input='y\n'
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'deleted' in result.output.lower()
|
||||||
|
|
||||||
|
def test_confirmation_prompt_denied(self, runner):
|
||||||
|
"""Should handle denied confirmation"""
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['delete', 'resource-id'],
|
||||||
|
input='n\n'
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert 'cancelled' in result.output.lower()
|
||||||
|
|
||||||
|
def test_multiple_prompts(self, runner):
|
||||||
|
"""Should handle multiple prompts in sequence"""
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['init'],
|
||||||
|
input='my-project\nJohn Doe\njohn@example.com\n'
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'my-project' in result.output
|
||||||
|
assert 'John Doe' in result.output
|
||||||
|
|
||||||
|
|
||||||
|
class TestOutputFormatting:
|
||||||
|
"""Test output formatting options"""
|
||||||
|
|
||||||
|
def test_json_output(self, runner):
|
||||||
|
"""Should output JSON format"""
|
||||||
|
result = runner.invoke(cli, ['status', '--format', 'json'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
json.loads(result.output)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pytest.fail("Output is not valid JSON")
|
||||||
|
|
||||||
|
def test_yaml_output(self, runner):
|
||||||
|
"""Should output YAML format"""
|
||||||
|
result = runner.invoke(cli, ['status', '--format', 'yaml'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert ':' in result.output
|
||||||
|
|
||||||
|
def test_table_output(self, runner):
|
||||||
|
"""Should output table format by default"""
|
||||||
|
result = runner.invoke(cli, ['list'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert '│' in result.output or '|' in result.output
|
||||||
|
|
||||||
|
def test_quiet_mode(self, runner):
|
||||||
|
"""Should suppress output in quiet mode"""
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production', '--quiet'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert len(result.output.strip()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileOperations:
|
||||||
|
"""Test file-based operations"""
|
||||||
|
|
||||||
|
def test_file_input(self, runner):
|
||||||
|
"""Should read from file"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
with open('input.txt', 'w') as f:
|
||||||
|
f.write('test data\n')
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ['process', '--input', 'input.txt'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_file_output(self, runner):
|
||||||
|
"""Should write to file"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(cli, ['export', '--output', 'output.txt'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
with open('output.txt', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
assert len(content) > 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsolation:
|
||||||
|
"""Test isolated filesystem operations"""
|
||||||
|
|
||||||
|
def test_isolated_filesystem(self, runner):
|
||||||
|
"""Should work in isolated filesystem"""
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(cli, ['init', 'test-project'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
import os
|
||||||
|
assert os.path.exists('test-project')
|
||||||
346
skills/cli-testing-patterns/templates/pytest-fixtures.py
Normal file
346
skills/cli-testing-patterns/templates/pytest-fixtures.py
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
"""
|
||||||
|
Pytest Fixtures Template
|
||||||
|
|
||||||
|
Reusable pytest fixtures for CLI testing with Click.testing.CliRunner
|
||||||
|
Provides common setup, teardown, and test utilities
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from mycli.cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
# Basic Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runner():
|
||||||
|
"""Create a CliRunner instance for testing"""
|
||||||
|
return CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def isolated_runner():
|
||||||
|
"""Create a CliRunner with isolated filesystem"""
|
||||||
|
runner = CliRunner()
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
yield runner
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_config_dir(tmp_path):
|
||||||
|
"""Create a temporary configuration directory"""
|
||||||
|
config_dir = tmp_path / '.mycli'
|
||||||
|
config_dir.mkdir()
|
||||||
|
return config_dir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_file(temp_config_dir):
|
||||||
|
"""Create a temporary configuration file"""
|
||||||
|
config_path = temp_config_dir / 'config.yaml'
|
||||||
|
config_content = """
|
||||||
|
api_key: your_test_key_here
|
||||||
|
environment: development
|
||||||
|
verbose: false
|
||||||
|
timeout: 30
|
||||||
|
"""
|
||||||
|
config_path.write_text(config_content)
|
||||||
|
return config_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def env_with_config(temp_config_dir, monkeypatch):
|
||||||
|
"""Set up environment with config directory"""
|
||||||
|
monkeypatch.setenv('MYCLI_CONFIG_DIR', str(temp_config_dir))
|
||||||
|
return temp_config_dir
|
||||||
|
|
||||||
|
|
||||||
|
# File System Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_workspace(tmp_path):
|
||||||
|
"""Create a temporary workspace directory"""
|
||||||
|
workspace = tmp_path / 'workspace'
|
||||||
|
workspace.mkdir()
|
||||||
|
return workspace
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_project(temp_workspace):
|
||||||
|
"""Create a sample project structure"""
|
||||||
|
project = temp_workspace / 'sample-project'
|
||||||
|
project.mkdir()
|
||||||
|
|
||||||
|
# Create sample files
|
||||||
|
(project / 'package.json').write_text('{"name": "sample", "version": "1.0.0"}')
|
||||||
|
(project / 'README.md').write_text('# Sample Project')
|
||||||
|
|
||||||
|
src_dir = project / 'src'
|
||||||
|
src_dir.mkdir()
|
||||||
|
(src_dir / 'index.js').write_text('console.log("Hello, World!");')
|
||||||
|
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_files(temp_workspace):
|
||||||
|
"""Create sample files for testing"""
|
||||||
|
files = {
|
||||||
|
'input.txt': 'test input data\n',
|
||||||
|
'config.yaml': 'key: value\n',
|
||||||
|
'data.json': '{"id": 1, "name": "test"}\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
created_files = {}
|
||||||
|
for filename, content in files.items():
|
||||||
|
file_path = temp_workspace / filename
|
||||||
|
file_path.write_text(content)
|
||||||
|
created_files[filename] = file_path
|
||||||
|
|
||||||
|
return created_files
|
||||||
|
|
||||||
|
|
||||||
|
# Mock Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_api_key(monkeypatch):
|
||||||
|
"""Mock API key environment variable"""
|
||||||
|
monkeypatch.setenv('MYCLI_API_KEY', 'test_api_key_123')
|
||||||
|
return 'test_api_key_123'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_home_dir(tmp_path, monkeypatch):
|
||||||
|
"""Mock home directory"""
|
||||||
|
home = tmp_path / 'home'
|
||||||
|
home.mkdir()
|
||||||
|
monkeypatch.setenv('HOME', str(home))
|
||||||
|
return home
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_no_config(monkeypatch):
|
||||||
|
"""Remove all configuration environment variables"""
|
||||||
|
vars_to_remove = [
|
||||||
|
'MYCLI_CONFIG_DIR',
|
||||||
|
'MYCLI_API_KEY',
|
||||||
|
'MYCLI_ENVIRONMENT',
|
||||||
|
]
|
||||||
|
for var in vars_to_remove:
|
||||||
|
monkeypatch.delenv(var, raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
# State Management Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cli_state(temp_workspace):
|
||||||
|
"""Create a CLI state file"""
|
||||||
|
state_file = temp_workspace / '.mycli-state'
|
||||||
|
state = {
|
||||||
|
'initialized': True,
|
||||||
|
'last_command': None,
|
||||||
|
'history': []
|
||||||
|
}
|
||||||
|
import json
|
||||||
|
state_file.write_text(json.dumps(state, indent=2))
|
||||||
|
return state_file
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def clean_state(temp_workspace):
|
||||||
|
"""Ensure no state file exists"""
|
||||||
|
state_file = temp_workspace / '.mycli-state'
|
||||||
|
if state_file.exists():
|
||||||
|
state_file.unlink()
|
||||||
|
return temp_workspace
|
||||||
|
|
||||||
|
|
||||||
|
# Helper Function Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def run_cli_command(runner):
|
||||||
|
"""Helper function to run CLI commands and return parsed results"""
|
||||||
|
def _run(args, input_data=None, env=None):
|
||||||
|
"""
|
||||||
|
Run a CLI command and return structured results
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: List of command arguments
|
||||||
|
input_data: Optional input for interactive prompts
|
||||||
|
env: Optional environment variables dict
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with keys: exit_code, output, lines, success
|
||||||
|
"""
|
||||||
|
result = runner.invoke(cli, args, input=input_data, env=env)
|
||||||
|
return {
|
||||||
|
'exit_code': result.exit_code,
|
||||||
|
'output': result.output,
|
||||||
|
'lines': result.output.splitlines(),
|
||||||
|
'success': result.exit_code == 0
|
||||||
|
}
|
||||||
|
return _run
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def assert_cli_success(runner):
|
||||||
|
"""Helper to assert successful CLI execution"""
|
||||||
|
def _assert(args, expected_in_output=None):
|
||||||
|
"""
|
||||||
|
Run CLI command and assert success
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: List of command arguments
|
||||||
|
expected_in_output: Optional string expected in output
|
||||||
|
"""
|
||||||
|
result = runner.invoke(cli, args)
|
||||||
|
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||||
|
if expected_in_output:
|
||||||
|
assert expected_in_output in result.output
|
||||||
|
return result
|
||||||
|
return _assert
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def assert_cli_failure(runner):
|
||||||
|
"""Helper to assert CLI command failure"""
|
||||||
|
def _assert(args, expected_in_output=None):
|
||||||
|
"""
|
||||||
|
Run CLI command and assert failure
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: List of command arguments
|
||||||
|
expected_in_output: Optional string expected in output
|
||||||
|
"""
|
||||||
|
result = runner.invoke(cli, args)
|
||||||
|
assert result.exit_code != 0, f"Command should have failed: {result.output}"
|
||||||
|
if expected_in_output:
|
||||||
|
assert expected_in_output in result.output
|
||||||
|
return result
|
||||||
|
return _assert
|
||||||
|
|
||||||
|
|
||||||
|
# Cleanup Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cleanup_temp_files(request):
|
||||||
|
"""Automatically clean up temporary files after tests"""
|
||||||
|
temp_files = []
|
||||||
|
|
||||||
|
def _register(filepath):
|
||||||
|
temp_files.append(filepath)
|
||||||
|
|
||||||
|
request.addfinalizer(lambda: [
|
||||||
|
os.remove(f) for f in temp_files if os.path.exists(f)
|
||||||
|
])
|
||||||
|
|
||||||
|
return _register
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def test_data_dir():
|
||||||
|
"""Provide path to test data directory"""
|
||||||
|
return Path(__file__).parent / 'test_data'
|
||||||
|
|
||||||
|
|
||||||
|
# Parametrized Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture(params=['json', 'yaml', 'table'])
|
||||||
|
def output_format(request):
|
||||||
|
"""Parametrize tests across different output formats"""
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=[True, False])
|
||||||
|
def verbose_mode(request):
|
||||||
|
"""Parametrize tests with and without verbose mode"""
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=['development', 'staging', 'production'])
|
||||||
|
def environment(request):
|
||||||
|
"""Parametrize tests across different environments"""
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
# Integration Test Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def integration_workspace(tmp_path):
|
||||||
|
"""
|
||||||
|
Create a complete integration test workspace with all necessary files
|
||||||
|
"""
|
||||||
|
workspace = tmp_path / 'integration'
|
||||||
|
workspace.mkdir()
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
(workspace / 'src').mkdir()
|
||||||
|
(workspace / 'tests').mkdir()
|
||||||
|
(workspace / 'config').mkdir()
|
||||||
|
(workspace / 'data').mkdir()
|
||||||
|
|
||||||
|
# Create config files
|
||||||
|
(workspace / 'config' / 'dev.yaml').write_text('env: development\n')
|
||||||
|
(workspace / 'config' / 'prod.yaml').write_text('env: production\n')
|
||||||
|
|
||||||
|
# Initialize CLI
|
||||||
|
runner = CliRunner()
|
||||||
|
with runner.isolated_filesystem(temp_dir=workspace):
|
||||||
|
runner.invoke(cli, ['init'])
|
||||||
|
|
||||||
|
return workspace
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_external_service(monkeypatch):
|
||||||
|
"""Mock external service API calls"""
|
||||||
|
class MockService:
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def call_api(self, endpoint, method='GET', data=None):
|
||||||
|
self.calls.append({
|
||||||
|
'endpoint': endpoint,
|
||||||
|
'method': method,
|
||||||
|
'data': data
|
||||||
|
})
|
||||||
|
return {'status': 'success', 'data': 'mock response'}
|
||||||
|
|
||||||
|
mock = MockService()
|
||||||
|
# Replace actual service with mock
|
||||||
|
monkeypatch.setattr('mycli.services.api', mock)
|
||||||
|
return mock
|
||||||
|
|
||||||
|
|
||||||
|
# Snapshot Testing Fixtures
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def snapshot_dir(tmp_path):
|
||||||
|
"""Create directory for snapshot testing"""
|
||||||
|
snapshot = tmp_path / 'snapshots'
|
||||||
|
snapshot.mkdir()
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def compare_output(snapshot_dir):
|
||||||
|
"""Compare CLI output with saved snapshot"""
|
||||||
|
def _compare(output, snapshot_name):
|
||||||
|
snapshot_file = snapshot_dir / f'{snapshot_name}.txt'
|
||||||
|
|
||||||
|
if not snapshot_file.exists():
|
||||||
|
# Create snapshot
|
||||||
|
snapshot_file.write_text(output)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Compare with existing snapshot
|
||||||
|
expected = snapshot_file.read_text()
|
||||||
|
return output == expected
|
||||||
|
|
||||||
|
return _compare
|
||||||
378
skills/cli-testing-patterns/templates/pytest-integration-test.py
Normal file
378
skills/cli-testing-patterns/templates/pytest-integration-test.py
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
"""
|
||||||
|
Pytest Integration Test Template
|
||||||
|
|
||||||
|
Complete workflow testing for CLI applications using Click.testing.CliRunner
|
||||||
|
Tests multi-command workflows, state persistence, and end-to-end scenarios
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from mycli.cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def integration_runner():
|
||||||
|
"""Create runner with isolated filesystem for integration tests"""
|
||||||
|
runner = CliRunner()
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
yield runner
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeploymentWorkflow:
|
||||||
|
"""Test complete deployment workflow"""
|
||||||
|
|
||||||
|
def test_full_deployment_workflow(self, integration_runner):
|
||||||
|
"""Should complete init -> configure -> build -> deploy workflow"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
# Step 1: Initialize project
|
||||||
|
result = runner.invoke(cli, ['init', 'my-project'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Project initialized' in result.output
|
||||||
|
assert os.path.exists('my-project')
|
||||||
|
|
||||||
|
# Step 2: Configure API key
|
||||||
|
os.chdir('my-project')
|
||||||
|
result = runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Step 3: Build project
|
||||||
|
result = runner.invoke(cli, ['build', '--production'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Build successful' in result.output
|
||||||
|
|
||||||
|
# Step 4: Deploy to production
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Deployed successfully' in result.output
|
||||||
|
|
||||||
|
def test_deployment_without_config_fails(self, integration_runner):
|
||||||
|
"""Should fail deployment without required configuration"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
# Initialize but don't configure
|
||||||
|
runner.invoke(cli, ['init', 'my-project'])
|
||||||
|
os.chdir('my-project')
|
||||||
|
|
||||||
|
# Try to deploy without API key
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'api_key' in result.output.lower()
|
||||||
|
|
||||||
|
def test_deployment_rollback(self, integration_runner):
|
||||||
|
"""Should rollback failed deployment"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
# Setup and deploy
|
||||||
|
runner.invoke(cli, ['init', 'my-project'])
|
||||||
|
os.chdir('my-project')
|
||||||
|
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
||||||
|
runner.invoke(cli, ['deploy', 'staging'])
|
||||||
|
|
||||||
|
# Rollback
|
||||||
|
result = runner.invoke(cli, ['rollback'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Rollback successful' in result.output
|
||||||
|
|
||||||
|
|
||||||
|
class TestMultiEnvironmentWorkflow:
|
||||||
|
"""Test multi-environment configuration and deployment"""
|
||||||
|
|
||||||
|
def test_manage_multiple_environments(self, integration_runner):
|
||||||
|
"""Should manage dev, staging, and production environments"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
runner.invoke(cli, ['init', 'multi-env-project'])
|
||||||
|
os.chdir('multi-env-project')
|
||||||
|
|
||||||
|
# Configure development
|
||||||
|
runner.invoke(cli, ['config', 'set', 'api_key', 'dev_key', '--env', 'development'])
|
||||||
|
runner.invoke(cli, ['config', 'set', 'base_url', 'https://dev.api.example.com', '--env', 'development'])
|
||||||
|
|
||||||
|
# Configure staging
|
||||||
|
runner.invoke(cli, ['config', 'set', 'api_key', 'staging_key', '--env', 'staging'])
|
||||||
|
runner.invoke(cli, ['config', 'set', 'base_url', 'https://staging.api.example.com', '--env', 'staging'])
|
||||||
|
|
||||||
|
# Configure production
|
||||||
|
runner.invoke(cli, ['config', 'set', 'api_key', 'prod_key', '--env', 'production'])
|
||||||
|
runner.invoke(cli, ['config', 'set', 'base_url', 'https://api.example.com', '--env', 'production'])
|
||||||
|
|
||||||
|
# Deploy to each environment
|
||||||
|
dev_result = runner.invoke(cli, ['deploy', 'development'])
|
||||||
|
assert dev_result.exit_code == 0
|
||||||
|
assert 'dev.api.example.com' in dev_result.output
|
||||||
|
|
||||||
|
staging_result = runner.invoke(cli, ['deploy', 'staging'])
|
||||||
|
assert staging_result.exit_code == 0
|
||||||
|
assert 'staging.api.example.com' in staging_result.output
|
||||||
|
|
||||||
|
prod_result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert prod_result.exit_code == 0
|
||||||
|
assert 'api.example.com' in prod_result.output
|
||||||
|
|
||||||
|
def test_environment_isolation(self, integration_runner):
|
||||||
|
"""Should keep environment configurations isolated"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
runner.invoke(cli, ['init', 'isolated-project'])
|
||||||
|
os.chdir('isolated-project')
|
||||||
|
|
||||||
|
# Set different values for each environment
|
||||||
|
runner.invoke(cli, ['config', 'set', 'timeout', '10', '--env', 'development'])
|
||||||
|
runner.invoke(cli, ['config', 'set', 'timeout', '30', '--env', 'production'])
|
||||||
|
|
||||||
|
# Verify values are isolated
|
||||||
|
dev_result = runner.invoke(cli, ['config', 'get', 'timeout', '--env', 'development'])
|
||||||
|
assert '10' in dev_result.output
|
||||||
|
|
||||||
|
prod_result = runner.invoke(cli, ['config', 'get', 'timeout', '--env', 'production'])
|
||||||
|
assert '30' in prod_result.output
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatePersistence:
|
||||||
|
"""Test state management and persistence"""
|
||||||
|
|
||||||
|
def test_state_persistence_across_commands(self, integration_runner):
|
||||||
|
"""Should maintain state across multiple commands"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
# Initialize state
|
||||||
|
result = runner.invoke(cli, ['state', 'init'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Set multiple state values
|
||||||
|
runner.invoke(cli, ['state', 'set', 'counter', '0'])
|
||||||
|
runner.invoke(cli, ['state', 'set', 'user', 'testuser'])
|
||||||
|
|
||||||
|
# Increment counter multiple times
|
||||||
|
for i in range(5):
|
||||||
|
runner.invoke(cli, ['increment'])
|
||||||
|
|
||||||
|
# Verify final state
|
||||||
|
result = runner.invoke(cli, ['state', 'get', 'counter'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert '5' in result.output
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ['state', 'get', 'user'])
|
||||||
|
assert 'testuser' in result.output
|
||||||
|
|
||||||
|
def test_state_recovery_from_corruption(self, integration_runner):
|
||||||
|
"""Should recover from corrupted state file"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
# Create valid state
|
||||||
|
runner.invoke(cli, ['state', 'init'])
|
||||||
|
runner.invoke(cli, ['state', 'set', 'key', 'value'])
|
||||||
|
|
||||||
|
# Corrupt the state file
|
||||||
|
with open('.mycli-state', 'w') as f:
|
||||||
|
f.write('invalid json {[}')
|
||||||
|
|
||||||
|
# Should detect corruption and recover
|
||||||
|
result = runner.invoke(cli, ['state', 'get', 'key'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'corrupt' in result.output.lower()
|
||||||
|
|
||||||
|
# Should be able to reset
|
||||||
|
result = runner.invoke(cli, ['state', 'reset'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestPluginWorkflow:
|
||||||
|
"""Test plugin installation and usage"""
|
||||||
|
|
||||||
|
def test_plugin_lifecycle(self, integration_runner):
|
||||||
|
"""Should install, use, and uninstall plugins"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
runner.invoke(cli, ['init', 'plugin-project'])
|
||||||
|
os.chdir('plugin-project')
|
||||||
|
|
||||||
|
# Install plugin
|
||||||
|
result = runner.invoke(cli, ['plugin', 'install', 'test-plugin'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'installed' in result.output.lower()
|
||||||
|
|
||||||
|
# Verify plugin is listed
|
||||||
|
result = runner.invoke(cli, ['plugin', 'list'])
|
||||||
|
assert 'test-plugin' in result.output
|
||||||
|
|
||||||
|
# Use plugin command
|
||||||
|
result = runner.invoke(cli, ['test-plugin:command', '--arg', 'value'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Uninstall plugin
|
||||||
|
result = runner.invoke(cli, ['plugin', 'uninstall', 'test-plugin'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Verify plugin is removed
|
||||||
|
result = runner.invoke(cli, ['plugin', 'list'])
|
||||||
|
assert 'test-plugin' not in result.output
|
||||||
|
|
||||||
|
def test_plugin_conflict_detection(self, integration_runner):
|
||||||
|
"""Should detect and handle plugin conflicts"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
runner.invoke(cli, ['init', 'conflict-project'])
|
||||||
|
os.chdir('conflict-project')
|
||||||
|
|
||||||
|
# Install first plugin
|
||||||
|
runner.invoke(cli, ['plugin', 'install', 'plugin-a'])
|
||||||
|
|
||||||
|
# Try to install conflicting plugin
|
||||||
|
result = runner.invoke(cli, ['plugin', 'install', 'plugin-b'])
|
||||||
|
if 'conflict' in result.output.lower():
|
||||||
|
assert result.exit_code != 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestDataMigration:
|
||||||
|
"""Test data migration workflows"""
|
||||||
|
|
||||||
|
def test_version_migration(self, integration_runner):
|
||||||
|
"""Should migrate data between versions"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
# Create old version data
|
||||||
|
old_data = {
|
||||||
|
'version': 1,
|
||||||
|
'format': 'legacy',
|
||||||
|
'data': {'key': 'value'}
|
||||||
|
}
|
||||||
|
with open('data.json', 'w') as f:
|
||||||
|
json.dump(old_data, f)
|
||||||
|
|
||||||
|
# Run migration
|
||||||
|
result = runner.invoke(cli, ['migrate', '--to', '2.0'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Verify new format
|
||||||
|
with open('data.json', 'r') as f:
|
||||||
|
new_data = json.load(f)
|
||||||
|
assert new_data['version'] == 2
|
||||||
|
assert 'legacy' not in new_data.get('format', '')
|
||||||
|
|
||||||
|
def test_migration_backup(self, integration_runner):
|
||||||
|
"""Should create backup during migration"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
# Create data
|
||||||
|
data = {'version': 1, 'data': 'important'}
|
||||||
|
with open('data.json', 'w') as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
# Migrate with backup
|
||||||
|
result = runner.invoke(cli, ['migrate', '--to', '2.0', '--backup'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Verify backup exists
|
||||||
|
assert os.path.exists('data.json.backup')
|
||||||
|
|
||||||
|
|
||||||
|
class TestConcurrentOperations:
|
||||||
|
"""Test handling of concurrent operations"""
|
||||||
|
|
||||||
|
def test_file_locking(self, integration_runner):
|
||||||
|
"""Should prevent concurrent modifications"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
runner.invoke(cli, ['init', 'lock-project'])
|
||||||
|
os.chdir('lock-project')
|
||||||
|
|
||||||
|
# Create lock file
|
||||||
|
with open('.mycli.lock', 'w') as f:
|
||||||
|
f.write('locked')
|
||||||
|
|
||||||
|
# Try to run command that needs lock
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert 'lock' in result.output.lower()
|
||||||
|
|
||||||
|
def test_lock_timeout(self, integration_runner):
|
||||||
|
"""Should timeout waiting for lock"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
runner.invoke(cli, ['init', 'timeout-project'])
|
||||||
|
os.chdir('timeout-project')
|
||||||
|
|
||||||
|
# Create stale lock
|
||||||
|
with open('.mycli.lock', 'w') as f:
|
||||||
|
import time
|
||||||
|
f.write(str(time.time() - 3600)) # 1 hour old
|
||||||
|
|
||||||
|
# Should detect stale lock and continue
|
||||||
|
result = runner.invoke(cli, ['build'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestErrorRecovery:
|
||||||
|
"""Test error recovery and retry logic"""
|
||||||
|
|
||||||
|
def test_retry_on_failure(self, integration_runner):
|
||||||
|
"""Should retry failed operations"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
runner.invoke(cli, ['init', 'retry-project'])
|
||||||
|
os.chdir('retry-project')
|
||||||
|
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
||||||
|
|
||||||
|
# Simulate failure and retry
|
||||||
|
result = runner.invoke(cli, ['deploy', 'staging', '--retry', '3'])
|
||||||
|
# Should attempt retry logic
|
||||||
|
|
||||||
|
def test_partial_failure_recovery(self, integration_runner):
|
||||||
|
"""Should recover from partial failures"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
runner.invoke(cli, ['init', 'recovery-project'])
|
||||||
|
os.chdir('recovery-project')
|
||||||
|
|
||||||
|
# Create partial state
|
||||||
|
runner.invoke(cli, ['build', '--step', '1'])
|
||||||
|
runner.invoke(cli, ['build', '--step', '2'])
|
||||||
|
|
||||||
|
# Complete from last successful step
|
||||||
|
result = runner.invoke(cli, ['build', '--continue'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompleteWorkflow:
|
||||||
|
"""Test complete end-to-end workflows"""
|
||||||
|
|
||||||
|
def test_full_project_lifecycle(self, integration_runner):
|
||||||
|
"""Should complete entire project lifecycle"""
|
||||||
|
runner = integration_runner
|
||||||
|
|
||||||
|
# Create project
|
||||||
|
result = runner.invoke(cli, ['create', 'full-project'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
os.chdir('full-project')
|
||||||
|
|
||||||
|
# Configure
|
||||||
|
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
||||||
|
runner.invoke(cli, ['config', 'set', 'region', 'us-west-1'])
|
||||||
|
|
||||||
|
# Add dependencies
|
||||||
|
result = runner.invoke(cli, ['add', 'dependency', 'package-name'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Build
|
||||||
|
result = runner.invoke(cli, ['build', '--production'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Test
|
||||||
|
result = runner.invoke(cli, ['test'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Verify deployment
|
||||||
|
result = runner.invoke(cli, ['status'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'deployed' in result.output.lower()
|
||||||
509
skills/cli-testing-patterns/templates/test-helpers.py
Normal file
509
skills/cli-testing-patterns/templates/test-helpers.py
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
"""
|
||||||
|
Python Test Helper Functions
|
||||||
|
|
||||||
|
Utility functions for CLI testing with pytest and Click.testing.CliRunner
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Callable
|
||||||
|
from click.testing import CliRunner, Result
|
||||||
|
|
||||||
|
|
||||||
|
class CLITestHarness:
|
||||||
|
"""Test harness for CLI testing with helpful assertion methods"""
|
||||||
|
|
||||||
|
def __init__(self, cli_app):
|
||||||
|
"""
|
||||||
|
Initialize test harness
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_app: Click CLI application to test
|
||||||
|
"""
|
||||||
|
self.cli = cli_app
|
||||||
|
self.runner = CliRunner()
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
args: List[str],
|
||||||
|
input_data: Optional[str] = None,
|
||||||
|
env: Optional[Dict[str, str]] = None
|
||||||
|
) -> Result:
|
||||||
|
"""
|
||||||
|
Run CLI command
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Command arguments
|
||||||
|
input_data: Input for interactive prompts
|
||||||
|
env: Environment variables
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Click Result object
|
||||||
|
"""
|
||||||
|
return self.runner.invoke(self.cli, args, input=input_data, env=env)
|
||||||
|
|
||||||
|
def assert_success(
|
||||||
|
self,
|
||||||
|
args: List[str],
|
||||||
|
expected_in_output: Optional[str] = None
|
||||||
|
) -> Result:
|
||||||
|
"""
|
||||||
|
Run command and assert successful execution
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Command arguments
|
||||||
|
expected_in_output: Optional string expected in output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Click Result object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If command fails or output doesn't match
|
||||||
|
"""
|
||||||
|
result = self.run(args)
|
||||||
|
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||||
|
|
||||||
|
if expected_in_output:
|
||||||
|
assert expected_in_output in result.output, \
|
||||||
|
f"Expected '{expected_in_output}' in output: {result.output}"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def assert_failure(
|
||||||
|
self,
|
||||||
|
args: List[str],
|
||||||
|
expected_in_output: Optional[str] = None
|
||||||
|
) -> Result:
|
||||||
|
"""
|
||||||
|
Run command and assert it fails
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Command arguments
|
||||||
|
expected_in_output: Optional string expected in output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Click Result object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If command succeeds or output doesn't match
|
||||||
|
"""
|
||||||
|
result = self.run(args)
|
||||||
|
assert result.exit_code != 0, f"Command should have failed: {result.output}"
|
||||||
|
|
||||||
|
if expected_in_output:
|
||||||
|
assert expected_in_output in result.output, \
|
||||||
|
f"Expected '{expected_in_output}' in output: {result.output}"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def assert_exit_code(self, args: List[str], expected_code: int) -> Result:
|
||||||
|
"""
|
||||||
|
Run command and assert specific exit code
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Command arguments
|
||||||
|
expected_code: Expected exit code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Click Result object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If exit code doesn't match
|
||||||
|
"""
|
||||||
|
result = self.run(args)
|
||||||
|
assert result.exit_code == expected_code, \
|
||||||
|
f"Expected exit code {expected_code}, got {result.exit_code}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
def run_json(self, args: List[str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Run command and parse JSON output
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Command arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed JSON object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If command fails
|
||||||
|
json.JSONDecodeError: If output is not valid JSON
|
||||||
|
"""
|
||||||
|
result = self.assert_success(args)
|
||||||
|
return json.loads(result.output)
|
||||||
|
|
||||||
|
|
||||||
|
def create_temp_workspace() -> Path:
|
||||||
|
"""
|
||||||
|
Create temporary workspace directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to temporary workspace
|
||||||
|
"""
|
||||||
|
temp_dir = Path(tempfile.mkdtemp(prefix='cli-test-'))
|
||||||
|
return temp_dir
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_workspace(workspace: Path) -> None:
|
||||||
|
"""
|
||||||
|
Clean up temporary workspace
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace: Path to workspace to remove
|
||||||
|
"""
|
||||||
|
if workspace.exists():
|
||||||
|
shutil.rmtree(workspace)
|
||||||
|
|
||||||
|
|
||||||
|
def create_temp_file(content: str, suffix: str = '.txt') -> Path:
|
||||||
|
"""
|
||||||
|
Create temporary file with content
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: File content
|
||||||
|
suffix: File extension
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to created file
|
||||||
|
"""
|
||||||
|
fd, path = tempfile.mkstemp(suffix=suffix)
|
||||||
|
with os.fdopen(fd, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
return Path(path)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_file_exists(filepath: Path, message: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Assert file exists
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to file
|
||||||
|
message: Optional custom error message
|
||||||
|
"""
|
||||||
|
assert filepath.exists(), message or f"File does not exist: {filepath}"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_file_contains(filepath: Path, expected: str) -> None:
|
||||||
|
"""
|
||||||
|
Assert file contains expected text
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to file
|
||||||
|
expected: Expected text
|
||||||
|
"""
|
||||||
|
content = filepath.read_text()
|
||||||
|
assert expected in content, \
|
||||||
|
f"Expected '{expected}' in file {filepath}\nActual content: {content}"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_json_output(result: Result, schema: Dict[str, type]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Assert output is valid JSON matching schema
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: Click Result object
|
||||||
|
schema: Expected schema as dict of {key: expected_type}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed JSON object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If JSON is invalid or doesn't match schema
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = json.loads(result.output)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise AssertionError(f"Invalid JSON output: {e}\nOutput: {result.output}")
|
||||||
|
|
||||||
|
for key, expected_type in schema.items():
|
||||||
|
assert key in data, f"Missing key in JSON output: {key}"
|
||||||
|
assert isinstance(data[key], expected_type), \
|
||||||
|
f"Expected type {expected_type} for key {key}, got {type(data[key])}"
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def mock_env_vars(vars_dict: Dict[str, str]) -> Callable[[], None]:
|
||||||
|
"""
|
||||||
|
Mock environment variables
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vars_dict: Dictionary of environment variables to set
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Function to restore original environment
|
||||||
|
|
||||||
|
Example:
|
||||||
|
restore = mock_env_vars({'API_KEY': 'test_key'})
|
||||||
|
# ... run tests ...
|
||||||
|
restore()
|
||||||
|
"""
|
||||||
|
original = {}
|
||||||
|
|
||||||
|
for key, value in vars_dict.items():
|
||||||
|
original[key] = os.environ.get(key)
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
|
def restore():
|
||||||
|
for key, value in original.items():
|
||||||
|
if value is None:
|
||||||
|
os.environ.pop(key, None)
|
||||||
|
else:
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
|
return restore
|
||||||
|
|
||||||
|
|
||||||
|
def compare_output_lines(result: Result, expected_lines: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Compare output with expected lines
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: Click Result object
|
||||||
|
expected_lines: List of expected lines in output
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If any expected line is missing
|
||||||
|
"""
|
||||||
|
output = result.output
|
||||||
|
for expected in expected_lines:
|
||||||
|
assert expected in output, \
|
||||||
|
f"Expected line '{expected}' not found in output:\n{output}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_table_output(result: Result) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Parse table output into list of dictionaries
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: Click Result object with table output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of row dictionaries
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Expects table with headers and │ separators
|
||||||
|
"""
|
||||||
|
lines = result.output.strip().split('\n')
|
||||||
|
|
||||||
|
# Find header line
|
||||||
|
header_line = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if '│' in line and i > 0:
|
||||||
|
header_line = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if header_line is None:
|
||||||
|
raise ValueError("Could not find table header")
|
||||||
|
|
||||||
|
# Parse headers
|
||||||
|
headers = [h.strip() for h in lines[header_line].split('│') if h.strip()]
|
||||||
|
|
||||||
|
# Parse rows
|
||||||
|
rows = []
|
||||||
|
for line in lines[header_line + 2:]: # Skip separator
|
||||||
|
if '│' in line:
|
||||||
|
values = [v.strip() for v in line.split('│') if v.strip()]
|
||||||
|
if len(values) == len(headers):
|
||||||
|
rows.append(dict(zip(headers, values)))
|
||||||
|
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotTester:
|
||||||
|
"""Helper for snapshot testing CLI output"""
|
||||||
|
|
||||||
|
def __init__(self, snapshot_dir: Path):
|
||||||
|
"""
|
||||||
|
Initialize snapshot tester
|
||||||
|
|
||||||
|
Args:
|
||||||
|
snapshot_dir: Directory to store snapshots
|
||||||
|
"""
|
||||||
|
self.snapshot_dir = snapshot_dir
|
||||||
|
self.snapshot_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def assert_matches(
|
||||||
|
self,
|
||||||
|
result: Result,
|
||||||
|
snapshot_name: str,
|
||||||
|
update: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Assert output matches snapshot
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: Click Result object
|
||||||
|
snapshot_name: Name of snapshot file
|
||||||
|
update: Whether to update snapshot
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If output doesn't match snapshot
|
||||||
|
"""
|
||||||
|
snapshot_file = self.snapshot_dir / f'{snapshot_name}.txt'
|
||||||
|
|
||||||
|
if update or not snapshot_file.exists():
|
||||||
|
snapshot_file.write_text(result.output)
|
||||||
|
return
|
||||||
|
|
||||||
|
expected = snapshot_file.read_text()
|
||||||
|
assert result.output == expected, \
|
||||||
|
f"Output doesn't match snapshot {snapshot_name}\n" \
|
||||||
|
f"Expected:\n{expected}\n\nActual:\n{result.output}"
|
||||||
|
|
||||||
|
|
||||||
|
class MockConfig:
|
||||||
|
"""Mock configuration file for testing"""
|
||||||
|
|
||||||
|
def __init__(self, workspace: Path, filename: str = '.myclirc'):
|
||||||
|
"""
|
||||||
|
Initialize mock config
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace: Workspace directory
|
||||||
|
filename: Config filename
|
||||||
|
"""
|
||||||
|
self.config_path = workspace / filename
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
def set(self, key: str, value: Any) -> None:
|
||||||
|
"""Set configuration value"""
|
||||||
|
self.data[key] = value
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
|
"""Get configuration value"""
|
||||||
|
return self.data.get(key, default)
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
"""Save configuration to file"""
|
||||||
|
import yaml
|
||||||
|
with open(self.config_path, 'w') as f:
|
||||||
|
yaml.dump(self.data, f)
|
||||||
|
|
||||||
|
def load(self) -> None:
|
||||||
|
"""Load configuration from file"""
|
||||||
|
if self.config_path.exists():
|
||||||
|
import yaml
|
||||||
|
with open(self.config_path, 'r') as f:
|
||||||
|
self.data = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_file(filepath: Path, timeout: float = 5.0) -> None:
|
||||||
|
"""
|
||||||
|
Wait for file to exist
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to file
|
||||||
|
timeout: Timeout in seconds
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TimeoutError: If file doesn't exist within timeout
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
while not filepath.exists():
|
||||||
|
if time.time() - start > timeout:
|
||||||
|
raise TimeoutError(f"Timeout waiting for file: {filepath}")
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
def capture_output(func: Callable) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Capture stdout and stderr during function execution
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: Function to execute
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with 'stdout' and 'stderr' keys
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
old_stderr = sys.stderr
|
||||||
|
|
||||||
|
stdout_capture = StringIO()
|
||||||
|
stderr_capture = StringIO()
|
||||||
|
|
||||||
|
sys.stdout = stdout_capture
|
||||||
|
sys.stderr = stderr_capture
|
||||||
|
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
finally:
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
|
return {
|
||||||
|
'stdout': stdout_capture.getvalue(),
|
||||||
|
'stderr': stderr_capture.getvalue()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestHelper:
|
||||||
|
"""Helper for integration testing with state management"""
|
||||||
|
|
||||||
|
def __init__(self, cli_app, workspace: Optional[Path] = None):
|
||||||
|
"""
|
||||||
|
Initialize integration test helper
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_app: Click CLI application
|
||||||
|
workspace: Optional workspace directory
|
||||||
|
"""
|
||||||
|
self.harness = CLITestHarness(cli_app)
|
||||||
|
self.workspace = workspace or create_temp_workspace()
|
||||||
|
self.original_cwd = Path.cwd()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Enter context - change to workspace"""
|
||||||
|
os.chdir(self.workspace)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Exit context - restore cwd and cleanup"""
|
||||||
|
os.chdir(self.original_cwd)
|
||||||
|
cleanup_workspace(self.workspace)
|
||||||
|
|
||||||
|
def run_workflow(self, commands: List[List[str]]) -> List[Result]:
|
||||||
|
"""
|
||||||
|
Run multiple commands in sequence
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commands: List of command argument lists
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Result objects
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for cmd in commands:
|
||||||
|
result = self.harness.run(cmd)
|
||||||
|
results.append(result)
|
||||||
|
if result.exit_code != 0:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
|
||||||
|
def assert_workflow_success(self, commands: List[List[str]]) -> List[Result]:
|
||||||
|
"""
|
||||||
|
Run workflow and assert all commands succeed
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commands: List of command argument lists
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Result objects
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If any command fails
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for i, cmd in enumerate(commands):
|
||||||
|
result = self.harness.assert_success(cmd)
|
||||||
|
results.append(result)
|
||||||
|
return results
|
||||||
362
skills/cli-testing-patterns/templates/test-helpers.ts
Normal file
362
skills/cli-testing-patterns/templates/test-helpers.ts
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
/**
|
||||||
|
* Node.js Test Helper Functions
|
||||||
|
*
|
||||||
|
* Utility functions for CLI testing with Jest
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync, spawn, SpawnOptions } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI execution result interface
|
||||||
|
*/
|
||||||
|
export interface CLIResult {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
code: number;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute CLI command synchronously
|
||||||
|
* @param cliPath - Path to CLI executable
|
||||||
|
* @param args - Command arguments
|
||||||
|
* @param options - Execution options
|
||||||
|
* @returns CLI execution result
|
||||||
|
*/
|
||||||
|
export function runCLI(
|
||||||
|
cliPath: string,
|
||||||
|
args: string,
|
||||||
|
options: {
|
||||||
|
cwd?: string;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
timeout?: number;
|
||||||
|
} = {}
|
||||||
|
): CLIResult {
|
||||||
|
try {
|
||||||
|
const stdout = execSync(`${cliPath} ${args}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: { ...process.env, ...options.env },
|
||||||
|
timeout: options.timeout,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
stdout,
|
||||||
|
stderr: '',
|
||||||
|
code: 0,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
stdout: error.stdout || '',
|
||||||
|
stderr: error.stderr || '',
|
||||||
|
code: error.status || 1,
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute CLI command asynchronously
|
||||||
|
* @param cliPath - Path to CLI executable
|
||||||
|
* @param args - Command arguments array
|
||||||
|
* @param options - Spawn options
|
||||||
|
* @returns Promise of CLI execution result
|
||||||
|
*/
|
||||||
|
export function runCLIAsync(
|
||||||
|
cliPath: string,
|
||||||
|
args: string[],
|
||||||
|
options: SpawnOptions = {}
|
||||||
|
): Promise<CLIResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn(cliPath, args, {
|
||||||
|
...options,
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
resolve({
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
code: code || 0,
|
||||||
|
success: code === 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
resolve({
|
||||||
|
stdout,
|
||||||
|
stderr: stderr + error.message,
|
||||||
|
code: 1,
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create temporary test directory
|
||||||
|
* @returns Path to temporary directory
|
||||||
|
*/
|
||||||
|
export function createTempDir(): string {
|
||||||
|
const tempDir = path.join(os.tmpdir(), `cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up temporary directory
|
||||||
|
* @param dirPath - Directory to remove
|
||||||
|
*/
|
||||||
|
export function cleanupTempDir(dirPath: string): void {
|
||||||
|
if (fs.existsSync(dirPath)) {
|
||||||
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create temporary file with content
|
||||||
|
* @param content - File content
|
||||||
|
* @param extension - File extension
|
||||||
|
* @returns Path to created file
|
||||||
|
*/
|
||||||
|
export function createTempFile(content: string, extension: string = 'txt'): string {
|
||||||
|
const tempFile = path.join(os.tmpdir(), `test-${Date.now()}.${extension}`);
|
||||||
|
fs.writeFileSync(tempFile, content);
|
||||||
|
return tempFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert CLI command succeeds
|
||||||
|
* @param result - CLI execution result
|
||||||
|
* @param expectedOutput - Optional expected output substring
|
||||||
|
*/
|
||||||
|
export function assertSuccess(result: CLIResult, expectedOutput?: string): void {
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(`CLI command failed with exit code ${result.code}\nStderr: ${result.stderr}`);
|
||||||
|
}
|
||||||
|
if (expectedOutput && !result.stdout.includes(expectedOutput)) {
|
||||||
|
throw new Error(`Expected output to contain "${expectedOutput}"\nActual: ${result.stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert CLI command fails
|
||||||
|
* @param result - CLI execution result
|
||||||
|
* @param expectedError - Optional expected error substring
|
||||||
|
*/
|
||||||
|
export function assertFailure(result: CLIResult, expectedError?: string): void {
|
||||||
|
if (result.success) {
|
||||||
|
throw new Error(`CLI command should have failed but succeeded\nStdout: ${result.stdout}`);
|
||||||
|
}
|
||||||
|
if (expectedError && !result.stderr.includes(expectedError) && !result.stdout.includes(expectedError)) {
|
||||||
|
throw new Error(`Expected error to contain "${expectedError}"\nActual stderr: ${result.stderr}\nActual stdout: ${result.stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert exit code matches expected value
|
||||||
|
* @param result - CLI execution result
|
||||||
|
* @param expectedCode - Expected exit code
|
||||||
|
*/
|
||||||
|
export function assertExitCode(result: CLIResult, expectedCode: number): void {
|
||||||
|
if (result.code !== expectedCode) {
|
||||||
|
throw new Error(`Expected exit code ${expectedCode} but got ${result.code}\nStderr: ${result.stderr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse JSON output from CLI
|
||||||
|
* @param result - CLI execution result
|
||||||
|
* @returns Parsed JSON object
|
||||||
|
*/
|
||||||
|
export function parseJSONOutput<T = any>(result: CLIResult): T {
|
||||||
|
try {
|
||||||
|
return JSON.parse(result.stdout);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to parse JSON output: ${error}\nStdout: ${result.stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock environment variables for test
|
||||||
|
* @param vars - Environment variables to set
|
||||||
|
* @returns Function to restore original environment
|
||||||
|
*/
|
||||||
|
export function mockEnv(vars: Record<string, string>): () => void {
|
||||||
|
const original = { ...process.env };
|
||||||
|
|
||||||
|
Object.entries(vars).forEach(([key, value]) => {
|
||||||
|
process.env[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Object.keys(process.env).forEach((key) => {
|
||||||
|
if (!(key in original)) {
|
||||||
|
delete process.env[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.entries(original).forEach(([key, value]) => {
|
||||||
|
process.env[key] = value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for file to exist
|
||||||
|
* @param filePath - Path to file
|
||||||
|
* @param timeout - Timeout in milliseconds
|
||||||
|
* @returns Promise that resolves when file exists
|
||||||
|
*/
|
||||||
|
export async function waitForFile(filePath: string, timeout: number = 5000): Promise<void> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
while (!fs.existsSync(filePath)) {
|
||||||
|
if (Date.now() - startTime > timeout) {
|
||||||
|
throw new Error(`Timeout waiting for file: ${filePath}`);
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create CLI test fixture with setup and teardown
|
||||||
|
* @param setup - Setup function
|
||||||
|
* @param teardown - Teardown function
|
||||||
|
* @returns Test fixture object
|
||||||
|
*/
|
||||||
|
export function createFixture<T>(
|
||||||
|
setup: () => T | Promise<T>,
|
||||||
|
teardown: (fixture: T) => void | Promise<void>
|
||||||
|
): {
|
||||||
|
beforeEach: () => Promise<T>;
|
||||||
|
afterEach: (fixture: T) => Promise<void>;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
beforeEach: async () => setup(),
|
||||||
|
afterEach: async (fixture: T) => teardown(fixture),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture stdout/stderr during function execution
|
||||||
|
* @param fn - Function to execute
|
||||||
|
* @returns Captured output
|
||||||
|
*/
|
||||||
|
export function captureOutput(fn: () => void): { stdout: string; stderr: string } {
|
||||||
|
const originalStdout = process.stdout.write;
|
||||||
|
const originalStderr = process.stderr.write;
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
process.stdout.write = ((chunk: any) => {
|
||||||
|
stdout += chunk.toString();
|
||||||
|
return true;
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
process.stderr.write = ((chunk: any) => {
|
||||||
|
stderr += chunk.toString();
|
||||||
|
return true;
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} finally {
|
||||||
|
process.stdout.write = originalStdout;
|
||||||
|
process.stderr.write = originalStderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stdout, stderr };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test helper for testing CLI with different input combinations
|
||||||
|
*/
|
||||||
|
export class CLITestHarness {
|
||||||
|
constructor(private cliPath: string) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run command with arguments
|
||||||
|
*/
|
||||||
|
run(args: string, options?: { cwd?: string; env?: Record<string, string> }): CLIResult {
|
||||||
|
return runCLI(this.cliPath, args, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run command and assert success
|
||||||
|
*/
|
||||||
|
assertSuccess(args: string, expectedOutput?: string): CLIResult {
|
||||||
|
const result = this.run(args);
|
||||||
|
assertSuccess(result, expectedOutput);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run command and assert failure
|
||||||
|
*/
|
||||||
|
assertFailure(args: string, expectedError?: string): CLIResult {
|
||||||
|
const result = this.run(args);
|
||||||
|
assertFailure(result, expectedError);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run command and parse JSON output
|
||||||
|
*/
|
||||||
|
runJSON<T = any>(args: string): T {
|
||||||
|
const result = this.run(args);
|
||||||
|
assertSuccess(result);
|
||||||
|
return parseJSONOutput<T>(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate JSON schema in CLI output
|
||||||
|
* @param result - CLI execution result
|
||||||
|
* @param schema - Expected schema object
|
||||||
|
*/
|
||||||
|
export function validateJSONSchema(result: CLIResult, schema: Record<string, string>): void {
|
||||||
|
const output = parseJSONOutput(result);
|
||||||
|
|
||||||
|
Object.entries(schema).forEach(([key, expectedType]) => {
|
||||||
|
if (!(key in output)) {
|
||||||
|
throw new Error(`Missing expected key in JSON output: ${key}`);
|
||||||
|
}
|
||||||
|
const actualType = typeof output[key];
|
||||||
|
if (actualType !== expectedType) {
|
||||||
|
throw new Error(`Expected type ${expectedType} for key ${key}, but got ${actualType}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare CLI output with snapshot
|
||||||
|
* @param result - CLI execution result
|
||||||
|
* @param snapshotPath - Path to snapshot file
|
||||||
|
* @param update - Whether to update snapshot
|
||||||
|
*/
|
||||||
|
export function compareSnapshot(result: CLIResult, snapshotPath: string, update: boolean = false): void {
|
||||||
|
if (update || !fs.existsSync(snapshotPath)) {
|
||||||
|
fs.writeFileSync(snapshotPath, result.stdout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = fs.readFileSync(snapshotPath, 'utf8');
|
||||||
|
if (result.stdout !== snapshot) {
|
||||||
|
throw new Error(`Output does not match snapshot\nExpected:\n${snapshot}\n\nActual:\n${result.stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
skills/click-patterns/SKILL.md
Normal file
126
skills/click-patterns/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: click-patterns
|
||||||
|
description: Click framework examples and templates - decorators, nested commands, parameter validation. Use when building Python CLI with Click, implementing command groups, adding CLI options/arguments, validating CLI parameters, creating nested subcommands, or when user mentions Click framework, @click decorators, command-line interface.
|
||||||
|
allowed-tools: Read, Write, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
# Click Framework Patterns
|
||||||
|
|
||||||
|
This skill provides comprehensive Click framework patterns, templates, and examples for building production-ready Python CLIs.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### When Building a Click CLI
|
||||||
|
|
||||||
|
1. Read the appropriate template based on complexity:
|
||||||
|
- Simple CLI: `templates/basic-cli.py`
|
||||||
|
- Nested commands: `templates/nested-commands.py`
|
||||||
|
- Custom validators: `templates/validators.py`
|
||||||
|
|
||||||
|
2. Generate new Click project:
|
||||||
|
```bash
|
||||||
|
bash scripts/generate-click-cli.sh <project-name> <cli-type>
|
||||||
|
```
|
||||||
|
Where cli-type is: basic, nested, or advanced
|
||||||
|
|
||||||
|
3. Study complete examples:
|
||||||
|
- `examples/complete-example.md` - Full-featured CLI
|
||||||
|
- `examples/patterns.md` - Common patterns and best practices
|
||||||
|
|
||||||
|
4. Validate your Click setup:
|
||||||
|
```bash
|
||||||
|
bash scripts/validate-click.sh <cli-file.py>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Click Patterns
|
||||||
|
|
||||||
|
**Command Groups:**
|
||||||
|
```python
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
"""Main CLI entry point"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def subcommand():
|
||||||
|
"""A subcommand"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options and Arguments:**
|
||||||
|
```python
|
||||||
|
@click.option('--template', '-t', default='basic', help='Template name')
|
||||||
|
@click.argument('environment')
|
||||||
|
def deploy(template, environment):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nested Groups:**
|
||||||
|
```python
|
||||||
|
@cli.group()
|
||||||
|
def config():
|
||||||
|
"""Configuration management"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
def get():
|
||||||
|
"""Get config value"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameter Validation:**
|
||||||
|
```python
|
||||||
|
@click.option('--mode', type=click.Choice(['fast', 'safe', 'rollback']))
|
||||||
|
@click.option('--count', type=click.IntRange(1, 100))
|
||||||
|
def command(mode, count):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Templates
|
||||||
|
|
||||||
|
1. **basic-cli.py** - Simple single-command CLI
|
||||||
|
2. **nested-commands.py** - Command groups and subcommands
|
||||||
|
3. **validators.py** - Custom parameter validators
|
||||||
|
4. **advanced-cli.py** - Advanced patterns with plugins and chaining
|
||||||
|
|
||||||
|
### Available Scripts
|
||||||
|
|
||||||
|
1. **generate-click-cli.sh** - Creates Click project structure
|
||||||
|
2. **validate-click.sh** - Validates Click CLI implementation
|
||||||
|
3. **setup-click-project.sh** - Setup dependencies and environment
|
||||||
|
|
||||||
|
### Available Examples
|
||||||
|
|
||||||
|
1. **complete-example.md** - Production-ready Click CLI
|
||||||
|
2. **patterns.md** - Best practices and common patterns
|
||||||
|
3. **edge-cases.md** - Edge cases and solutions
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Click 8.0+ (`pip install click`)
|
||||||
|
- Rich for colored output (`pip install rich`)
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use command groups** for organizing related commands
|
||||||
|
2. **Add help text** to all commands and options
|
||||||
|
3. **Validate parameters** using Click's built-in validators
|
||||||
|
4. **Use context** (@click.pass_context) for sharing state
|
||||||
|
5. **Handle errors gracefully** with try-except blocks
|
||||||
|
6. **Add version info** with @click.version_option()
|
||||||
|
7. **Use Rich** for beautiful colored output
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
- Building CLI tools with multiple commands
|
||||||
|
- Creating deployment scripts with options
|
||||||
|
- Implementing configuration management CLIs
|
||||||
|
- Building database migration tools
|
||||||
|
- Creating API testing CLIs
|
||||||
|
- Implementing project scaffolding tools
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Purpose:** Provide Click framework templates and patterns for Python CLI development
|
||||||
|
**Load when:** Building Click CLIs, implementing command groups, or validating CLI parameters
|
||||||
405
skills/click-patterns/examples/complete-example.md
Normal file
405
skills/click-patterns/examples/complete-example.md
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
# Complete Click CLI Example
|
||||||
|
|
||||||
|
A production-ready Click CLI demonstrating all major patterns and best practices.
|
||||||
|
|
||||||
|
## Full Implementation
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Production-ready Click CLI with all major patterns.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Command groups and nested subcommands
|
||||||
|
- Options and arguments with validation
|
||||||
|
- Context sharing across commands
|
||||||
|
- Error handling and colored output
|
||||||
|
- Configuration management
|
||||||
|
- Environment-specific commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
# Custom validators
|
||||||
|
def validate_email(ctx, param, value):
|
||||||
|
"""Validate email format"""
|
||||||
|
import re
|
||||||
|
if value and not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', value):
|
||||||
|
raise click.BadParameter('Invalid email format')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# Main CLI group
|
||||||
|
@click.group()
|
||||||
|
@click.version_option(version='1.0.0')
|
||||||
|
@click.option('--config', type=click.Path(), default='config.json',
|
||||||
|
help='Configuration file path')
|
||||||
|
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, config, verbose):
|
||||||
|
"""
|
||||||
|
A powerful CLI tool for project management.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
cli init --template basic
|
||||||
|
cli deploy production --mode safe
|
||||||
|
cli config get api-key
|
||||||
|
cli database migrate --create-tables
|
||||||
|
"""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj['console'] = console
|
||||||
|
ctx.obj['verbose'] = verbose
|
||||||
|
ctx.obj['config_file'] = config
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
console.print(f"[dim]Config file: {config}[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize command
|
||||||
|
@cli.command()
|
||||||
|
@click.option('--template', '-t',
|
||||||
|
type=click.Choice(['basic', 'advanced', 'minimal']),
|
||||||
|
default='basic',
|
||||||
|
help='Project template')
|
||||||
|
@click.option('--name', prompt=True, help='Project name')
|
||||||
|
@click.option('--description', prompt=True, help='Project description')
|
||||||
|
@click.pass_context
|
||||||
|
def init(ctx, template, name, description):
|
||||||
|
"""Initialize a new project"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
verbose = ctx.obj['verbose']
|
||||||
|
|
||||||
|
console.print(f"[cyan]Initializing project: {name}[/cyan]")
|
||||||
|
console.print(f"[dim]Template: {template}[/dim]")
|
||||||
|
console.print(f"[dim]Description: {description}[/dim]")
|
||||||
|
|
||||||
|
# Create project structure
|
||||||
|
project_dir = Path(name)
|
||||||
|
if project_dir.exists():
|
||||||
|
console.print(f"[red]✗[/red] Directory already exists: {name}")
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
try:
|
||||||
|
project_dir.mkdir(parents=True)
|
||||||
|
(project_dir / 'src').mkdir()
|
||||||
|
(project_dir / 'tests').mkdir()
|
||||||
|
(project_dir / 'docs').mkdir()
|
||||||
|
|
||||||
|
# Create config file
|
||||||
|
config = {
|
||||||
|
'name': name,
|
||||||
|
'description': description,
|
||||||
|
'template': template,
|
||||||
|
'version': '1.0.0'
|
||||||
|
}
|
||||||
|
with open(project_dir / 'config.json', 'w') as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
|
||||||
|
console.print(f"[green]✓[/green] Project initialized successfully!")
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
console.print(f"[dim]Created directories: src/, tests/, docs/[/dim]")
|
||||||
|
console.print(f"[dim]Created config.json[/dim]")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]✗[/red] Error: {e}")
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
|
||||||
|
# Deploy command
|
||||||
|
@cli.command()
|
||||||
|
@click.argument('environment',
|
||||||
|
type=click.Choice(['dev', 'staging', 'production']))
|
||||||
|
@click.option('--force', '-f', is_flag=True, help='Force deployment')
|
||||||
|
@click.option('--mode', '-m',
|
||||||
|
type=click.Choice(['fast', 'safe', 'rollback']),
|
||||||
|
default='safe',
|
||||||
|
help='Deployment mode')
|
||||||
|
@click.option('--skip-tests', is_flag=True, help='Skip test execution')
|
||||||
|
@click.pass_context
|
||||||
|
def deploy(ctx, environment, force, mode, skip_tests):
|
||||||
|
"""Deploy to specified environment"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
verbose = ctx.obj['verbose']
|
||||||
|
|
||||||
|
console.print(f"[cyan]Deploying to {environment} in {mode} mode[/cyan]")
|
||||||
|
|
||||||
|
if force:
|
||||||
|
console.print("[yellow]⚠ Force mode enabled - skipping safety checks[/yellow]")
|
||||||
|
|
||||||
|
# Pre-deployment checks
|
||||||
|
if not skip_tests and not force:
|
||||||
|
console.print("[dim]Running tests...[/dim]")
|
||||||
|
# Simulate test execution
|
||||||
|
if verbose:
|
||||||
|
console.print("[green]✓[/green] All tests passed")
|
||||||
|
|
||||||
|
# Deployment simulation
|
||||||
|
steps = [
|
||||||
|
"Building artifacts",
|
||||||
|
"Uploading to server",
|
||||||
|
"Running migrations",
|
||||||
|
"Restarting services",
|
||||||
|
"Verifying deployment"
|
||||||
|
]
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
|
console.print(f"[dim]- {step}...[/dim]")
|
||||||
|
|
||||||
|
console.print(f"[green]✓[/green] Deployment completed successfully!")
|
||||||
|
|
||||||
|
if mode == 'safe':
|
||||||
|
console.print("[dim]Rollback available for 24 hours[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
# Config group
|
||||||
|
@cli.group()
|
||||||
|
def config():
|
||||||
|
"""Manage configuration settings"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@click.pass_context
|
||||||
|
def get(ctx, key):
|
||||||
|
"""Get configuration value"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
config_file = ctx.obj['config_file']
|
||||||
|
|
||||||
|
try:
|
||||||
|
if Path(config_file).exists():
|
||||||
|
with open(config_file) as f:
|
||||||
|
config_data = json.load(f)
|
||||||
|
if key in config_data:
|
||||||
|
console.print(f"[dim]Config[/dim] {key}: [green]{config_data[key]}[/green]")
|
||||||
|
else:
|
||||||
|
console.print(f"[yellow]Key not found: {key}[/yellow]")
|
||||||
|
else:
|
||||||
|
console.print(f"[red]Config file not found: {config_file}[/red]")
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Error reading config: {e}[/red]")
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@click.argument('value')
|
||||||
|
@click.pass_context
|
||||||
|
def set(ctx, key, value):
|
||||||
|
"""Set configuration value"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
config_file = ctx.obj['config_file']
|
||||||
|
|
||||||
|
try:
|
||||||
|
config_data = {}
|
||||||
|
if Path(config_file).exists():
|
||||||
|
with open(config_file) as f:
|
||||||
|
config_data = json.load(f)
|
||||||
|
|
||||||
|
config_data[key] = value
|
||||||
|
|
||||||
|
with open(config_file, 'w') as f:
|
||||||
|
json.dump(config_data, f, indent=2)
|
||||||
|
|
||||||
|
console.print(f"[green]✓[/green] Set {key} = {value}")
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Error writing config: {e}[/red]")
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.pass_context
|
||||||
|
def list(ctx):
|
||||||
|
"""List all configuration settings"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
config_file = ctx.obj['config_file']
|
||||||
|
|
||||||
|
try:
|
||||||
|
if Path(config_file).exists():
|
||||||
|
with open(config_file) as f:
|
||||||
|
config_data = json.load(f)
|
||||||
|
console.print("[cyan]Configuration Settings:[/cyan]")
|
||||||
|
for key, value in config_data.items():
|
||||||
|
console.print(f" {key}: [green]{value}[/green]")
|
||||||
|
else:
|
||||||
|
console.print("[yellow]No configuration file found[/yellow]")
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Error reading config: {e}[/red]")
|
||||||
|
|
||||||
|
|
||||||
|
# Database group
|
||||||
|
@cli.group()
|
||||||
|
def database():
|
||||||
|
"""Database management commands"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@database.command()
|
||||||
|
@click.option('--create-tables', is_flag=True, help='Create tables')
|
||||||
|
@click.option('--seed-data', is_flag=True, help='Seed initial data')
|
||||||
|
@click.pass_context
|
||||||
|
def migrate(ctx, create_tables, seed_data):
|
||||||
|
"""Run database migrations"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
|
||||||
|
console.print("[cyan]Running migrations...[/cyan]")
|
||||||
|
|
||||||
|
if create_tables:
|
||||||
|
console.print("[dim]- Creating tables...[/dim]")
|
||||||
|
console.print("[green]✓[/green] Tables created")
|
||||||
|
|
||||||
|
if seed_data:
|
||||||
|
console.print("[dim]- Seeding data...[/dim]")
|
||||||
|
console.print("[green]✓[/green] Data seeded")
|
||||||
|
|
||||||
|
console.print("[green]✓[/green] Migrations completed")
|
||||||
|
|
||||||
|
|
||||||
|
@database.command()
|
||||||
|
@click.option('--confirm', is_flag=True,
|
||||||
|
prompt='This will delete all data. Continue?',
|
||||||
|
help='Confirm reset')
|
||||||
|
@click.pass_context
|
||||||
|
def reset(ctx, confirm):
|
||||||
|
"""Reset database (destructive operation)"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
|
||||||
|
if not confirm:
|
||||||
|
console.print("[yellow]Operation cancelled[/yellow]")
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
console.print("[red]Resetting database...[/red]")
|
||||||
|
console.print("[dim]- Dropping tables...[/dim]")
|
||||||
|
console.print("[dim]- Clearing cache...[/dim]")
|
||||||
|
console.print("[green]✓[/green] Database reset completed")
|
||||||
|
|
||||||
|
|
||||||
|
# User management group
|
||||||
|
@cli.group()
|
||||||
|
def user():
|
||||||
|
"""User management commands"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@user.command()
|
||||||
|
@click.option('--email', callback=validate_email, prompt=True,
|
||||||
|
help='User email address')
|
||||||
|
@click.option('--name', prompt=True, help='User full name')
|
||||||
|
@click.option('--role',
|
||||||
|
type=click.Choice(['admin', 'user', 'guest']),
|
||||||
|
default='user',
|
||||||
|
help='User role')
|
||||||
|
@click.pass_context
|
||||||
|
def create(ctx, email, name, role):
|
||||||
|
"""Create a new user"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
|
||||||
|
console.print(f"[cyan]Creating user: {name}[/cyan]")
|
||||||
|
console.print(f"[dim]Email: {email}[/dim]")
|
||||||
|
console.print(f"[dim]Role: {role}[/dim]")
|
||||||
|
|
||||||
|
console.print(f"[green]✓[/green] User created successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@user.command()
|
||||||
|
@click.argument('email')
|
||||||
|
@click.pass_context
|
||||||
|
def delete(ctx, email):
|
||||||
|
"""Delete a user"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
|
||||||
|
if not click.confirm(f"Delete user {email}?"):
|
||||||
|
console.print("[yellow]Operation cancelled[/yellow]")
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(f"[cyan]Deleting user: {email}[/cyan]")
|
||||||
|
console.print(f"[green]✓[/green] User deleted")
|
||||||
|
|
||||||
|
|
||||||
|
# Error handling wrapper
|
||||||
|
def main():
|
||||||
|
"""Main entry point with error handling"""
|
||||||
|
try:
|
||||||
|
cli(obj={})
|
||||||
|
except click.Abort:
|
||||||
|
console.print("[yellow]Operation aborted[/yellow]")
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Unexpected error: {e}[/red]")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Initialize a project
|
||||||
|
```bash
|
||||||
|
python cli.py init --template advanced --name myproject --description "My awesome project"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy to production
|
||||||
|
```bash
|
||||||
|
python cli.py deploy production --mode safe
|
||||||
|
python cli.py deploy staging --force --skip-tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration management
|
||||||
|
```bash
|
||||||
|
python cli.py config set api-key abc123
|
||||||
|
python cli.py config get api-key
|
||||||
|
python cli.py config list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database operations
|
||||||
|
```bash
|
||||||
|
python cli.py database migrate --create-tables --seed-data
|
||||||
|
python cli.py database reset --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
### User management
|
||||||
|
```bash
|
||||||
|
python cli.py user create --email user@example.com --name "John Doe" --role admin
|
||||||
|
python cli.py user delete user@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### With verbose output
|
||||||
|
```bash
|
||||||
|
python cli.py --verbose deploy production
|
||||||
|
```
|
||||||
|
|
||||||
|
### With custom config file
|
||||||
|
```bash
|
||||||
|
python cli.py --config /path/to/config.json config list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features Demonstrated
|
||||||
|
|
||||||
|
1. **Command Groups**: Organized commands into logical groups (config, database, user)
|
||||||
|
2. **Context Sharing**: Using @click.pass_context to share state
|
||||||
|
3. **Input Validation**: Custom validators for email, built-in validators for choices
|
||||||
|
4. **Colored Output**: Using Rich console for beautiful output
|
||||||
|
5. **Error Handling**: Graceful error handling and user feedback
|
||||||
|
6. **Interactive Prompts**: Using prompt=True for interactive input
|
||||||
|
7. **Confirmation Dialogs**: Using click.confirm() for dangerous operations
|
||||||
|
8. **File Operations**: Reading/writing JSON configuration files
|
||||||
|
9. **Flags and Options**: Boolean flags, default values, short flags
|
||||||
|
10. **Version Information**: @click.version_option() decorator
|
||||||
|
|
||||||
|
## Best Practices Applied
|
||||||
|
|
||||||
|
- Clear help text for all commands and options
|
||||||
|
- Sensible defaults for options
|
||||||
|
- Validation for user inputs
|
||||||
|
- Colored output for better UX
|
||||||
|
- Verbose mode for debugging
|
||||||
|
- Confirmation for destructive operations
|
||||||
|
- Proper error handling and messages
|
||||||
|
- Clean separation of concerns with command groups
|
||||||
|
- Context object for sharing state
|
||||||
482
skills/click-patterns/examples/edge-cases.md
Normal file
482
skills/click-patterns/examples/edge-cases.md
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
# Click Framework Edge Cases and Solutions
|
||||||
|
|
||||||
|
Common edge cases, gotchas, and their solutions when working with Click.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Parameter Handling Edge Cases](#parameter-handling-edge-cases)
|
||||||
|
2. [Context and State Edge Cases](#context-and-state-edge-cases)
|
||||||
|
3. [Error Handling Edge Cases](#error-handling-edge-cases)
|
||||||
|
4. [Testing Edge Cases](#testing-edge-cases)
|
||||||
|
5. [Platform-Specific Edge Cases](#platform-specific-edge-cases)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parameter Handling Edge Cases
|
||||||
|
|
||||||
|
### Case 1: Multiple Values with Same Option
|
||||||
|
|
||||||
|
**Problem**: User specifies the same option multiple times
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cli --tag python --tag docker --tag kubernetes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Use `multiple=True`
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.option('--tag', multiple=True)
|
||||||
|
def command(tag):
|
||||||
|
"""Handle multiple values"""
|
||||||
|
for t in tag:
|
||||||
|
click.echo(t)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 2: Option vs Argument Ambiguity
|
||||||
|
|
||||||
|
**Problem**: Argument that looks like an option
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cli process --file=-myfile.txt # -myfile.txt looks like option
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Use `--` separator or quotes
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.argument('filename')
|
||||||
|
def process(filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# cli process -- -myfile.txt
|
||||||
|
# cli process "-myfile.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 3: Empty String vs None
|
||||||
|
|
||||||
|
**Problem**: Distinguishing between no value and empty string
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.option('--name')
|
||||||
|
def command(name):
|
||||||
|
# name is None when not provided
|
||||||
|
# name is '' when provided as empty
|
||||||
|
if name is None:
|
||||||
|
click.echo('Not provided')
|
||||||
|
elif name == '':
|
||||||
|
click.echo('Empty string provided')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Use callback for custom handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handle_empty(ctx, param, value):
|
||||||
|
if value == '':
|
||||||
|
return None # Treat empty as None
|
||||||
|
return value
|
||||||
|
|
||||||
|
@click.option('--name', callback=handle_empty)
|
||||||
|
def command(name):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 4: Boolean Flag with Default True
|
||||||
|
|
||||||
|
**Problem**: Need a flag that's True by default, but can be disabled
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Wrong approach:
|
||||||
|
@click.option('--enable', is_flag=True, default=True) # Doesn't work as expected
|
||||||
|
|
||||||
|
# Correct approach:
|
||||||
|
@click.option('--disable', is_flag=True)
|
||||||
|
def command(disable):
|
||||||
|
enabled = not disable
|
||||||
|
```
|
||||||
|
|
||||||
|
**Better Solution**: Use flag_value
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.option('--ssl/--no-ssl', default=True)
|
||||||
|
def command(ssl):
|
||||||
|
"""SSL is enabled by default, use --no-ssl to disable"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 5: Required Option with Environment Variable
|
||||||
|
|
||||||
|
**Problem**: Make option required unless env var is set
|
||||||
|
|
||||||
|
```python
|
||||||
|
def require_if_no_env(ctx, param, value):
|
||||||
|
"""Require option if environment variable not set"""
|
||||||
|
if value is None:
|
||||||
|
import os
|
||||||
|
env_value = os.getenv('API_KEY')
|
||||||
|
if env_value:
|
||||||
|
return env_value
|
||||||
|
raise click.MissingParameter(param=param)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@click.option('--api-key', callback=require_if_no_env)
|
||||||
|
def command(api_key):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context and State Edge Cases
|
||||||
|
|
||||||
|
### Case 6: Context Not Available in Callbacks
|
||||||
|
|
||||||
|
**Problem**: Need context in parameter callback
|
||||||
|
|
||||||
|
```python
|
||||||
|
# This doesn't work - context not yet initialized:
|
||||||
|
def my_callback(ctx, param, value):
|
||||||
|
config = ctx.obj['config'] # Error: ctx.obj is None
|
||||||
|
return value
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Use command decorator to set up context first
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj['config'] = load_config()
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option('--value', callback=validate_with_config)
|
||||||
|
@click.pass_context
|
||||||
|
def subcommand(ctx, value):
|
||||||
|
# Now ctx.obj is available
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 7: Sharing State Between Command Groups
|
||||||
|
|
||||||
|
**Problem**: State not persisting across nested groups
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.group()
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj['data'] = 'test'
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
@click.pass_context
|
||||||
|
def subgroup(ctx):
|
||||||
|
# ctx.obj is still available here
|
||||||
|
assert ctx.obj['data'] == 'test'
|
||||||
|
|
||||||
|
@subgroup.command()
|
||||||
|
@click.pass_context
|
||||||
|
def command(ctx):
|
||||||
|
# ctx.obj is still available here too
|
||||||
|
assert ctx.obj['data'] == 'test'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 8: Mutating Context Objects
|
||||||
|
|
||||||
|
**Problem**: Changes to context not persisting
|
||||||
|
|
||||||
|
```python
|
||||||
|
# This works:
|
||||||
|
ctx.obj['key'] = 'value' # Modifying dict
|
||||||
|
|
||||||
|
# This doesn't persist:
|
||||||
|
ctx.obj = {'key': 'value'} # Replacing dict
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling Edge Cases
|
||||||
|
|
||||||
|
### Case 9: Graceful Handling of Ctrl+C
|
||||||
|
|
||||||
|
**Problem**: Ugly traceback on keyboard interrupt
|
||||||
|
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
cli()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
click.echo('\n\nOperation cancelled by user')
|
||||||
|
raise SystemExit(130) # Standard exit code for Ctrl+C
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 10: Custom Error Messages for Validation
|
||||||
|
|
||||||
|
**Problem**: Default error messages aren't user-friendly
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Default error:
|
||||||
|
@click.option('--port', type=click.IntRange(1, 65535))
|
||||||
|
# Error: Invalid value for '--port': 70000 is not in the range 1<=x<=65535
|
||||||
|
|
||||||
|
# Custom error:
|
||||||
|
def validate_port(ctx, param, value):
|
||||||
|
if not 1 <= value <= 65535:
|
||||||
|
raise click.BadParameter(
|
||||||
|
f'Port {value} is out of range. Please use a port between 1 and 65535.'
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@click.option('--port', type=int, callback=validate_port)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 11: Handling Mutually Exclusive Options
|
||||||
|
|
||||||
|
**Problem**: Options that can't be used together
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_exclusive(ctx, param, value):
|
||||||
|
"""Ensure mutually exclusive options"""
|
||||||
|
if value and ctx.params.get('other_option'):
|
||||||
|
raise click.UsageError(
|
||||||
|
'Cannot use --option and --other-option together'
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option('--option', callback=validate_exclusive)
|
||||||
|
@click.option('--other-option')
|
||||||
|
def command(option, other_option):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 12: Dependent Options
|
||||||
|
|
||||||
|
**Problem**: One option requires another
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.option('--ssl', is_flag=True)
|
||||||
|
@click.option('--cert', type=click.Path(exists=True))
|
||||||
|
@click.option('--key', type=click.Path(exists=True))
|
||||||
|
def server(ssl, cert, key):
|
||||||
|
"""Validate dependent options"""
|
||||||
|
if ssl:
|
||||||
|
if not cert or not key:
|
||||||
|
raise click.UsageError(
|
||||||
|
'--ssl requires both --cert and --key'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Edge Cases
|
||||||
|
|
||||||
|
### Case 13: Testing with Environment Variables
|
||||||
|
|
||||||
|
**Problem**: Tests failing due to environment pollution
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_with_clean_env():
|
||||||
|
"""Test with isolated environment"""
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
# This isolates environment variables:
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['command'],
|
||||||
|
env={'API_KEY': 'test'},
|
||||||
|
catch_exceptions=False
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 14: Testing Interactive Prompts with Validation
|
||||||
|
|
||||||
|
**Problem**: Prompts with retry logic
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_interactive_retry():
|
||||||
|
"""Test prompt with retry on invalid input"""
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
# Provide multiple inputs (first invalid, second valid)
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
['create'],
|
||||||
|
input='invalid-email\nvalid@email.com\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'Invalid email' in result.output
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 15: Testing File Operations
|
||||||
|
|
||||||
|
**Problem**: Tests creating actual files
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_file_operations():
|
||||||
|
"""Test with isolated filesystem"""
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# Create test file
|
||||||
|
with open('input.txt', 'w') as f:
|
||||||
|
f.write('test data')
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ['process', 'input.txt'])
|
||||||
|
|
||||||
|
# Verify output file
|
||||||
|
assert Path('output.txt').exists()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Platform-Specific Edge Cases
|
||||||
|
|
||||||
|
### Case 16: Windows Path Handling
|
||||||
|
|
||||||
|
**Problem**: Backslashes in Windows paths
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.option('--path', type=click.Path())
|
||||||
|
def command(path):
|
||||||
|
# Use pathlib for cross-platform compatibility
|
||||||
|
from pathlib import Path
|
||||||
|
p = Path(path) # Handles Windows/Unix paths
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 17: Unicode in Command Line Arguments
|
||||||
|
|
||||||
|
**Problem**: Non-ASCII characters in arguments
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.argument('name')
|
||||||
|
def greet(name):
|
||||||
|
"""Handle unicode properly"""
|
||||||
|
# Click handles unicode automatically on Python 3
|
||||||
|
click.echo(f'Hello, {name}!')
|
||||||
|
|
||||||
|
# This works:
|
||||||
|
# cli greet "José"
|
||||||
|
# cli greet "北京"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 18: Terminal Width Detection
|
||||||
|
|
||||||
|
**Problem**: Output formatting for different terminal sizes
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
def status():
|
||||||
|
"""Adapt to terminal width"""
|
||||||
|
terminal_width = click.get_terminal_size()[0]
|
||||||
|
|
||||||
|
if terminal_width < 80:
|
||||||
|
# Compact output for narrow terminals
|
||||||
|
click.echo('Status: OK')
|
||||||
|
else:
|
||||||
|
# Detailed output for wide terminals
|
||||||
|
click.echo('=' * terminal_width)
|
||||||
|
click.echo('Detailed Status Information')
|
||||||
|
click.echo('=' * terminal_width)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Edge Cases
|
||||||
|
|
||||||
|
### Case 19: Dynamic Command Registration
|
||||||
|
|
||||||
|
**Problem**: Register commands at runtime
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DynamicGroup(click.Group):
|
||||||
|
"""Group that discovers commands dynamically"""
|
||||||
|
|
||||||
|
def list_commands(self, ctx):
|
||||||
|
"""List available commands"""
|
||||||
|
# Dynamically discover commands
|
||||||
|
return ['cmd1', 'cmd2', 'cmd3']
|
||||||
|
|
||||||
|
def get_command(self, ctx, name):
|
||||||
|
"""Load command on demand"""
|
||||||
|
if name in self.list_commands(ctx):
|
||||||
|
# Import and return command
|
||||||
|
module = __import__(f'commands.{name}')
|
||||||
|
return getattr(module, name).cli
|
||||||
|
return None
|
||||||
|
|
||||||
|
@click.command(cls=DynamicGroup)
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 20: Command Aliases
|
||||||
|
|
||||||
|
**Problem**: Support command aliases
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AliasedGroup(click.Group):
|
||||||
|
"""Group that supports command aliases"""
|
||||||
|
|
||||||
|
def get_command(self, ctx, cmd_name):
|
||||||
|
"""Resolve aliases"""
|
||||||
|
aliases = {
|
||||||
|
'ls': 'list',
|
||||||
|
'rm': 'remove',
|
||||||
|
'cp': 'copy'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve alias
|
||||||
|
resolved = aliases.get(cmd_name, cmd_name)
|
||||||
|
return super().get_command(ctx, resolved)
|
||||||
|
|
||||||
|
@click.group(cls=AliasedGroup)
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def list():
|
||||||
|
"""List items (alias: ls)"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Case 21: Progress Bar with Unknown Length
|
||||||
|
|
||||||
|
**Problem**: Show progress when total is unknown
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
def process():
|
||||||
|
"""Process with indeterminate progress"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
# For unknown length, use length=None
|
||||||
|
with click.progressbar(
|
||||||
|
range(100),
|
||||||
|
length=None,
|
||||||
|
label='Processing'
|
||||||
|
) as bar:
|
||||||
|
for _ in bar:
|
||||||
|
time.sleep(0.1)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Key takeaways for handling edge cases:
|
||||||
|
|
||||||
|
1. **Parameters**: Use callbacks and custom types for complex validation
|
||||||
|
2. **Context**: Ensure context is initialized before accessing ctx.obj
|
||||||
|
3. **Errors**: Provide clear, actionable error messages
|
||||||
|
4. **Testing**: Use CliRunner's isolation features
|
||||||
|
5. **Platform**: Use pathlib and Click's built-in utilities for portability
|
||||||
|
|
||||||
|
For more edge cases, consult the [Click documentation](https://click.palletsprojects.com/) and [GitHub issues](https://github.com/pallets/click/issues).
|
||||||
521
skills/click-patterns/examples/patterns.md
Normal file
521
skills/click-patterns/examples/patterns.md
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
# Click Framework Common Patterns
|
||||||
|
|
||||||
|
Best practices and common patterns for building production-ready Click CLIs.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Command Structure Patterns](#command-structure-patterns)
|
||||||
|
2. [Parameter Patterns](#parameter-patterns)
|
||||||
|
3. [Validation Patterns](#validation-patterns)
|
||||||
|
4. [Error Handling Patterns](#error-handling-patterns)
|
||||||
|
5. [Output Patterns](#output-patterns)
|
||||||
|
6. [Configuration Patterns](#configuration-patterns)
|
||||||
|
7. [Testing Patterns](#testing-patterns)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Command Structure Patterns
|
||||||
|
|
||||||
|
### Single Command CLI
|
||||||
|
|
||||||
|
For simple tools with one main function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.option('--name', default='World')
|
||||||
|
def hello(name):
|
||||||
|
"""Simple greeting CLI"""
|
||||||
|
click.echo(f'Hello, {name}!')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Group Pattern
|
||||||
|
|
||||||
|
For CLIs with multiple related commands:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
"""Main CLI entry point"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def cmd1():
|
||||||
|
"""First command"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def cmd2():
|
||||||
|
"""Second command"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Command Groups
|
||||||
|
|
||||||
|
For complex CLIs with logical grouping:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
"""Main CLI"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
def database():
|
||||||
|
"""Database commands"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@database.command()
|
||||||
|
def migrate():
|
||||||
|
"""Run migrations"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@database.command()
|
||||||
|
def reset():
|
||||||
|
"""Reset database"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context-Aware Commands
|
||||||
|
|
||||||
|
Share state across commands:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.group()
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
"""Main CLI with shared context"""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj['config'] = load_config()
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
def deploy(ctx):
|
||||||
|
"""Use shared config"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parameter Patterns
|
||||||
|
|
||||||
|
### Options vs Arguments
|
||||||
|
|
||||||
|
**Options** (optional, named):
|
||||||
|
```python
|
||||||
|
@click.option('--name', '-n', default='World', help='Name to greet')
|
||||||
|
@click.option('--count', '-c', default=1, type=int)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments** (required, positional):
|
||||||
|
```python
|
||||||
|
@click.argument('filename')
|
||||||
|
@click.argument('output', type=click.Path())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Options
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.option('--api-key', required=True, help='API key (required)')
|
||||||
|
@click.option('--config', required=True, type=click.Path(exists=True))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Values
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Multiple option values
|
||||||
|
@click.option('--tag', multiple=True, help='Tags (can specify multiple times)')
|
||||||
|
def command(tag):
|
||||||
|
for t in tag:
|
||||||
|
click.echo(t)
|
||||||
|
|
||||||
|
# Variable arguments
|
||||||
|
@click.argument('files', nargs=-1, type=click.Path(exists=True))
|
||||||
|
def process(files):
|
||||||
|
for f in files:
|
||||||
|
click.echo(f)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.option('--api-key', envvar='API_KEY', help='API key (from env: API_KEY)')
|
||||||
|
@click.option('--debug', envvar='DEBUG', is_flag=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive Prompts
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.option('--name', prompt=True, help='Your name')
|
||||||
|
@click.option('--password', prompt=True, hide_input=True)
|
||||||
|
@click.option('--confirm', prompt='Continue?', confirmation_prompt=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Patterns
|
||||||
|
|
||||||
|
### Built-in Validators
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Choice validation
|
||||||
|
@click.option('--env', type=click.Choice(['dev', 'staging', 'prod']))
|
||||||
|
|
||||||
|
# Range validation
|
||||||
|
@click.option('--port', type=click.IntRange(1, 65535))
|
||||||
|
@click.option('--rate', type=click.FloatRange(0.0, 1.0))
|
||||||
|
|
||||||
|
# Path validation
|
||||||
|
@click.option('--input', type=click.Path(exists=True, dir_okay=False))
|
||||||
|
@click.option('--output', type=click.Path(writable=True))
|
||||||
|
@click.option('--dir', type=click.Path(exists=True, file_okay=False))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Validators with Callbacks
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_email(ctx, param, value):
|
||||||
|
"""Validate email format"""
|
||||||
|
import re
|
||||||
|
if value and not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', value):
|
||||||
|
raise click.BadParameter('Invalid email format')
|
||||||
|
return value
|
||||||
|
|
||||||
|
@click.option('--email', callback=validate_email)
|
||||||
|
def command(email):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Click Types
|
||||||
|
|
||||||
|
```python
|
||||||
|
class EmailType(click.ParamType):
|
||||||
|
"""Custom email type"""
|
||||||
|
name = 'email'
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
import re
|
||||||
|
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', value):
|
||||||
|
self.fail(f'{value} is not a valid email', param, ctx)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@click.option('--email', type=EmailType())
|
||||||
|
def command(email):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.option('--ssl', is_flag=True)
|
||||||
|
@click.option('--cert', type=click.Path(exists=True))
|
||||||
|
@click.option('--key', type=click.Path(exists=True))
|
||||||
|
def server(ssl, cert, key):
|
||||||
|
"""Start server with SSL validation"""
|
||||||
|
if ssl and (not cert or not key):
|
||||||
|
raise click.UsageError('SSL requires --cert and --key')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling Patterns
|
||||||
|
|
||||||
|
### Graceful Error Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
def command():
|
||||||
|
"""Command with error handling"""
|
||||||
|
try:
|
||||||
|
# Operation that might fail
|
||||||
|
result = risky_operation()
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise click.FileError(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise click.ClickException(f'Operation failed: {e}')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Exit Codes
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
def deploy():
|
||||||
|
"""Deploy with custom exit codes"""
|
||||||
|
if not check_prerequisites():
|
||||||
|
ctx = click.get_current_context()
|
||||||
|
ctx.exit(1)
|
||||||
|
|
||||||
|
if not deploy_application():
|
||||||
|
ctx = click.get_current_context()
|
||||||
|
ctx.exit(2)
|
||||||
|
|
||||||
|
click.echo('Deployment successful')
|
||||||
|
ctx = click.get_current_context()
|
||||||
|
ctx.exit(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Confirmation for Dangerous Operations
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.option('--force', is_flag=True, help='Skip confirmation')
|
||||||
|
def delete(force):
|
||||||
|
"""Delete with confirmation"""
|
||||||
|
if not force:
|
||||||
|
click.confirm('This will delete all data. Continue?', abort=True)
|
||||||
|
|
||||||
|
# Proceed with deletion
|
||||||
|
click.echo('Deleting...')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Patterns
|
||||||
|
|
||||||
|
### Colored Output with Click
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
def status():
|
||||||
|
"""Show status with colors"""
|
||||||
|
click.secho('Success!', fg='green', bold=True)
|
||||||
|
click.secho('Warning!', fg='yellow')
|
||||||
|
click.secho('Error!', fg='red', bold=True)
|
||||||
|
click.echo(click.style('Info', fg='cyan'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rich Console Integration
|
||||||
|
|
||||||
|
```python
|
||||||
|
from rich.console import Console
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def deploy():
|
||||||
|
"""Deploy with Rich output"""
|
||||||
|
console.print('[cyan]Starting deployment...[/cyan]')
|
||||||
|
console.print('[green]✓[/green] Build successful')
|
||||||
|
console.print('[yellow]⚠[/yellow] Warning: Cache cleared')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Progress Bars
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.argument('files', nargs=-1)
|
||||||
|
def process(files):
|
||||||
|
"""Process with progress bar"""
|
||||||
|
with click.progressbar(files, label='Processing files') as bar:
|
||||||
|
for file in bar:
|
||||||
|
# Process file
|
||||||
|
time.sleep(0.1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verbose Mode Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.option('--verbose', '-v', is_flag=True)
|
||||||
|
def command(verbose):
|
||||||
|
"""Command with verbose output"""
|
||||||
|
click.echo('Starting operation...')
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
click.echo('Debug: Loading configuration')
|
||||||
|
click.echo('Debug: Connecting to database')
|
||||||
|
|
||||||
|
# Main operation
|
||||||
|
click.echo('Operation completed')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Patterns
|
||||||
|
|
||||||
|
### Configuration File Loading
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option('--config', type=click.Path(), default='config.json')
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, config):
|
||||||
|
"""CLI with config file"""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
if Path(config).exists():
|
||||||
|
with open(config) as f:
|
||||||
|
ctx.obj['config'] = json.load(f)
|
||||||
|
else:
|
||||||
|
ctx.obj['config'] = {}
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
def deploy(ctx):
|
||||||
|
"""Use config"""
|
||||||
|
config = ctx.obj['config']
|
||||||
|
api_key = config.get('api_key')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment-Based Configuration
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.command()
|
||||||
|
@click.option('--env', type=click.Choice(['dev', 'staging', 'prod']), default='dev')
|
||||||
|
def deploy(env):
|
||||||
|
"""Deploy with environment config"""
|
||||||
|
config_file = f'config.{env}.json'
|
||||||
|
with open(config_file) as f:
|
||||||
|
config = json.load(f)
|
||||||
|
# Use environment-specific config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Priority
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_config_value(ctx, param_value, env_var, config_key, default):
|
||||||
|
"""Get value with priority: param > env > config > default"""
|
||||||
|
if param_value:
|
||||||
|
return param_value
|
||||||
|
if env_var in os.environ:
|
||||||
|
return os.environ[env_var]
|
||||||
|
if config_key in ctx.obj['config']:
|
||||||
|
return ctx.obj['config'][config_key]
|
||||||
|
return default
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
### Basic Testing with CliRunner
|
||||||
|
|
||||||
|
```python
|
||||||
|
from click.testing import CliRunner
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_command():
|
||||||
|
"""Test Click command"""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ['--help'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Usage:' in result.output
|
||||||
|
|
||||||
|
def test_command_with_args():
|
||||||
|
"""Test with arguments"""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ['deploy', 'production'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Deploying to production' in result.output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing with Temporary Files
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_with_file():
|
||||||
|
"""Test with temporary file"""
|
||||||
|
runner = CliRunner()
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
with open('test.txt', 'w') as f:
|
||||||
|
f.write('test content')
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ['process', 'test.txt'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Interactive Prompts
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_interactive():
|
||||||
|
"""Test interactive prompts"""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ['create'], input='username\npassword\n')
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'User created' in result.output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Environment Variables
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_with_env():
|
||||||
|
"""Test with environment variables"""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ['deploy'], env={'API_KEY': 'test123'})
|
||||||
|
assert result.exit_code == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### Plugin System
|
||||||
|
|
||||||
|
```python
|
||||||
|
@click.group()
|
||||||
|
def cli():
|
||||||
|
"""CLI with plugin support"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Allow plugins to register commands
|
||||||
|
def register_plugin(group, plugin_name):
|
||||||
|
"""Register plugin commands"""
|
||||||
|
plugin_module = importlib.import_module(f'plugins.{plugin_name}')
|
||||||
|
for name, cmd in plugin_module.commands.items():
|
||||||
|
group.add_command(cmd, name)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Loading
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LazyGroup(click.Group):
|
||||||
|
"""Lazy load commands"""
|
||||||
|
|
||||||
|
def get_command(self, ctx, cmd_name):
|
||||||
|
"""Load command on demand"""
|
||||||
|
module = importlib.import_module(f'commands.{cmd_name}')
|
||||||
|
return module.cli
|
||||||
|
|
||||||
|
@click.command(cls=LazyGroup)
|
||||||
|
def cli():
|
||||||
|
"""CLI with lazy loading"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Middleware Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
def with_database(f):
|
||||||
|
"""Decorator to inject database connection"""
|
||||||
|
@click.pass_context
|
||||||
|
def wrapper(ctx, *args, **kwargs):
|
||||||
|
ctx.obj['db'] = connect_database()
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
ctx.obj['db'].close()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@with_database
|
||||||
|
@click.pass_context
|
||||||
|
def query(ctx):
|
||||||
|
"""Command with database"""
|
||||||
|
db = ctx.obj['db']
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
These patterns cover the most common use cases for Click CLIs:
|
||||||
|
|
||||||
|
1. **Structure**: Choose between single command, command group, or nested groups
|
||||||
|
2. **Parameters**: Use options for named parameters, arguments for positional
|
||||||
|
3. **Validation**: Leverage built-in validators or create custom ones
|
||||||
|
4. **Errors**: Handle errors gracefully with proper messages
|
||||||
|
5. **Output**: Use colored output and progress bars for better UX
|
||||||
|
6. **Config**: Load configuration from files with proper priority
|
||||||
|
7. **Testing**: Test thoroughly with CliRunner
|
||||||
|
|
||||||
|
For more patterns and advanced usage, see the [Click documentation](https://click.palletsprojects.com/).
|
||||||
334
skills/click-patterns/scripts/generate-click-cli.sh
Executable file
334
skills/click-patterns/scripts/generate-click-cli.sh
Executable file
@@ -0,0 +1,334 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# generate-click-cli.sh - Generate Click CLI project structure
|
||||||
|
#
|
||||||
|
# Usage: generate-click-cli.sh <project-name> [cli-type]
|
||||||
|
# cli-type: basic, nested, or advanced (default: basic)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_info() { echo -e "${CYAN}ℹ${NC} $1"; }
|
||||||
|
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||||
|
print_error() { echo -e "${RED}✗${NC} $1" >&2; }
|
||||||
|
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||||
|
|
||||||
|
# Validate arguments
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
print_error "Usage: $0 <project-name> [cli-type]"
|
||||||
|
echo " cli-type: basic, nested, or advanced (default: basic)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_NAME="$1"
|
||||||
|
CLI_TYPE="${2:-basic}"
|
||||||
|
|
||||||
|
# Validate CLI type
|
||||||
|
if [[ ! "$CLI_TYPE" =~ ^(basic|nested|advanced)$ ]]; then
|
||||||
|
print_error "Invalid CLI type: $CLI_TYPE"
|
||||||
|
echo " Valid types: basic, nested, advanced"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate project name
|
||||||
|
if [[ ! "$PROJECT_NAME" =~ ^[a-z0-9_-]+$ ]]; then
|
||||||
|
print_error "Invalid project name: $PROJECT_NAME"
|
||||||
|
echo " Must contain only lowercase letters, numbers, hyphens, and underscores"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create project directory
|
||||||
|
if [ -d "$PROJECT_NAME" ]; then
|
||||||
|
print_error "Directory already exists: $PROJECT_NAME"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_info "Creating Click CLI project: $PROJECT_NAME (type: $CLI_TYPE)"
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
mkdir -p "$PROJECT_NAME"/{src,tests,docs}
|
||||||
|
|
||||||
|
# Determine which template to use
|
||||||
|
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
TEMPLATE_FILE=""
|
||||||
|
|
||||||
|
case "$CLI_TYPE" in
|
||||||
|
basic)
|
||||||
|
TEMPLATE_FILE="$SKILL_DIR/templates/basic-cli.py"
|
||||||
|
;;
|
||||||
|
nested)
|
||||||
|
TEMPLATE_FILE="$SKILL_DIR/templates/nested-commands.py"
|
||||||
|
;;
|
||||||
|
advanced)
|
||||||
|
# For advanced, use nested as base with validators
|
||||||
|
TEMPLATE_FILE="$SKILL_DIR/templates/nested-commands.py"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Copy template
|
||||||
|
if [ ! -f "$TEMPLATE_FILE" ]; then
|
||||||
|
print_error "Template file not found: $TEMPLATE_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$TEMPLATE_FILE" "$PROJECT_NAME/src/cli.py"
|
||||||
|
print_success "Created src/cli.py from template"
|
||||||
|
|
||||||
|
# Copy validators if advanced type
|
||||||
|
if [ "$CLI_TYPE" = "advanced" ]; then
|
||||||
|
VALIDATORS_FILE="$SKILL_DIR/templates/validators.py"
|
||||||
|
if [ -f "$VALIDATORS_FILE" ]; then
|
||||||
|
cp "$VALIDATORS_FILE" "$PROJECT_NAME/src/validators.py"
|
||||||
|
print_success "Created src/validators.py"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create __init__.py
|
||||||
|
cat > "$PROJECT_NAME/src/__init__.py" <<'EOF'
|
||||||
|
"""
|
||||||
|
CLI application package
|
||||||
|
"""
|
||||||
|
from .cli import cli
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__all__ = ["cli"]
|
||||||
|
EOF
|
||||||
|
print_success "Created src/__init__.py"
|
||||||
|
|
||||||
|
# Create requirements.txt
|
||||||
|
cat > "$PROJECT_NAME/requirements.txt" <<'EOF'
|
||||||
|
click>=8.0.0
|
||||||
|
rich>=13.0.0
|
||||||
|
EOF
|
||||||
|
print_success "Created requirements.txt"
|
||||||
|
|
||||||
|
# Create setup.py
|
||||||
|
cat > "$PROJECT_NAME/setup.py" <<EOF
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="${PROJECT_NAME}",
|
||||||
|
version="1.0.0",
|
||||||
|
packages=find_packages(),
|
||||||
|
install_requires=[
|
||||||
|
"click>=8.0.0",
|
||||||
|
"rich>=13.0.0",
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"${PROJECT_NAME}=src.cli:cli",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
python_requires=">=3.8",
|
||||||
|
)
|
||||||
|
EOF
|
||||||
|
print_success "Created setup.py"
|
||||||
|
|
||||||
|
# Create pyproject.toml
|
||||||
|
cat > "$PROJECT_NAME/pyproject.toml" <<EOF
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=45", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "${PROJECT_NAME}"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A Click-based CLI tool"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [
|
||||||
|
"click>=8.0.0",
|
||||||
|
"rich>=13.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
${PROJECT_NAME} = "src.cli:cli"
|
||||||
|
EOF
|
||||||
|
print_success "Created pyproject.toml"
|
||||||
|
|
||||||
|
# Create README.md
|
||||||
|
cat > "$PROJECT_NAME/README.md" <<EOF
|
||||||
|
# ${PROJECT_NAME}
|
||||||
|
|
||||||
|
A CLI tool built with Click framework.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
pip install -e .
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Show help
|
||||||
|
${PROJECT_NAME} --help
|
||||||
|
|
||||||
|
# Run command
|
||||||
|
${PROJECT_NAME} <command>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Install in development mode
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pytest tests/
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
black src/ tests/
|
||||||
|
|
||||||
|
# Lint code
|
||||||
|
pylint src/ tests/
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
${PROJECT_NAME}/
|
||||||
|
├── src/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── cli.py # Main CLI implementation
|
||||||
|
├── tests/
|
||||||
|
│ └── test_cli.py # Unit tests
|
||||||
|
├── docs/
|
||||||
|
│ └── usage.md # Usage documentation
|
||||||
|
├── requirements.txt # Dependencies
|
||||||
|
├── setup.py # Setup configuration
|
||||||
|
└── README.md # This file
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
EOF
|
||||||
|
print_success "Created README.md"
|
||||||
|
|
||||||
|
# Create basic test file
|
||||||
|
cat > "$PROJECT_NAME/tests/test_cli.py" <<'EOF'
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from src.cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_help():
|
||||||
|
"""Test CLI help output"""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ['--help'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Usage:' in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_version():
|
||||||
|
"""Test CLI version output"""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ['--version'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert '1.0.0' in result.output
|
||||||
|
EOF
|
||||||
|
print_success "Created tests/test_cli.py"
|
||||||
|
|
||||||
|
# Create .gitignore
|
||||||
|
cat > "$PROJECT_NAME/.gitignore" <<'EOF'
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
EOF
|
||||||
|
print_success "Created .gitignore"
|
||||||
|
|
||||||
|
# Create usage documentation
|
||||||
|
cat > "$PROJECT_NAME/docs/usage.md" <<EOF
|
||||||
|
# ${PROJECT_NAME} Usage Guide
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install the CLI tool:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
pip install -e .
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Help
|
||||||
|
|
||||||
|
Show available commands:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
${PROJECT_NAME} --help
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Version
|
||||||
|
|
||||||
|
Show version information:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
${PROJECT_NAME} --version
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Add specific examples for your CLI commands here.
|
||||||
|
EOF
|
||||||
|
print_success "Created docs/usage.md"
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
echo ""
|
||||||
|
print_success "Click CLI project created successfully!"
|
||||||
|
echo ""
|
||||||
|
print_info "Next steps:"
|
||||||
|
echo " 1. cd $PROJECT_NAME"
|
||||||
|
echo " 2. python -m venv venv"
|
||||||
|
echo " 3. source venv/bin/activate"
|
||||||
|
echo " 4. pip install -e ."
|
||||||
|
echo " 5. $PROJECT_NAME --help"
|
||||||
|
echo ""
|
||||||
|
print_info "Project type: $CLI_TYPE"
|
||||||
|
print_info "Location: $(pwd)/$PROJECT_NAME"
|
||||||
108
skills/click-patterns/scripts/setup-click-project.sh
Executable file
108
skills/click-patterns/scripts/setup-click-project.sh
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# setup-click-project.sh - Setup Click project dependencies and environment
|
||||||
|
#
|
||||||
|
# Usage: setup-click-project.sh [project-directory]
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_info() { echo -e "${CYAN}ℹ${NC} $1"; }
|
||||||
|
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||||
|
print_error() { echo -e "${RED}✗${NC} $1" >&2; }
|
||||||
|
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||||
|
|
||||||
|
PROJECT_DIR="${1:-.}"
|
||||||
|
|
||||||
|
print_info "Setting up Click project in: $PROJECT_DIR"
|
||||||
|
|
||||||
|
# Check if Python is installed
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
print_error "Python 3 is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
|
||||||
|
print_success "Python $PYTHON_VERSION detected"
|
||||||
|
|
||||||
|
# Navigate to project directory
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# Check if virtual environment exists
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
print_info "Creating virtual environment..."
|
||||||
|
python3 -m venv venv
|
||||||
|
print_success "Virtual environment created"
|
||||||
|
else
|
||||||
|
print_info "Virtual environment already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
print_info "Activating virtual environment..."
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Upgrade pip
|
||||||
|
print_info "Upgrading pip..."
|
||||||
|
pip install --upgrade pip > /dev/null 2>&1
|
||||||
|
print_success "pip upgraded"
|
||||||
|
|
||||||
|
# Install Click and dependencies
|
||||||
|
print_info "Installing Click and dependencies..."
|
||||||
|
if [ -f "requirements.txt" ]; then
|
||||||
|
pip install -r requirements.txt
|
||||||
|
print_success "Installed from requirements.txt"
|
||||||
|
else
|
||||||
|
pip install click rich
|
||||||
|
print_success "Installed click and rich"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install development dependencies
|
||||||
|
print_info "Installing development dependencies..."
|
||||||
|
pip install pytest pytest-cov black pylint mypy
|
||||||
|
print_success "Development dependencies installed"
|
||||||
|
|
||||||
|
# Create .env.example if it doesn't exist
|
||||||
|
if [ ! -f ".env.example" ]; then
|
||||||
|
cat > .env.example <<'EOF'
|
||||||
|
# Environment variables for CLI
|
||||||
|
API_KEY=your_api_key_here
|
||||||
|
DEBUG=false
|
||||||
|
LOG_LEVEL=info
|
||||||
|
EOF
|
||||||
|
print_success "Created .env.example"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup pre-commit hooks if git repo
|
||||||
|
if [ -d ".git" ]; then
|
||||||
|
print_info "Setting up git hooks..."
|
||||||
|
cat > .git/hooks/pre-commit <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Run tests before commit
|
||||||
|
source venv/bin/activate
|
||||||
|
black src/ tests/ --check || exit 1
|
||||||
|
pylint src/ || exit 1
|
||||||
|
pytest tests/ || exit 1
|
||||||
|
EOF
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
print_success "Git hooks configured"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
print_info "Verifying installation..."
|
||||||
|
python3 -c "import click; print(f'Click version: {click.__version__}')"
|
||||||
|
print_success "Click is properly installed"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_success "Setup completed successfully!"
|
||||||
|
echo ""
|
||||||
|
print_info "Next steps:"
|
||||||
|
echo " 1. source venv/bin/activate"
|
||||||
|
echo " 2. python src/cli.py --help"
|
||||||
|
echo " 3. pytest tests/"
|
||||||
162
skills/click-patterns/scripts/validate-click.sh
Executable file
162
skills/click-patterns/scripts/validate-click.sh
Executable file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# validate-click.sh - Validate Click CLI implementation
|
||||||
|
#
|
||||||
|
# Usage: validate-click.sh <cli-file.py>
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_info() { echo -e "${CYAN}ℹ${NC} $1"; }
|
||||||
|
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||||
|
print_error() { echo -e "${RED}✗${NC} $1" >&2; }
|
||||||
|
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||||
|
|
||||||
|
# Validate arguments
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
print_error "Usage: $0 <cli-file.py>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CLI_FILE="$1"
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
if [ ! -f "$CLI_FILE" ]; then
|
||||||
|
print_error "File not found: $CLI_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_info "Validating Click CLI: $CLI_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
VALIDATION_PASSED=true
|
||||||
|
|
||||||
|
# Check 1: File is a Python file
|
||||||
|
if [[ ! "$CLI_FILE" =~ \.py$ ]]; then
|
||||||
|
print_error "File must be a Python file (.py)"
|
||||||
|
VALIDATION_PASSED=false
|
||||||
|
else
|
||||||
|
print_success "File extension is valid (.py)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 2: File imports Click
|
||||||
|
if grep -q "import click" "$CLI_FILE"; then
|
||||||
|
print_success "Click module is imported"
|
||||||
|
else
|
||||||
|
print_error "Click module is not imported"
|
||||||
|
VALIDATION_PASSED=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 3: Has at least one Click decorator
|
||||||
|
DECORATOR_COUNT=$(grep -c "@click\." "$CLI_FILE" || true)
|
||||||
|
if [ "$DECORATOR_COUNT" -gt 0 ]; then
|
||||||
|
print_success "Found $DECORATOR_COUNT Click decorator(s)"
|
||||||
|
else
|
||||||
|
print_error "No Click decorators found"
|
||||||
|
VALIDATION_PASSED=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 4: Has main entry point or group
|
||||||
|
if grep -q "@click.command()\|@click.group()" "$CLI_FILE"; then
|
||||||
|
print_success "Has Click command or group decorator"
|
||||||
|
else
|
||||||
|
print_error "Missing @click.command() or @click.group()"
|
||||||
|
VALIDATION_PASSED=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 5: Has if __name__ == '__main__' block
|
||||||
|
if grep -q "if __name__ == '__main__':" "$CLI_FILE"; then
|
||||||
|
print_success "Has main execution block"
|
||||||
|
else
|
||||||
|
print_warning "Missing main execution block (if __name__ == '__main__':)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 6: Python syntax is valid
|
||||||
|
if python3 -m py_compile "$CLI_FILE" 2>/dev/null; then
|
||||||
|
print_success "Python syntax is valid"
|
||||||
|
else
|
||||||
|
print_error "Python syntax errors detected"
|
||||||
|
VALIDATION_PASSED=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 7: Has help text
|
||||||
|
if grep -q '"""' "$CLI_FILE"; then
|
||||||
|
print_success "Contains docstrings/help text"
|
||||||
|
else
|
||||||
|
print_warning "No docstrings found (recommended for help text)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 8: Has option or argument decorators
|
||||||
|
if grep -q "@click.option\|@click.argument" "$CLI_FILE"; then
|
||||||
|
print_success "Has options or arguments defined"
|
||||||
|
else
|
||||||
|
print_warning "No options or arguments defined"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 9: Uses recommended patterns
|
||||||
|
echo ""
|
||||||
|
print_info "Checking best practices..."
|
||||||
|
|
||||||
|
# Check for version option
|
||||||
|
if grep -q "@click.version_option" "$CLI_FILE"; then
|
||||||
|
print_success "Has version option"
|
||||||
|
else
|
||||||
|
print_warning "Consider adding @click.version_option()"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for help parameter
|
||||||
|
if grep -q "help=" "$CLI_FILE"; then
|
||||||
|
print_success "Uses help parameters"
|
||||||
|
else
|
||||||
|
print_warning "Consider adding help text to options"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for context usage
|
||||||
|
if grep -q "@click.pass_context" "$CLI_FILE"; then
|
||||||
|
print_success "Uses context for state sharing"
|
||||||
|
else
|
||||||
|
print_info "No context usage detected (optional)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for command groups
|
||||||
|
if grep -q "@click.group()" "$CLI_FILE"; then
|
||||||
|
print_success "Uses command groups"
|
||||||
|
# Check for subcommands
|
||||||
|
SUBCOMMAND_COUNT=$(grep -c "\.command()" "$CLI_FILE" || true)
|
||||||
|
if [ "$SUBCOMMAND_COUNT" -gt 0 ]; then
|
||||||
|
print_success "Has $SUBCOMMAND_COUNT subcommand(s)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for validation
|
||||||
|
if grep -q "click.Choice\|click.IntRange\|click.FloatRange\|click.Path" "$CLI_FILE"; then
|
||||||
|
print_success "Uses Click's built-in validators"
|
||||||
|
else
|
||||||
|
print_info "No built-in validators detected (optional)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for colored output (Rich or Click's styling)
|
||||||
|
if grep -q "from rich\|click.style\|click.echo.*fg=" "$CLI_FILE"; then
|
||||||
|
print_success "Uses colored output"
|
||||||
|
else
|
||||||
|
print_info "No colored output detected (optional)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
if [ "$VALIDATION_PASSED" = true ]; then
|
||||||
|
print_success "All critical validations passed!"
|
||||||
|
echo ""
|
||||||
|
print_info "Try running: python3 $CLI_FILE --help"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
print_error "Validation failed. Please fix the errors above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
310
skills/click-patterns/templates/advanced-cli.py
Normal file
310
skills/click-patterns/templates/advanced-cli.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Advanced Click CLI Template
|
||||||
|
|
||||||
|
Demonstrates advanced patterns including:
|
||||||
|
- Custom parameter types
|
||||||
|
- Command chaining
|
||||||
|
- Plugin architecture
|
||||||
|
- Configuration management
|
||||||
|
- Logging integration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
import logging
|
||||||
|
from rich.console import Console
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Custom parameter types
|
||||||
|
class JsonType(click.ParamType):
|
||||||
|
"""Custom type for JSON parsing"""
|
||||||
|
name = 'json'
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
self.fail(f'Invalid JSON: {e}', param, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
class PathListType(click.ParamType):
|
||||||
|
"""Custom type for comma-separated paths"""
|
||||||
|
name = 'pathlist'
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
paths = [Path(p.strip()) for p in value.split(',')]
|
||||||
|
for path in paths:
|
||||||
|
if not path.exists():
|
||||||
|
self.fail(f'Path does not exist: {path}', param, ctx)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration class
|
||||||
|
class Config:
|
||||||
|
"""Application configuration"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.debug = False
|
||||||
|
self.log_level = 'INFO'
|
||||||
|
self.config_file = 'config.json'
|
||||||
|
self._data = {}
|
||||||
|
|
||||||
|
def load(self, config_file: Optional[str] = None):
|
||||||
|
"""Load configuration from file"""
|
||||||
|
file_path = Path(config_file or self.config_file)
|
||||||
|
if file_path.exists():
|
||||||
|
with open(file_path) as f:
|
||||||
|
self._data = json.load(f)
|
||||||
|
logger.info(f"Loaded config from {file_path}")
|
||||||
|
|
||||||
|
def get(self, key: str, default=None):
|
||||||
|
"""Get configuration value"""
|
||||||
|
return self._data.get(key, default)
|
||||||
|
|
||||||
|
def set(self, key: str, value):
|
||||||
|
"""Set configuration value"""
|
||||||
|
self._data[key] = value
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Save configuration to file"""
|
||||||
|
file_path = Path(self.config_file)
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
json.dump(self._data, f, indent=2)
|
||||||
|
logger.info(f"Saved config to {file_path}")
|
||||||
|
|
||||||
|
|
||||||
|
# Pass config between commands
|
||||||
|
pass_config = click.make_pass_decorator(Config, ensure=True)
|
||||||
|
|
||||||
|
|
||||||
|
# Main CLI group
|
||||||
|
@click.group(chain=True)
|
||||||
|
@click.option('--debug', is_flag=True, help='Enable debug mode')
|
||||||
|
@click.option('--config', type=click.Path(), default='config.json',
|
||||||
|
help='Configuration file')
|
||||||
|
@click.option('--log-level',
|
||||||
|
type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR']),
|
||||||
|
default='INFO',
|
||||||
|
help='Logging level')
|
||||||
|
@click.version_option(version='2.0.0')
|
||||||
|
@pass_config
|
||||||
|
def cli(config: Config, debug: bool, config: str, log_level: str):
|
||||||
|
"""
|
||||||
|
Advanced CLI with chaining and plugin support.
|
||||||
|
|
||||||
|
Commands can be chained together:
|
||||||
|
cli init process deploy
|
||||||
|
cli config set key=value process --validate
|
||||||
|
"""
|
||||||
|
config.debug = debug
|
||||||
|
config.log_level = log_level
|
||||||
|
config.config_file = config
|
||||||
|
config.load()
|
||||||
|
|
||||||
|
# Set logging level
|
||||||
|
logger.setLevel(getattr(logging, log_level))
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
console.print("[dim]Debug mode enabled[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
# Pipeline commands (chainable)
|
||||||
|
@cli.command()
|
||||||
|
@click.option('--template', type=click.Choice(['basic', 'advanced', 'api']),
|
||||||
|
default='basic')
|
||||||
|
@pass_config
|
||||||
|
def init(config: Config, template: str):
|
||||||
|
"""Initialize project (chainable)"""
|
||||||
|
console.print(f"[cyan]Initializing with {template} template...[/cyan]")
|
||||||
|
config.set('template', template)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option('--validate', is_flag=True, help='Validate before processing')
|
||||||
|
@click.option('--parallel', is_flag=True, help='Process in parallel')
|
||||||
|
@pass_config
|
||||||
|
def process(config: Config, validate: bool, parallel: bool):
|
||||||
|
"""Process data (chainable)"""
|
||||||
|
console.print("[cyan]Processing data...[/cyan]")
|
||||||
|
|
||||||
|
if validate:
|
||||||
|
console.print("[dim]Validating input...[/dim]")
|
||||||
|
|
||||||
|
mode = "parallel" if parallel else "sequential"
|
||||||
|
console.print(f"[dim]Processing mode: {mode}[/dim]")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument('environment', type=click.Choice(['dev', 'staging', 'prod']))
|
||||||
|
@click.option('--dry-run', is_flag=True, help='Simulate deployment')
|
||||||
|
@pass_config
|
||||||
|
def deploy(config: Config, environment: str, dry_run: bool):
|
||||||
|
"""Deploy to environment (chainable)"""
|
||||||
|
prefix = "[yellow][DRY RUN][/yellow] " if dry_run else ""
|
||||||
|
console.print(f"{prefix}[cyan]Deploying to {environment}...[/cyan]")
|
||||||
|
|
||||||
|
template = config.get('template', 'unknown')
|
||||||
|
console.print(f"[dim]Template: {template}[/dim]")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
# Advanced configuration commands
|
||||||
|
@cli.group()
|
||||||
|
def config():
|
||||||
|
"""Advanced configuration management"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@pass_config
|
||||||
|
def get(config: Config, key: str):
|
||||||
|
"""Get configuration value"""
|
||||||
|
value = config.get(key)
|
||||||
|
if value is not None:
|
||||||
|
console.print(f"{key}: [green]{value}[/green]")
|
||||||
|
else:
|
||||||
|
console.print(f"[yellow]Key not found: {key}[/yellow]")
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.argument('pair')
|
||||||
|
@pass_config
|
||||||
|
def set(config: Config, pair: str):
|
||||||
|
"""Set configuration (format: key=value)"""
|
||||||
|
if '=' not in pair:
|
||||||
|
raise click.BadParameter('Format must be key=value')
|
||||||
|
|
||||||
|
key, value = pair.split('=', 1)
|
||||||
|
config.set(key, value)
|
||||||
|
config.save()
|
||||||
|
console.print(f"[green]✓[/green] Set {key} = {value}")
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.option('--format', type=click.Choice(['json', 'yaml', 'env']),
|
||||||
|
default='json')
|
||||||
|
@pass_config
|
||||||
|
def export(config: Config, format: str):
|
||||||
|
"""Export configuration in different formats"""
|
||||||
|
console.print(f"[cyan]Exporting config as {format}...[/cyan]")
|
||||||
|
|
||||||
|
if format == 'json':
|
||||||
|
output = json.dumps(config._data, indent=2)
|
||||||
|
elif format == 'yaml':
|
||||||
|
# Simplified YAML output
|
||||||
|
output = '\n'.join(f"{k}: {v}" for k, v in config._data.items())
|
||||||
|
else: # env
|
||||||
|
output = '\n'.join(f"{k.upper()}={v}" for k, v in config._data.items())
|
||||||
|
|
||||||
|
console.print(output)
|
||||||
|
|
||||||
|
|
||||||
|
# Advanced data operations
|
||||||
|
@cli.group()
|
||||||
|
def data():
|
||||||
|
"""Data operations with advanced types"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@data.command()
|
||||||
|
@click.option('--json-data', type=JsonType(), help='JSON data to import')
|
||||||
|
@click.option('--paths', type=PathListType(), help='Comma-separated paths')
|
||||||
|
@pass_config
|
||||||
|
def import_data(config: Config, json_data: Optional[dict], paths: Optional[list]):
|
||||||
|
"""Import data from various sources"""
|
||||||
|
console.print("[cyan]Importing data...[/cyan]")
|
||||||
|
|
||||||
|
if json_data:
|
||||||
|
console.print(f"[dim]JSON data: {json_data}[/dim]")
|
||||||
|
|
||||||
|
if paths:
|
||||||
|
console.print(f"[dim]Processing {len(paths)} path(s)[/dim]")
|
||||||
|
for path in paths:
|
||||||
|
console.print(f" - {path}")
|
||||||
|
|
||||||
|
|
||||||
|
@data.command()
|
||||||
|
@click.option('--input', type=click.File('r'), help='Input file')
|
||||||
|
@click.option('--output', type=click.File('w'), help='Output file')
|
||||||
|
@click.option('--format',
|
||||||
|
type=click.Choice(['json', 'csv', 'xml']),
|
||||||
|
default='json')
|
||||||
|
def transform(input, output, format):
|
||||||
|
"""Transform data between formats"""
|
||||||
|
console.print(f"[cyan]Transforming data to {format}...[/cyan]")
|
||||||
|
|
||||||
|
if input:
|
||||||
|
data = input.read()
|
||||||
|
console.print(f"[dim]Read {len(data)} bytes[/dim]")
|
||||||
|
|
||||||
|
if output:
|
||||||
|
# Would write transformed data here
|
||||||
|
output.write('{}') # Placeholder
|
||||||
|
console.print("[green]✓[/green] Transformation complete")
|
||||||
|
|
||||||
|
|
||||||
|
# Plugin system
|
||||||
|
@cli.group()
|
||||||
|
def plugin():
|
||||||
|
"""Plugin management"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@plugin.command()
|
||||||
|
@click.argument('plugin_name')
|
||||||
|
@click.option('--version', help='Plugin version')
|
||||||
|
def install(plugin_name: str, version: Optional[str]):
|
||||||
|
"""Install a plugin"""
|
||||||
|
version_str = f"@{version}" if version else "@latest"
|
||||||
|
console.print(f"[cyan]Installing plugin: {plugin_name}{version_str}...[/cyan]")
|
||||||
|
console.print("[green]✓[/green] Plugin installed successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@plugin.command()
|
||||||
|
def list():
|
||||||
|
"""List installed plugins"""
|
||||||
|
console.print("[cyan]Installed Plugins:[/cyan]")
|
||||||
|
# Placeholder plugin list
|
||||||
|
plugins = [
|
||||||
|
{"name": "auth-plugin", "version": "1.0.0", "status": "active"},
|
||||||
|
{"name": "database-plugin", "version": "2.1.0", "status": "active"},
|
||||||
|
]
|
||||||
|
for p in plugins:
|
||||||
|
status_color = "green" if p["status"] == "active" else "yellow"
|
||||||
|
console.print(f" - {p['name']} ({p['version']}) [{status_color}]{p['status']}[/{status_color}]")
|
||||||
|
|
||||||
|
|
||||||
|
# Batch operations
|
||||||
|
@cli.command()
|
||||||
|
@click.argument('commands', nargs=-1, required=True)
|
||||||
|
@pass_config
|
||||||
|
def batch(config: Config, commands: tuple):
|
||||||
|
"""Execute multiple commands in batch"""
|
||||||
|
console.print(f"[cyan]Executing {len(commands)} command(s)...[/cyan]")
|
||||||
|
|
||||||
|
for i, cmd in enumerate(commands, 1):
|
||||||
|
console.print(f"[dim]{i}. {cmd}[/dim]")
|
||||||
|
# Would execute actual commands here
|
||||||
|
|
||||||
|
console.print("[green]✓[/green] Batch execution completed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cli()
|
||||||
37
skills/click-patterns/templates/basic-cli.py
Normal file
37
skills/click-patterns/templates/basic-cli.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Basic Click CLI Template
|
||||||
|
|
||||||
|
A simple single-command CLI using Click framework.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.version_option(version='1.0.0')
|
||||||
|
@click.option('--name', '-n', default='World', help='Name to greet')
|
||||||
|
@click.option('--count', '-c', default=1, type=int, help='Number of greetings')
|
||||||
|
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
|
||||||
|
def cli(name, count, verbose):
|
||||||
|
"""
|
||||||
|
A simple greeting CLI tool.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python cli.py --name Alice --count 3
|
||||||
|
"""
|
||||||
|
if verbose:
|
||||||
|
console.print(f"[dim]Running with name={name}, count={count}[/dim]")
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
console.print(f"[green]Hello, {name}![/green]")
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
console.print(f"[dim]Completed {count} greeting(s)[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cli()
|
||||||
126
skills/click-patterns/templates/nested-commands.py
Normal file
126
skills/click-patterns/templates/nested-commands.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Nested Commands Click Template
|
||||||
|
|
||||||
|
Demonstrates command groups, nested subcommands, and context sharing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.version_option(version='1.0.0')
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
"""
|
||||||
|
A powerful CLI tool with nested commands.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python cli.py init --template basic
|
||||||
|
python cli.py deploy production --mode safe
|
||||||
|
python cli.py config get api-key
|
||||||
|
"""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj['console'] = console
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option('--template', '-t', default='basic',
|
||||||
|
type=click.Choice(['basic', 'advanced', 'minimal']),
|
||||||
|
help='Project template')
|
||||||
|
@click.pass_context
|
||||||
|
def init(ctx, template):
|
||||||
|
"""Initialize a new project"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
console.print(f"[green]✓[/green] Initializing project with {template} template...")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument('environment', type=click.Choice(['dev', 'staging', 'production']))
|
||||||
|
@click.option('--force', '-f', is_flag=True, help='Force deployment')
|
||||||
|
@click.option('--mode', '-m',
|
||||||
|
type=click.Choice(['fast', 'safe', 'rollback']),
|
||||||
|
default='safe',
|
||||||
|
help='Deployment mode')
|
||||||
|
@click.pass_context
|
||||||
|
def deploy(ctx, environment, force, mode):
|
||||||
|
"""Deploy to specified environment"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
console.print(f"[cyan]Deploying to {environment} in {mode} mode[/cyan]")
|
||||||
|
if force:
|
||||||
|
console.print("[yellow]⚠ Force mode enabled[/yellow]")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
def config():
|
||||||
|
"""Manage configuration settings"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@click.pass_context
|
||||||
|
def get(ctx, key):
|
||||||
|
"""Get configuration value"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
# Placeholder for actual config retrieval
|
||||||
|
value = "example_value"
|
||||||
|
console.print(f"[dim]Config[/dim] {key}: [green]{value}[/green]")
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@click.argument('value')
|
||||||
|
@click.pass_context
|
||||||
|
def set(ctx, key, value):
|
||||||
|
"""Set configuration value"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
# Placeholder for actual config storage
|
||||||
|
console.print(f"[green]✓[/green] Set {key} = {value}")
|
||||||
|
|
||||||
|
|
||||||
|
@config.command()
|
||||||
|
@click.pass_context
|
||||||
|
def list(ctx):
|
||||||
|
"""List all configuration settings"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
console.print("[cyan]Configuration Settings:[/cyan]")
|
||||||
|
# Placeholder for actual config listing
|
||||||
|
console.print(" api-key: [dim]***hidden***[/dim]")
|
||||||
|
console.print(" debug: [green]true[/green]")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
def database():
|
||||||
|
"""Database management commands"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@database.command()
|
||||||
|
@click.option('--create-tables', is_flag=True, help='Create tables')
|
||||||
|
@click.pass_context
|
||||||
|
def migrate(ctx, create_tables):
|
||||||
|
"""Run database migrations"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
console.print("[cyan]Running migrations...[/cyan]")
|
||||||
|
if create_tables:
|
||||||
|
console.print("[green]✓[/green] Tables created")
|
||||||
|
|
||||||
|
|
||||||
|
@database.command()
|
||||||
|
@click.option('--confirm', is_flag=True, help='Confirm reset')
|
||||||
|
@click.pass_context
|
||||||
|
def reset(ctx, confirm):
|
||||||
|
"""Reset database (destructive)"""
|
||||||
|
console = ctx.obj['console']
|
||||||
|
if not confirm:
|
||||||
|
console.print("[yellow]⚠ Use --confirm to proceed[/yellow]")
|
||||||
|
return
|
||||||
|
console.print("[red]Resetting database...[/red]")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cli(obj={})
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user