playbook/outfitter-agents/plugins/outfitter/scripts/init-plugin.sh

589 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# init-plugin.sh - Interactive plugin initialization wizard
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Print colored output
print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
print_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
print_prompt() { echo -e "${CYAN}[?]${NC} $1"; }
# Usage information
usage() {
cat << EOF
Usage: $0 [options]
Interactive wizard to initialize a new Claude Code plugin.
Options:
-d, --directory DIR Create plugin in specified directory (default: current)
-n, --non-interactive Use defaults without prompting
-h, --help Show this help message
Example:
$0 # Interactive mode
$0 -d ./plugins # Create in specific directory
$0 -n # Non-interactive with defaults
EOF
exit 1
}
# Parse arguments
PLUGIN_DIR="."
INTERACTIVE=true
while [[ $# -gt 0 ]]; do
case $1 in
-d|--directory)
PLUGIN_DIR="$2"
shift 2
;;
-n|--non-interactive)
INTERACTIVE=false
shift
;;
-h|--help)
usage
;;
-*)
print_error "Unknown option: $1"
usage
;;
*)
print_error "Unexpected argument: $1"
usage
;;
esac
done
# Prompt for input with default
prompt_with_default() {
local prompt="$1"
local default="$2"
local result
if [[ "$INTERACTIVE" == "false" ]]; then
echo "$default"
return
fi
read -p "$(echo -e "${CYAN}[?]${NC} ${prompt} [${default}]: ")" result
echo "${result:-$default}"
}
# Prompt yes/no with default
prompt_yes_no() {
local prompt="$1"
local default="$2"
local result
if [[ "$INTERACTIVE" == "false" ]]; then
echo "$default"
return
fi
while true; do
read -p "$(echo -e "${CYAN}[?]${NC} ${prompt} (y/n) [${default}]: ")" result
result="${result:-$default}"
case "$result" in
y|Y|yes|Yes|YES)
echo "y"
return
;;
n|N|no|No|NO)
echo "n"
return
;;
*)
print_warn "Please answer y or n"
;;
esac
done
}
# Validate plugin name
validate_plugin_name() {
local name="$1"
if [[ -z "$name" ]]; then
print_error "Plugin name cannot be empty"
return 1
fi
if [[ ! "$name" =~ ^[a-z][a-z0-9-]*$ ]]; then
print_error "Plugin name must be kebab-case (lowercase letters, numbers, hyphens only)"
print_error "Examples: my-plugin, dev-tools, claude-helper"
return 1
fi
return 0
}
# Banner
echo -e "${BLUE}╔════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Claude Code Plugin Initialization ║${NC}"
echo -e "${BLUE}╔════════════════════════════════════════════╗${NC}"
echo
# Step 1: Plugin name
print_step "Plugin Information"
echo
while true; do
PLUGIN_NAME=$(prompt_with_default "Plugin name (kebab-case)" "my-plugin")
if validate_plugin_name "$PLUGIN_NAME"; then
break
fi
done
PLUGIN_PATH="$PLUGIN_DIR/$PLUGIN_NAME"
# Check if directory exists
if [[ -d "$PLUGIN_PATH" ]]; then
print_error "Directory already exists: $PLUGIN_PATH"
exit 1
fi
# Step 2: Plugin metadata
DESCRIPTION=$(prompt_with_default "Plugin description" "A Claude Code plugin")
VERSION=$(prompt_with_default "Initial version" "0.1.0")
LICENSE=$(prompt_with_default "License" "MIT")
# Get author info from git config or prompt
GIT_USER=$(git config user.name 2>/dev/null || echo "")
GIT_EMAIL=$(git config user.email 2>/dev/null || echo "")
AUTHOR_NAME=$(prompt_with_default "Author name" "${GIT_USER:-Author Name}")
AUTHOR_EMAIL=$(prompt_with_default "Author email" "${GIT_EMAIL:-author@example.com}")
# Step 3: Plugin components
echo
print_step "Plugin Components"
echo
print_info "Select which components to include:"
echo
WITH_SKILLS=$(prompt_yes_no "Include skills?" "n")
WITH_COMMANDS=$(prompt_yes_no "Include slash commands?" "n")
WITH_AGENTS=$(prompt_yes_no "Include custom agents?" "n")
WITH_HOOKS=$(prompt_yes_no "Include event hooks?" "n")
WITH_MCP=$(prompt_yes_no "Include MCP server?" "n")
# Step 4: Repository setup
echo
print_step "Repository Configuration"
echo
INIT_GIT=$(prompt_yes_no "Initialize git repository?" "y")
CREATE_GITHUB_ACTIONS=$(prompt_yes_no "Add GitHub Actions workflow?" "n")
# Step 5: Confirmation
echo
print_step "Summary"
echo
echo "Plugin Name: $PLUGIN_NAME"
echo "Location: $PLUGIN_PATH"
echo "Description: $DESCRIPTION"
echo "Version: $VERSION"
echo "License: $LICENSE"
echo "Author: $AUTHOR_NAME <$AUTHOR_EMAIL>"
echo
echo "Components:"
[[ "$WITH_SKILLS" == "y" ]] && echo " ✓ Skills"
[[ "$WITH_COMMANDS" == "y" ]] && echo " ✓ Slash Commands"
[[ "$WITH_AGENTS" == "y" ]] && echo " ✓ Custom Agents"
[[ "$WITH_HOOKS" == "y" ]] && echo " ✓ Event Hooks"
[[ "$WITH_MCP" == "y" ]] && echo " ✓ MCP Server"
echo
if [[ "$INTERACTIVE" == "true" ]]; then
CONFIRM=$(prompt_yes_no "Create plugin?" "y")
if [[ "$CONFIRM" != "y" ]]; then
print_warn "Cancelled"
exit 0
fi
fi
# Create plugin structure
echo
print_info "Creating plugin structure..."
mkdir -p "$PLUGIN_PATH/.claude-plugin"
# Create plugin.json
print_info "Creating plugin.json"
cat > "$PLUGIN_PATH/.claude-plugin/plugin.json" << EOF
{
"name": "$PLUGIN_NAME",
"version": "$VERSION",
"description": "$DESCRIPTION",
"author": {
"name": "$AUTHOR_NAME",
"email": "$AUTHOR_EMAIL"
},
"license": "$LICENSE",
"keywords": []
}
EOF
# Create README.md
print_info "Creating README.md"
cat > "$PLUGIN_PATH/README.md" << EOF
# $PLUGIN_NAME
$DESCRIPTION
## Installation
\`\`\`bash
# Add marketplace
/plugin marketplace add <path-or-repo>
# Install plugin
/plugin install $PLUGIN_NAME@$PLUGIN_NAME
\`\`\`
## Features
TODO: List your plugin features
## Usage
TODO: Describe how to use your plugin
## Development
TODO: Add development instructions
## License
$LICENSE License - see [LICENSE](LICENSE) for details.
EOF
# Create CHANGELOG.md
print_info "Creating CHANGELOG.md"
cat > "$PLUGIN_PATH/CHANGELOG.md" << EOF
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [$VERSION] - $(date +%Y-%m-%d)
### Added
- Initial release
EOF
# Create LICENSE
print_info "Creating LICENSE"
if [[ "$LICENSE" == "MIT" ]]; then
cat > "$PLUGIN_PATH/LICENSE" << EOF
MIT License
Copyright (c) $(date +%Y) $AUTHOR_NAME
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
EOF
else
touch "$PLUGIN_PATH/LICENSE"
print_warn "Please add $LICENSE license text to LICENSE file"
fi
# Create .gitignore
cat > "$PLUGIN_PATH/.gitignore" << 'EOF'
# Logs
*.log
logs/
# OS files
.DS_Store
Thumbs.db
# Editor directories
.vscode/
.idea/
*.swp
*.swo
*~
# Environment
.env
.env.local
# Dependencies
node_modules/
__pycache__/
*.pyc
.venv/
venv/
dist/
build/
# Test coverage
coverage/
.coverage
*.cover
EOF
# Create components based on selections
if [[ "$WITH_SKILLS" == "y" ]]; then
print_info "Creating skills structure"
mkdir -p "$PLUGIN_PATH/skills/example-skill"
cat > "$PLUGIN_PATH/skills/example-skill/SKILL.md" << 'EOF'
---
name: example-skill
description: An example skill that demonstrates skill authoring
version: 0.1.0
---
# Example Skill
This is an example skill. Skills are automatically activated based on:
- Keywords in the description
- User mentioning the skill name
- Context matching skill capabilities
## What This Skill Does
Describe what this skill helps with.
## When to Use
This skill activates when:
- Condition 1
- Condition 2
- Condition 3
## Quick Start
Provide a quick example of using this skill.
## Detailed Guide
Add comprehensive documentation here.
EOF
print_info "Add skills to plugin.json manually or use the skill authoring tools"
fi
if [[ "$WITH_COMMANDS" == "y" ]]; then
print_info "Creating commands structure"
mkdir -p "$PLUGIN_PATH/commands"
cat > "$PLUGIN_PATH/commands/hello.md" << 'EOF'
---
description: "Say hello with a friendly greeting"
---
Generate a friendly greeting for {{0:name}}.
Make the greeting:
- Warm and welcoming
- Include time of day
- Professional yet friendly
EOF
fi
if [[ "$WITH_AGENTS" == "y" ]]; then
print_info "Creating agents structure"
mkdir -p "$PLUGIN_PATH/agents"
cat > "$PLUGIN_PATH/agents/helper.md" << 'EOF'
---
name: helper
description: "A helpful assistant for common tasks"
---
You are a helpful assistant specialized in [domain].
Your responsibilities:
1. Task 1
2. Task 2
3. Task 3
Guidelines:
- Be clear and concise
- Provide examples
- Explain your reasoning
EOF
# Update plugin.json
tmp=$(mktemp)
jq '.agents = ["./agents/helper.md"]' "$PLUGIN_PATH/.claude-plugin/plugin.json" > "$tmp"
mv "$tmp" "$PLUGIN_PATH/.claude-plugin/plugin.json"
fi
if [[ "$WITH_HOOKS" == "y" ]]; then
print_info "Creating hooks structure"
mkdir -p "$PLUGIN_PATH/hooks"
cat > "$PLUGIN_PATH/hooks/example-hook.sh" << 'EOF'
#!/usr/bin/env bash
# Example hook - receives JSON on stdin, outputs JSON to stdout
input=$(cat)
# Parse input
file_path=$(echo "$input" | jq -r '.parameters.file_path // empty')
# Add your logic here
# For PreToolUse hooks, you can approve/block:
# echo '{"allowed": true}'
# echo '{"allowed": false, "reason": "validation failed"}'
# For PostToolUse hooks, you can modify the result:
# echo "$input" | jq '.result.modified = true'
# Default: allow the operation
echo '{"allowed": true}'
EOF
chmod +x "$PLUGIN_PATH/hooks/example-hook.sh"
print_info "Add hooks to plugin.json manually or use the hook authoring tools"
fi
if [[ "$WITH_MCP" == "y" ]]; then
print_info "Creating MCP server structure"
mkdir -p "$PLUGIN_PATH/servers/$PLUGIN_NAME-server"
cat > "$PLUGIN_PATH/servers/$PLUGIN_NAME-server/server.py" << EOF
"""MCP server for $PLUGIN_NAME"""
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("$PLUGIN_NAME")
@mcp.tool()
async def example_tool(param: str) -> str:
"""
Example tool implementation.
Args:
param: Parameter description
Returns:
Result description
"""
return f"Processed: {param}"
if __name__ == "__main__":
mcp.run(transport='stdio')
EOF
cat > "$PLUGIN_PATH/servers/$PLUGIN_NAME-server/pyproject.toml" << EOF
[project]
name = "$PLUGIN_NAME-server"
version = "$VERSION"
description = "MCP server for $PLUGIN_NAME"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.2.0",
]
EOF
# Update plugin.json
tmp=$(mktemp)
jq --arg name "$PLUGIN_NAME" '.mcpServers = {($name): {"command": "uv", "args": ["--directory", "${CLAUDE_PLUGIN_ROOT}/servers/\($name)-server", "run", "server.py"]}}' "$PLUGIN_PATH/.claude-plugin/plugin.json" > "$tmp"
mv "$tmp" "$PLUGIN_PATH/.claude-plugin/plugin.json"
fi
# Create GitHub Actions workflow if requested
if [[ "$CREATE_GITHUB_ACTIONS" == "y" ]]; then
print_info "Creating GitHub Actions workflow"
mkdir -p "$PLUGIN_PATH/.github/workflows"
cat > "$PLUGIN_PATH/.github/workflows/validate.yml" << 'EOF'
name: Validate Plugin
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate plugin.json
run: |
# Add validation logic here
if [ ! -f ".claude-plugin/plugin.json" ]; then
echo "Error: plugin.json not found"
exit 1
fi
# Validate JSON syntax
jq empty .claude-plugin/plugin.json
EOF
fi
# Initialize git repository
if [[ "$INIT_GIT" == "y" ]] && command -v git &> /dev/null; then
print_info "Initializing git repository"
cd "$PLUGIN_PATH"
git init -q
git add .
git commit -q -m "feat: initial plugin structure
- Generated with outfitter init-plugin.sh
- Initialized $PLUGIN_NAME v$VERSION"
cd - > /dev/null
fi
# Success message
echo
print_info "✓ Plugin created successfully!"
echo
echo -e "${BLUE}Plugin Location:${NC} $PLUGIN_PATH"
echo
echo -e "${YELLOW}Next Steps:${NC}"
echo " 1. cd $PLUGIN_PATH"
echo " 2. Edit .claude-plugin/plugin.json to update metadata"
echo " 3. Update README.md with plugin details"
[[ "$WITH_SKILLS" == "y" ]] && echo " 4. Customize skills in skills/"
[[ "$WITH_COMMANDS" == "y" ]] && echo " 4. Add commands to commands/"
[[ "$WITH_AGENTS" == "y" ]] && echo " 4. Customize agents in agents/"
[[ "$WITH_HOOKS" == "y" ]] && echo " 4. Implement hooks in hooks/"
[[ "$WITH_MCP" == "y" ]] && echo " 4. Implement MCP server in servers/"
echo
echo -e "${YELLOW}Test Locally:${NC}"
echo " /plugin marketplace add $PLUGIN_PATH"
echo " /plugin install $PLUGIN_NAME@$PLUGIN_NAME"
echo
print_info "Happy coding!"