📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# validate-plugin.sh - Comprehensive Claude Code plugin validation
|
||||
|
||||
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
|
||||
|
||||
# Counters
|
||||
ERRORS=0
|
||||
WARNINGS=0
|
||||
CHECKS=0
|
||||
|
||||
# Options
|
||||
STRICT_MODE=false
|
||||
QUIET_MODE=false
|
||||
FIX_MODE=false
|
||||
|
||||
# Print functions
|
||||
print_error() {
|
||||
((ERRORS++))
|
||||
echo -e "${RED}✗ ERROR:${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
((WARNINGS++))
|
||||
echo -e "${YELLOW}⚠ WARNING:${NC} $1"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
[[ "$QUIET_MODE" == "false" ]] && echo -e "${BLUE}ℹ INFO:${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
[[ "$QUIET_MODE" == "false" ]] && echo -e "${GREEN}✓ PASS:${NC} $1"
|
||||
}
|
||||
|
||||
print_check() {
|
||||
((CHECKS++))
|
||||
[[ "$QUIET_MODE" == "false" ]] && echo -e "${CYAN}[CHECK $CHECKS]${NC} $1"
|
||||
}
|
||||
|
||||
# Usage
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: $0 [options] <plugin-directory>
|
||||
|
||||
Comprehensive validation for Claude Code plugins.
|
||||
|
||||
Arguments:
|
||||
plugin-directory Path to plugin root directory
|
||||
|
||||
Options:
|
||||
-s, --strict Treat warnings as errors
|
||||
-q, --quiet Only show errors and warnings
|
||||
-f, --fix Auto-fix issues where possible
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
# Validate current plugin
|
||||
$0 .
|
||||
|
||||
# Validate specific plugin
|
||||
$0 /path/to/my-plugin
|
||||
|
||||
# Strict validation
|
||||
$0 --strict .
|
||||
|
||||
# Auto-fix common issues
|
||||
$0 --fix .
|
||||
|
||||
Exit Codes:
|
||||
0 - No errors
|
||||
1 - Validation errors found
|
||||
2 - Invalid arguments or plugin not found
|
||||
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
PLUGIN_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--strict)
|
||||
STRICT_MODE=true
|
||||
shift
|
||||
;;
|
||||
-q|--quiet)
|
||||
QUIET_MODE=true
|
||||
shift
|
||||
;;
|
||||
-f|--fix)
|
||||
FIX_MODE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-*)
|
||||
echo -e "${RED}Error: Unknown option $1${NC}"
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
PLUGIN_DIR="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate arguments
|
||||
if [[ -z "$PLUGIN_DIR" ]]; then
|
||||
echo -e "${RED}Error: Plugin directory required${NC}"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -d "$PLUGIN_DIR" ]]; then
|
||||
echo -e "${RED}Error: Directory not found: $PLUGIN_DIR${NC}"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Convert to absolute path
|
||||
PLUGIN_DIR=$(cd "$PLUGIN_DIR" && pwd)
|
||||
|
||||
# Print header
|
||||
if [[ "$QUIET_MODE" == "false" ]]; then
|
||||
echo -e "${BLUE}╔════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ Claude Code Plugin Validation ║${NC}"
|
||||
echo -e "${BLUE}╚════════════════════════════════════════════╝${NC}"
|
||||
echo
|
||||
echo -e "${CYAN}Plugin Directory:${NC} $PLUGIN_DIR"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Check 1: plugin.json exists
|
||||
print_check "Checking for plugin.json"
|
||||
PLUGIN_JSON="$PLUGIN_DIR/.claude-plugin/plugin.json"
|
||||
|
||||
if [[ ! -f "$PLUGIN_JSON" ]]; then
|
||||
print_error "plugin.json not found at .claude-plugin/plugin.json"
|
||||
exit 1
|
||||
else
|
||||
print_success "plugin.json exists"
|
||||
fi
|
||||
|
||||
# Check 2: plugin.json is valid JSON
|
||||
print_check "Validating plugin.json syntax"
|
||||
if ! jq empty "$PLUGIN_JSON" 2>/dev/null; then
|
||||
print_error "plugin.json contains invalid JSON"
|
||||
exit 1
|
||||
else
|
||||
print_success "plugin.json is valid JSON"
|
||||
fi
|
||||
|
||||
# Check 3: Required fields in plugin.json
|
||||
print_check "Validating plugin.json required fields"
|
||||
|
||||
PLUGIN_NAME=$(jq -r '.name // empty' "$PLUGIN_JSON")
|
||||
PLUGIN_VERSION=$(jq -r '.version // empty' "$PLUGIN_JSON")
|
||||
PLUGIN_DESC=$(jq -r '.description // empty' "$PLUGIN_JSON")
|
||||
|
||||
if [[ -z "$PLUGIN_NAME" ]]; then
|
||||
print_error "plugin.json missing required field: name"
|
||||
else
|
||||
print_success "Plugin name: $PLUGIN_NAME"
|
||||
|
||||
# Validate name format
|
||||
if [[ ! "$PLUGIN_NAME" =~ ^[a-z][a-z0-9-]*$ ]]; then
|
||||
print_error "Plugin name must be kebab-case: $PLUGIN_NAME"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$PLUGIN_VERSION" ]]; then
|
||||
print_error "plugin.json missing required field: version"
|
||||
else
|
||||
print_success "Plugin version: $PLUGIN_VERSION"
|
||||
|
||||
# Validate semantic versioning
|
||||
if [[ ! "$PLUGIN_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
|
||||
print_warning "Version should follow semantic versioning (e.g., 1.0.0)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$PLUGIN_DESC" ]]; then
|
||||
print_warning "plugin.json missing recommended field: description"
|
||||
else
|
||||
print_success "Plugin description present"
|
||||
|
||||
# Check description length
|
||||
DESC_LEN=${#PLUGIN_DESC}
|
||||
if [[ $DESC_LEN -lt 20 ]]; then
|
||||
print_warning "Description is very short ($DESC_LEN chars)"
|
||||
elif [[ $DESC_LEN -gt 200 ]]; then
|
||||
print_warning "Description is very long ($DESC_LEN chars), consider shortening"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check 4: Author info
|
||||
print_check "Validating author information"
|
||||
AUTHOR_NAME=$(jq -r '.author.name // empty' "$PLUGIN_JSON")
|
||||
AUTHOR_EMAIL=$(jq -r '.author.email // empty' "$PLUGIN_JSON")
|
||||
|
||||
if [[ -z "$AUTHOR_NAME" ]]; then
|
||||
print_warning "plugin.json missing recommended field: author.name"
|
||||
else
|
||||
print_success "Author: $AUTHOR_NAME"
|
||||
fi
|
||||
|
||||
if [[ -z "$AUTHOR_EMAIL" ]]; then
|
||||
print_warning "plugin.json missing recommended field: author.email"
|
||||
elif [[ ! "$AUTHOR_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
||||
print_warning "Author email appears invalid: $AUTHOR_EMAIL"
|
||||
fi
|
||||
|
||||
# Check 5: License
|
||||
print_check "Checking license"
|
||||
PLUGIN_LICENSE=$(jq -r '.license // empty' "$PLUGIN_JSON")
|
||||
|
||||
if [[ -z "$PLUGIN_LICENSE" ]]; then
|
||||
print_warning "plugin.json missing recommended field: license"
|
||||
else
|
||||
print_success "License: $PLUGIN_LICENSE"
|
||||
|
||||
# Check for LICENSE file
|
||||
if [[ ! -f "$PLUGIN_DIR/LICENSE" ]] && [[ ! -f "$PLUGIN_DIR/LICENSE.md" ]]; then
|
||||
print_warning "No LICENSE file found in plugin root"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check 6: README.md
|
||||
print_check "Checking README.md"
|
||||
if [[ ! -f "$PLUGIN_DIR/README.md" ]]; then
|
||||
print_warning "No README.md found in plugin root"
|
||||
else
|
||||
print_success "README.md exists"
|
||||
|
||||
# Check README length
|
||||
README_LINES=$(wc -l < "$PLUGIN_DIR/README.md")
|
||||
if [[ $README_LINES -lt 10 ]]; then
|
||||
print_warning "README.md is very short ($README_LINES lines)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check 7: Validate skills
|
||||
print_check "Validating skills"
|
||||
SKILLS_DIR="$PLUGIN_DIR/skills"
|
||||
|
||||
if [[ -d "$SKILLS_DIR" ]]; then
|
||||
SKILL_COUNT=$(find "$SKILLS_DIR" -name "SKILL.md" | wc -l | xargs)
|
||||
if [[ $SKILL_COUNT -eq 0 ]]; then
|
||||
print_warning "skills/ directory exists but contains no SKILL.md files"
|
||||
else
|
||||
print_success "Found $SKILL_COUNT skill(s)"
|
||||
|
||||
# Validate each skill
|
||||
while IFS= read -r skill_file; do
|
||||
SKILL_NAME=$(dirname "$skill_file" | xargs basename)
|
||||
print_info "Validating skill: $SKILL_NAME"
|
||||
|
||||
# Check for frontmatter
|
||||
if ! head -n 1 "$skill_file" | grep -q '^---$'; then
|
||||
print_warning "Skill $SKILL_NAME missing frontmatter"
|
||||
else
|
||||
# Validate required frontmatter fields
|
||||
SKILL_FRONTMATTER=$(awk '/^---$/{flag=!flag; next} flag' "$skill_file" | head -n 20)
|
||||
|
||||
if ! echo "$SKILL_FRONTMATTER" | grep -q '^name:'; then
|
||||
print_error "Skill $SKILL_NAME missing 'name' in frontmatter"
|
||||
fi
|
||||
|
||||
if ! echo "$SKILL_FRONTMATTER" | grep -q '^description:'; then
|
||||
print_error "Skill $SKILL_NAME missing 'description' in frontmatter"
|
||||
fi
|
||||
|
||||
if ! echo "$SKILL_FRONTMATTER" | grep -q '^version:'; then
|
||||
print_warning "Skill $SKILL_NAME missing 'version' in frontmatter"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check file size (skills should have substantial content)
|
||||
SKILL_SIZE=$(wc -c < "$skill_file")
|
||||
if [[ $SKILL_SIZE -lt 500 ]]; then
|
||||
print_warning "Skill $SKILL_NAME is very small ($SKILL_SIZE bytes)"
|
||||
fi
|
||||
done < <(find "$SKILLS_DIR" -name "SKILL.md")
|
||||
fi
|
||||
else
|
||||
print_info "No skills/ directory found"
|
||||
fi
|
||||
|
||||
# Check 8: Validate commands
|
||||
print_check "Validating slash commands"
|
||||
COMMANDS_DIR="$PLUGIN_DIR/commands"
|
||||
|
||||
if [[ -d "$COMMANDS_DIR" ]]; then
|
||||
COMMAND_COUNT=$(find "$COMMANDS_DIR" -name "*.md" | wc -l | xargs)
|
||||
if [[ $COMMAND_COUNT -eq 0 ]]; then
|
||||
print_warning "commands/ directory exists but contains no .md files"
|
||||
else
|
||||
print_success "Found $COMMAND_COUNT command(s)"
|
||||
|
||||
# Validate each command
|
||||
while IFS= read -r cmd_file; do
|
||||
CMD_NAME=$(basename "$cmd_file" .md)
|
||||
print_info "Validating command: $CMD_NAME"
|
||||
|
||||
# Check filename format
|
||||
if [[ ! "$CMD_NAME" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
|
||||
print_warning "Command name should be kebab-case: $CMD_NAME"
|
||||
fi
|
||||
|
||||
# Check if file is empty
|
||||
if [[ ! -s "$cmd_file" ]]; then
|
||||
print_error "Command $CMD_NAME is empty"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check for frontmatter
|
||||
if head -n 1 "$cmd_file" | grep -q '^---$'; then
|
||||
# Validate frontmatter syntax
|
||||
FRONTMATTER=$(awk '/^---$/{flag=!flag; next} flag' "$cmd_file" | head -n 20)
|
||||
|
||||
# Check for description
|
||||
if echo "$FRONTMATTER" | grep -q '^description:'; then
|
||||
DESC=$(echo "$FRONTMATTER" | grep '^description:' | sed 's/^description: *//')
|
||||
if [[ -z "$DESC" ]]; then
|
||||
print_warning "Command $CMD_NAME has empty description"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for tabs in frontmatter (YAML doesn't allow tabs)
|
||||
if echo "$FRONTMATTER" | grep -q $'\t'; then
|
||||
print_error "Command $CMD_NAME frontmatter contains tabs (use spaces)"
|
||||
fi
|
||||
fi
|
||||
done < <(find "$COMMANDS_DIR" -name "*.md")
|
||||
fi
|
||||
else
|
||||
print_info "No commands/ directory found"
|
||||
fi
|
||||
|
||||
# Check 9: Validate agents
|
||||
print_check "Validating custom agents"
|
||||
AGENTS_DIR="$PLUGIN_DIR/agents"
|
||||
|
||||
if [[ -d "$AGENTS_DIR" ]]; then
|
||||
AGENT_COUNT=$(find "$AGENTS_DIR" -name "*.md" | wc -l | xargs)
|
||||
if [[ $AGENT_COUNT -eq 0 ]]; then
|
||||
print_warning "agents/ directory exists but contains no .md files"
|
||||
else
|
||||
print_success "Found $AGENT_COUNT agent(s)"
|
||||
|
||||
# Check if agents are referenced in plugin.json
|
||||
AGENTS_IN_JSON=$(jq -r '.agents // [] | length' "$PLUGIN_JSON")
|
||||
if [[ $AGENTS_IN_JSON -eq 0 ]]; then
|
||||
print_warning "Agents found but not referenced in plugin.json"
|
||||
fi
|
||||
|
||||
# Validate each agent
|
||||
while IFS= read -r agent_file; do
|
||||
AGENT_NAME=$(basename "$agent_file" .md)
|
||||
print_info "Validating agent: $AGENT_NAME"
|
||||
|
||||
# Check for frontmatter
|
||||
if ! head -n 1 "$agent_file" | grep -q '^---$'; then
|
||||
print_warning "Agent $AGENT_NAME missing frontmatter"
|
||||
else
|
||||
AGENT_FRONTMATTER=$(awk '/^---$/{flag=!flag; next} flag' "$agent_file" | head -n 20)
|
||||
|
||||
if ! echo "$AGENT_FRONTMATTER" | grep -q '^name:'; then
|
||||
print_error "Agent $AGENT_NAME missing 'name' in frontmatter"
|
||||
fi
|
||||
|
||||
if ! echo "$AGENT_FRONTMATTER" | grep -q '^description:'; then
|
||||
print_error "Agent $AGENT_NAME missing 'description' in frontmatter"
|
||||
fi
|
||||
fi
|
||||
done < <(find "$AGENTS_DIR" -name "*.md")
|
||||
fi
|
||||
else
|
||||
print_info "No agents/ directory found"
|
||||
fi
|
||||
|
||||
# Check 10: Validate hooks
|
||||
print_check "Validating event hooks"
|
||||
HOOKS_DIR="$PLUGIN_DIR/hooks"
|
||||
|
||||
if [[ -d "$HOOKS_DIR" ]]; then
|
||||
HOOK_COUNT=$(find "$HOOKS_DIR" -type f | wc -l | xargs)
|
||||
if [[ $HOOK_COUNT -eq 0 ]]; then
|
||||
print_warning "hooks/ directory exists but contains no files"
|
||||
else
|
||||
print_success "Found $HOOK_COUNT hook file(s)"
|
||||
|
||||
# Check if hooks are referenced in plugin.json
|
||||
HOOKS_IN_JSON=$(jq -r '.hooks // {} | length' "$PLUGIN_JSON")
|
||||
if [[ $HOOKS_IN_JSON -eq 0 ]]; then
|
||||
print_warning "Hook files found but not configured in plugin.json"
|
||||
fi
|
||||
|
||||
# Validate each hook script
|
||||
while IFS= read -r hook_file; do
|
||||
HOOK_NAME=$(basename "$hook_file")
|
||||
print_info "Validating hook: $HOOK_NAME"
|
||||
|
||||
# Check if file is executable
|
||||
if [[ ! -x "$hook_file" ]]; then
|
||||
if [[ "$FIX_MODE" == "true" ]]; then
|
||||
chmod +x "$hook_file"
|
||||
print_info "Fixed: Made $HOOK_NAME executable"
|
||||
else
|
||||
print_warning "Hook $HOOK_NAME is not executable (use chmod +x)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for shebang
|
||||
if ! head -n 1 "$hook_file" | grep -q '^#!'; then
|
||||
print_warning "Hook $HOOK_NAME missing shebang line"
|
||||
fi
|
||||
done < <(find "$HOOKS_DIR" -type f)
|
||||
fi
|
||||
else
|
||||
print_info "No hooks/ directory found"
|
||||
fi
|
||||
|
||||
# Check 11: Validate MCP servers
|
||||
print_check "Validating MCP servers"
|
||||
SERVERS_DIR="$PLUGIN_DIR/servers"
|
||||
|
||||
if [[ -d "$SERVERS_DIR" ]]; then
|
||||
SERVER_COUNT=$(find "$SERVERS_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l | xargs)
|
||||
if [[ $SERVER_COUNT -eq 0 ]]; then
|
||||
print_warning "servers/ directory exists but contains no server directories"
|
||||
else
|
||||
print_success "Found $SERVER_COUNT MCP server(s)"
|
||||
|
||||
# Check if servers are referenced in plugin.json
|
||||
SERVERS_IN_JSON=$(jq -r '.mcpServers // {} | length' "$PLUGIN_JSON")
|
||||
if [[ $SERVERS_IN_JSON -eq 0 ]]; then
|
||||
print_warning "MCP servers found but not configured in plugin.json"
|
||||
fi
|
||||
|
||||
# Validate each server
|
||||
while IFS= read -r server_dir; do
|
||||
SERVER_NAME=$(basename "$server_dir")
|
||||
print_info "Validating MCP server: $SERVER_NAME"
|
||||
|
||||
# Check for server implementation
|
||||
if [[ -f "$server_dir/server.py" ]]; then
|
||||
print_success "Found Python server implementation"
|
||||
|
||||
# Check for pyproject.toml
|
||||
if [[ ! -f "$server_dir/pyproject.toml" ]]; then
|
||||
print_warning "Server $SERVER_NAME missing pyproject.toml"
|
||||
fi
|
||||
elif [[ -f "$server_dir/index.js" ]] || [[ -f "$server_dir/index.ts" ]]; then
|
||||
print_success "Found Node.js server implementation"
|
||||
|
||||
# Check for package.json
|
||||
if [[ ! -f "$server_dir/package.json" ]]; then
|
||||
print_warning "Server $SERVER_NAME missing package.json"
|
||||
fi
|
||||
else
|
||||
print_warning "Server $SERVER_NAME missing server implementation file"
|
||||
fi
|
||||
done < <(find "$SERVERS_DIR" -mindepth 1 -maxdepth 1 -type d)
|
||||
fi
|
||||
else
|
||||
print_info "No servers/ directory found"
|
||||
fi
|
||||
|
||||
# Check 12: Check for common files
|
||||
print_check "Checking for common files"
|
||||
|
||||
if [[ ! -f "$PLUGIN_DIR/.gitignore" ]]; then
|
||||
print_warning "No .gitignore found"
|
||||
else
|
||||
print_success ".gitignore exists"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$PLUGIN_DIR/CHANGELOG.md" ]]; then
|
||||
print_warning "No CHANGELOG.md found (recommended for versioning)"
|
||||
else
|
||||
print_success "CHANGELOG.md exists"
|
||||
fi
|
||||
|
||||
# Check 13: Git repository
|
||||
print_check "Checking git repository"
|
||||
if [[ ! -d "$PLUGIN_DIR/.git" ]]; then
|
||||
print_info "Not a git repository"
|
||||
else
|
||||
print_success "Git repository initialized"
|
||||
|
||||
# Check for uncommitted changes
|
||||
if ! git -C "$PLUGIN_DIR" diff-index --quiet HEAD -- 2>/dev/null; then
|
||||
print_info "Repository has uncommitted changes"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo
|
||||
echo -e "${BLUE}╔════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ Validation Summary ║${NC}"
|
||||
echo -e "${BLUE}╚════════════════════════════════════════════╝${NC}"
|
||||
echo
|
||||
|
||||
echo -e "${CYAN}Checks Performed:${NC} $CHECKS"
|
||||
|
||||
if [[ $ERRORS -gt 0 ]]; then
|
||||
echo -e "${RED}Errors Found:${NC} $ERRORS"
|
||||
fi
|
||||
|
||||
if [[ $WARNINGS -gt 0 ]]; then
|
||||
echo -e "${YELLOW}Warnings Found:${NC} $WARNINGS"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# Convert warnings to errors in strict mode
|
||||
if [[ "$STRICT_MODE" == "true" && $WARNINGS -gt 0 ]]; then
|
||||
ERRORS=$((ERRORS + WARNINGS))
|
||||
WARNINGS=0
|
||||
echo -e "${YELLOW}(Strict mode: warnings treated as errors)${NC}"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Exit with appropriate code
|
||||
if [[ $ERRORS -eq 0 && $WARNINGS -eq 0 ]]; then
|
||||
echo -e "${GREEN}✓ Validation passed! Plugin is ready to use.${NC}"
|
||||
exit 0
|
||||
elif [[ $ERRORS -eq 0 ]]; then
|
||||
echo -e "${YELLOW}⚠ Validation passed with warnings.${NC}"
|
||||
echo -e "${YELLOW} Consider addressing warnings before distribution.${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Validation failed!${NC}"
|
||||
echo -e "${RED} Please fix errors before using this plugin.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user